diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cdb1ed76..69c85c35 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,7 +9,7 @@ plugins {
}
android {
- namespace = "bums.lunatic.launcher"
+ namespace = "bums.lunatic.launcher"
compileSdk = 35
defaultConfig {
@@ -115,6 +115,15 @@ android {
doNotStrip("**/libaria2c.zip.so")
doNotStrip("**/libffmpeg.zip.so")
doNotStrip("**/libpython.zip.so")
+ jniLibs {
+ pickFirsts.add("lib/**/libc++_shared.so")
+ pickFirsts.add("lib/**/libavcodec.so")
+ pickFirsts.add("lib/**/libavfilter.so")
+ pickFirsts.add("lib/**/libavformat.so")
+ pickFirsts.add("lib/**/libavutil.so")
+ pickFirsts.add("lib/**/libswresample.so")
+ pickFirsts.add("lib/**/libswscale.so")
+ }
}
}
@@ -126,6 +135,7 @@ dependencies {
"dir" to "libs",
"include" to listOf("*.aar", "*.jar"),
)))
+ implementation(project(":gdrive"))
val kotlinVersion: String? by extra
val realmVersion = "2.0.0"
implementation ("androidx.appcompat:appcompat:1.7.1")
@@ -196,6 +206,28 @@ dependencies {
// implementation ("me.everything:providers-core:1.0.1")
// implementation ("androidx.window:window:1.0.0")
// implementation("io.github.vaneproject:hanguleditor:1.0.0")
+
+
+// implementation ("androidx.concurrent:concurrent-listenablefuture-ktx:1.1.0")
+ implementation ("com.google.guava:guava:33.2.1-jre")
+ val camerax_version = "1.3.0" // 안정적인 최신 버전 사용
+
+ // CameraX 코어 라이브러리
+ implementation("androidx.camera:camera-core:$camerax_version")
+ implementation("androidx.camera:camera-camera2:$camerax_version")
+
+ // CameraX Lifecycle 라이브러리 (Fragment/Activity 생명주기 연결)
+ implementation("androidx.camera:camera-lifecycle:$camerax_version")
+
+ // CameraX View 라이브러리 (PreviewView 등 UI 구성 요소)
+ implementation("androidx.camera:camera-view:$camerax_version")
+
+ // (선택) CameraX 비디오 라이브러리
+ implementation("androidx.camera:camera-video:$camerax_version")
+
+ // (선택) CameraX 확장 라이브러리 (보케, HDR 등)
+ implementation("androidx.camera:camera-extensions:$camerax_version")
+
constraints {
// ⚠️ 이 버전을 프로젝트 루트의 build.gradle.kts에 정의된 kotlinVersion 값과 정확히 일치시키세요.
val targetKotlinVersion = "2.0.20"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 37f5d6b5..9edb2e9f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -58,11 +58,9 @@
-
-
-
-
+
+
-1 && it.id != R.id.dl_video }.forEach { it.visibility = visibility }
+ decoViews.filter { it.id > -1 && it.id != R.id.btn_dl_video }.forEach { it.visibility = visibility }
}
open fun loadUrl(url: String, param: String? = null) {
@@ -212,7 +212,7 @@ open class GeckoWeb @JvmOverloads constructor(
fun checkIfDownloadable(url: String) {
// UI 초기화
post {
- decoViews.firstOrNull { it.id == R.id.dl_video }?.let {
+ decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let {
it.setOnClickListener {}
it.visibility = GONE
}
@@ -242,7 +242,7 @@ open class GeckoWeb @JvmOverloads constructor(
val videoInfo = YoutubeDL.getInstance().getInfo(request)
if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) {
post {
- decoViews.firstOrNull { it.id == R.id.dl_video }?.let { view ->
+ decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view ->
view.setOnClickListener { videoDownload(url) }
view.visibility = VISIBLE
}
@@ -320,10 +320,11 @@ open class GeckoWeb @JvmOverloads constructor(
// 외부 뷰 업데이트
decoViews.forEach { view ->
+ if (view == null) return@forEach // 뷰가 null이면 건너뛰어 크래시 방지
when(view.id) {
- R.id.back -> view.setOnClickListener { session.goBack() }
- R.id.current_address -> (view as? TextView)?.apply { tag = currentTitle; text = url }
- R.id.reload -> view.setOnClickListener { session.reload() }
+ R.id.btn_back -> view.setOnClickListener { session.goBack() }
+ R.id.tv_address -> (view as? TextView)?.apply { tag = currentTitle; text = url }
+ R.id.btn_reload -> view.setOnClickListener { session.reload() }
}
}
// scrollState = 0
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 3621983c..8bf4b0b4 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt
@@ -164,7 +164,7 @@ open class NeoRssActivity : CommonActivity() {
}
else {
- val currentFragment = fragmentMap.values.find { it.isAdded && (!it.isHidden || it.isVisible) }
+ val currentFragment = supportFragmentManager.fragments.find { it.isAdded && (!it.isHidden || it.isVisible) }
if (currentFragment is KeyEventHandler) {
if (currentFragment.onKeyEvent(ev)) {
return true
@@ -338,28 +338,16 @@ open class NeoRssActivity : CommonActivity() {
}
return@setOnTouchListener false
}
- binding.floatingActionMenu.setOnMenuButtonClickListener { v->
- Blog.LOGE("v >> ${v}")
- showContents(v.id)
- }
+// binding.floatingActionMenu.setOnMenuButtonClickListener { v->
+// Blog.LOGE("v >> ${v}")
+// showContents(v.id)
+// }
if (intent?.action == Intent.ACTION_WEB_SEARCH) {
openWithIntent(intent)
}
val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)
binding.root.setPointerIcon(nullCursor)
- binding.share.setOnClickListener {
- if (binding.currentAddress.text.length > 5) {
- val sendIntent: Intent = Intent().apply {
- setAction(Intent.ACTION_SEND)
- (binding.currentAddress.tag as? String)?.let{putExtra(Intent.EXTRA_TITLE, it)}
- putExtra(Intent.EXTRA_TEXT, binding.currentAddress.text)
- setType("text/plain")
- }
- val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기")
- startActivity(shareIntent)
- }
- }
showContents(R.id.btn_info)
}
@@ -377,32 +365,31 @@ open class NeoRssActivity : CommonActivity() {
private var lastTouchY = 0f
- private val fragmentMap = mutableMapOf()
+// private val fragmentMap = mutableMapOf()
fun showContents(id : Int) {
- binding.fragmentLayer.visibility = View.VISIBLE
- binding.fragmentContainer.visibility = View.VISIBLE
- binding.controllPanel.visibility = View.VISIBLE
- binding.floatingActionMenu.visibility = View.VISIBLE
+ if (id == View.NO_ID) {
+ Blog.LOGE("무효한 ID(-1) 호출로 인해 showContents를 취소합니다.")
+ return
+ }
+
+ Blog.LOGE("targetFragment id ${id}")
+ // 1. 모든 관련 레이어 가시성 확보
+ binding.fragmentContainer.isVisible = true
val transaction = supportFragmentManager.beginTransaction()
-
- fragmentMap.values.forEach { fragment ->
- if (fragment.isAdded) {
- transaction.hide(fragment)
+ val tagKey = "TAG_$id"
+ Blog.LOGE("targetFragment id ${id} tagKey ${tagKey}")
+ // 2. 매니저에 등록된 모든 프래그먼트를 찾아서 숨김 (중복 방지)
+ val allFragments = supportFragmentManager.fragments
+ for (f in allFragments) {
+ if (f.isAdded && f.isVisible) {
+ transaction.hide(f)
}
}
- // 2. 요청된 ID에 해당하는 프래그먼트가 이미 생성되었는지 확인
- var targetFragment = fragmentMap[id]
- if (targetFragment == null) {
- targetFragment = supportFragmentManager.findFragmentByTag("TAG_$id")
-
- // 시스템이 복구해 놓은 게 있다면, 맵에 다시 등록(싱크 맞추기)
- if (targetFragment != null) {
- fragmentMap[id] = targetFragment
- }
- }
+ // 3. 대상 프래그먼트 찾기 (findFragmentByTag 우선)
+ var targetFragment = supportFragmentManager.findFragmentByTag(tagKey)
if (targetFragment == null) {
// 처음 호출되는 메뉴라면 인스턴스 생성 및 추가
@@ -426,10 +413,11 @@ open class NeoRssActivity : CommonActivity() {
}
targetFragment?.let {
- fragmentMap[id] = it
- transaction.add(R.id.fragment_container, it, "TAG_$id")
+ Blog.LOGE("targetFragment id ${id} key : ${tagKey}, instance ${targetFragment}")
+ transaction.add(R.id.fragment_container, it, tagKey)
}
} else {
+ Blog.LOGE("targetFragment id ${id} key : ${tagKey}, instance ${targetFragment}")
// 이미 생성된 프래그먼트가 있다면 다시 보여줌
transaction.show(targetFragment)
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt
index 85d0e45d..4dbf175a 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt
@@ -220,11 +220,11 @@ internal class RssHome : Fragment() , KeyEventHandler {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (isUp) {
- if (binding.geckoWeb.scrollState > 0) {
+ if (binding.lunaticBrowser.geckoWeb.scrollState > 0) {
// 사용자가 손으로 끝까지 내렸든, 볼륨키로 내렸든 상관없이 이곳에 도달함
doNextPage()
} else {
- binding.geckoWeb.pageDown()
+ binding.lunaticBrowser.geckoWeb.pageDown()
}
}
true
@@ -232,10 +232,10 @@ internal class RssHome : Fragment() , KeyEventHandler {
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (isUp) {
- if (binding.geckoWeb.scrollState < 0) {
- binding.geckoWeb.session?.goBack()
+ if (binding.lunaticBrowser.geckoWeb.scrollState < 0) {
+ binding.lunaticBrowser.geckoWeb.session?.goBack()
} else {
- binding.geckoWeb.pageUp()
+ binding.lunaticBrowser.geckoWeb.pageUp()
}
}
true
@@ -261,7 +261,7 @@ internal class RssHome : Fragment() , KeyEventHandler {
}
}
fun searchKeyword() {
- binding.geckoWeb.visibility = View.GONE
+ binding.lunaticBrowser.visibility = View.GONE
val bottomSheet = SearchBottomSheet()
bottomSheet.listener = object : OnSearchListener{
override fun onSearch(
@@ -280,11 +280,11 @@ internal class RssHome : Fragment() , KeyEventHandler {
fun ask() {
- binding.geckoWeb.visibility = View.GONE
+ binding.lunaticBrowser.visibility = View.GONE
val bottomSheet = WebBottomSheet()
bottomSheet.listener = object : WebBottomSheet.OnGoToWebListener{
override fun enterSearch() {
- binding.geckoWeb.sendSearchDo()
+ binding.lunaticBrowser.geckoWeb.sendSearchDo()
}
override fun onGotoWeb(
keyword: String,
@@ -294,56 +294,56 @@ internal class RssHome : Fragment() , KeyEventHandler {
when (category) {
RssDataType.ANYTHING -> {
- if (binding.geckoWeb.lastedUrl?.contains("search.brave") == true) {
- binding.geckoWeb.sendSearch(keyword)
+ if (binding.lunaticBrowser.geckoWeb.lastedUrl?.contains("search.brave") == true) {
+ binding.lunaticBrowser.geckoWeb.sendSearch(keyword)
} else {
- binding.geckoWeb.loadUrl("https://search.brave.com/", if (keyword.length ?: 0 > 0) { "search?q=${keyword}" } else { null })
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://search.brave.com/", if (keyword.length ?: 0 > 0) { "search?q=${keyword}" } else { null })
}
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.geckoWeb.privateMode = false
}
RssDataType.GOOGLE -> {
- if (binding.geckoWeb.lastedUrl?.contains("www.google") == true) {
- binding.geckoWeb.sendSearch(keyword)
+ if (binding.lunaticBrowser.geckoWeb.lastedUrl?.contains("www.google") == true) {
+ binding.lunaticBrowser.geckoWeb.sendSearch(keyword)
} else {
- binding.geckoWeb.loadUrl("https://www.google.com/", if (keyword.length ?: 0 > 0) { "search?q=${keyword}" } else { null })
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://www.google.com/", if (keyword.length ?: 0 > 0) { "search?q=${keyword}" } else { null })
}
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.geckoWeb.privateMode = false
}
RssDataType.NAMU -> {
- if (binding.geckoWeb.lastedUrl?.contains("namu.wiki") == true) {
- binding.geckoWeb.sendSearch(keyword)
+ if (binding.lunaticBrowser.geckoWeb.lastedUrl?.contains("namu.wiki") == true) {
+ binding.lunaticBrowser.geckoWeb.sendSearch(keyword)
} else {
- binding.geckoWeb.loadUrl("https://namu.wiki", if (keyword.length ?: 0 > 0) {"/Search?q=${keyword}"} else {null})
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://namu.wiki", if (keyword.length ?: 0 > 0) {"/Search?q=${keyword}"} else {null})
}
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.geckoWeb.privateMode = false
}
RssDataType.NAVER -> {
- if (binding.geckoWeb.lastedUrl?.contains("naver") == true) {
- binding.geckoWeb.sendSearch(keyword)
+ if (binding.lunaticBrowser.geckoWeb.lastedUrl?.contains("naver") == true) {
+ binding.lunaticBrowser.geckoWeb.sendSearch(keyword)
} else {
- binding.geckoWeb.loadUrl("https://search.naver.com/", if (keyword.length ?: 0 > 0) {"search.naver?where=nexearch&query==${keyword}"} else {null})
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://search.naver.com/", if (keyword.length ?: 0 > 0) {"search.naver?where=nexearch&query==${keyword}"} else {null})
}
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.geckoWeb.privateMode = false
}
RssDataType.NEWSFEED -> {
- if (binding.geckoWeb.lastedUrl?.contains("news.google") == true) {
- binding.geckoWeb.sendSearch(keyword)
+ if (binding.lunaticBrowser.geckoWeb.lastedUrl?.contains("news.google") == true) {
+ binding.lunaticBrowser.geckoWeb.sendSearch(keyword)
} else {
- binding.geckoWeb.loadUrl("https://news.google.com/", if (keyword.length ?: 0 > 0) {"search?q=${keyword}&hl=ko&gl=KR&ceid=KR%3Ako"} else {null})
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://news.google.com/", if (keyword.length ?: 0 > 0) {"search?q=${keyword}&hl=ko&gl=KR&ceid=KR%3Ako"} else {null})
}
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.geckoWeb.privateMode = false
}
RssDataType.NEWS -> {
- if (binding.geckoWeb.lastedUrl?.contains("bigkinds") == true) {
- binding.geckoWeb.sendSearch(keyword)
+ if (binding.lunaticBrowser.geckoWeb.lastedUrl?.contains("bigkinds") == true) {
+ binding.lunaticBrowser.geckoWeb.sendSearch(keyword)
} else {
- binding.geckoWeb.loadUrl("https://www.bigkinds.or.kr/", if (keyword.length ?: 0 > 0) {"search?q=${keyword}&hl=ko&gl=KR&ceid=KR%3Ako"} else {null})
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://www.bigkinds.or.kr/", if (keyword.length ?: 0 > 0) {"search?q=${keyword}&hl=ko&gl=KR&ceid=KR%3Ako"} else {null})
}
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.geckoWeb.privateMode = false
}
RssDataType.PRIVATE -> {
- binding.geckoWeb.privateMode = privateMode
- binding.geckoWeb.loadUrl("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=", if (keyword.length ?: 0 > 0) {"/?searchTerm=${keyword}"} else {null})
+ binding.lunaticBrowser.geckoWeb.privateMode = privateMode
+ binding.lunaticBrowser.geckoWeb.loadUrl("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=", if (keyword.length ?: 0 > 0) {"/?searchTerm=${keyword}"} else {null})
}
else -> {
@@ -365,7 +365,7 @@ internal class RssHome : Fragment() , KeyEventHandler {
}
appendReadCount(it, 1, false)
binding.layoutRssSummary.title.setOnLongClickListener {
- currentRss?.originPage?.let { binding.geckoWeb.loadUrl(it)}
+ currentRss?.originPage?.let { binding.lunaticBrowser.geckoWeb.loadUrl(it)}
binding.layoutRssSummary.root.visibility = View.GONE
true
}
@@ -437,12 +437,15 @@ internal class RssHome : Fragment() , KeyEventHandler {
@SuppressLint("SimpleDateFormat")
fun openGecko(rssData: RssData? = null) {
binding.layoutRssSummary.root.visibility = View.GONE
+ binding.lunaticBrowser.visibility = View.GONE
+ binding.lunaticBrowser.setupForRssHome()
if (rssData?.category()?.equals(RssDataType.PRIVATE) == true && rssData?.getMagnet().isNullOrEmpty()) {
openSummary(rssData)
} else if (rssData?.originPage?.isNotEmpty() == true) {
rssData?.let { rss ->
currentRss = rss
- binding.geckoWeb.privateMode = false
+ binding.lunaticBrowser.visibility = View.VISIBLE
+ binding.lunaticBrowser.geckoWeb.privateMode = false
appendReadCount(rss, 1, false)
rss?.originPage?.let { rssId->
synchronized(lasted) {
@@ -450,7 +453,7 @@ internal class RssHome : Fragment() , KeyEventHandler {
lasted.removeAll { target -> target.originPage.equals(rssId) }
}
}
- binding.geckoWeb.loadUrl(rssId)
+ binding.lunaticBrowser.geckoWeb.loadUrl(rssId)
}
}
} else {
@@ -487,17 +490,12 @@ internal class RssHome : Fragment() , KeyEventHandler {
return@setOnTouchListener false
}
}
- binding.vote.setOnClickListener {
- if (binding.geckoWeb.isVisible) {
- vote()
- }
- }
if (openQuery.length > 0) {
- binding.geckoWeb.loadUrl("https://www.google.com/search?q=${openQuery}")
+ binding.lunaticBrowser.geckoWeb.loadUrl("https://www.google.com/search?q=${openQuery}")
openQuery = ""
}
- binding.geckoWeb?.mOnSave = object : GeckoWeb.OnSave{
+ binding.lunaticBrowser.geckoWeb?.mOnSave = object : GeckoWeb.OnSave{
override fun saved() {
currentRss?.originPage.let {
Blog.LOGE("Arrow Center Click")
@@ -516,41 +514,7 @@ internal class RssHome : Fragment() , KeyEventHandler {
}
}
}
- binding.hide.setOnClickListener {
- if (binding.geckoWeb.isVisible) {
- WorkersDb.getRealm().apply {
- writeBlocking {
- currentRss?.originPage?.let {
- val result = query().query("originPage == $0", it).find()
- if (result.size > 0) {
- result.forEach {
- if (it.vote) {
- it.vote = false
- }
- it.hide = true
- }
- }
- }
- }
- }
- doNextPage()
- }
- }
- binding.home.setOnClickListener {
- if (binding.geckoWeb.isVisible || binding.layoutRssSummary.root.isVisible) {
- binding.geckoWeb.visibility = View.GONE
- binding.layoutRssSummary.root.visibility = View.GONE
- } else {
- queryInfos()
- }
- }
-
-
- binding.bookmark.setOnClickListener {
- binding.layoutRssSummary.root.visibility = View.GONE
- queryVotes()
- }
binding.layoutRssSummary.link.setOnClickListener {
(it.tag as? RssData)?.let {
@@ -573,46 +537,113 @@ internal class RssHome : Fragment() , KeyEventHandler {
queryInfos()
- binding.geckoWeb.progress = binding.progressBar
- binding.geckoWeb.jxInteface = { jxEvent ->
- when (jxEvent) {
- JxEvent.SCROLL_UP -> binding.geckoWeb.sendScrollDown(false)
- JxEvent.SCROLL_DOWN -> binding.geckoWeb.sendScrollDown(true)
- JxEvent.ON_CLICK -> {
- binding.geckoWeb.visibility = View.GONE
- }
+ val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL)
+ binding.root.setPointerIcon(nullCursor)
- JxEvent.SWIPE_LEFT -> {
+ binding.lunaticBrowser.let { browser ->
+ // 1. 상단 바 RssHome 전용 기능 연결
+ browser.binding.btnHome.setOnClickListener {
+// if (binding.lunaticBrowser.isVisible || binding.layoutRssSummary.root.isVisible) {
+ binding.lunaticBrowser.visibility = View.GONE
+ binding.layoutRssSummary.root.visibility = View.GONE
+// } else {
+ queryInfos()
+// }
+ }
+
+ browser.binding.btnBookmark.setOnClickListener {
+ binding.layoutRssSummary.root.visibility = View.GONE
+ binding.lunaticBrowser.visibility = View.GONE
+ queryVotes()
+ }
+
+ browser.binding.btnSearch.setOnClickListener { searchKeyword() }
+ browser.binding.btnSearch.setOnLongClickListener{
+ ask()
+ true
+ }
+ browser.binding.btnHide.setOnClickListener {
+ if (binding.lunaticBrowser.isVisible) {
+ WorkersDb.getRealm().apply {
+ writeBlocking {
+ currentRss?.originPage?.let {
+ val result = query().query("originPage == $0", it).find()
+ if (result.size > 0) {
+ result.forEach {
+ if (it.vote) {
+ it.vote = false
+ }
+ it.hide = true
+ }
+ }
+ }
+ }
+ }
doNextPage()
}
+ }
- JxEvent.SWIPE_RIGHT -> {
+ browser.binding.btnVote.setOnClickListener {
+ if (binding.lunaticBrowser.isVisible) {
vote()
}
}
+
+ // 2. GeckoWeb 콜백 및 설정 유지
+ browser.geckoWeb.jxInteface = { jxEvent ->
+ when (jxEvent) {
+ JxEvent.SWIPE_LEFT -> doNextPage()
+ JxEvent.SWIPE_RIGHT -> vote()
+ else -> {}
+ }
+ }
}
- val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL)
- binding.root.setPointerIcon(nullCursor)
- binding.search.setOnClickListener { searchKeyword() }
- binding.search.setOnLongClickListener{
- ask()
- true
+ binding.lunaticBrowser.geckoWeb.restoreSessionState()
+
+ binding.let { b ->
+ // 전체 탭
+ b.tabAll.setOnClickListener {
+ highlightTab(it as TextView)
+ queryInfos() // 기존 전체 쿼리 호출
+ }
+
+ // 북마크(Voted) 탭
+ b.tabVoted.setOnClickListener {
+ highlightTab(it as TextView)
+ queryVotes() // 기존 보트 쿼리 호출
+ }
+
+ // 프라이빗 탭
+ b.tabPrivate.setOnClickListener {
+ highlightTab(it as TextView)
+ queryPrevate() // 기존 프라이빗 쿼리 호출
+ }
+ b.tabAll.performClick()
+
+ b.tabSerarch.setOnClickListener {
+ searchKeyword()
+ }
+ b.tabSerarch.setOnLongClickListener {
+ ask()
+ true
+ }
+
}
- binding.geckoWeb.decoViews.add(binding.hide)
- binding.geckoWeb.decoViews.add(binding.vote)
- binding.geckoWeb.decoViews.add(binding.progressBar)
- (activity as? NeoRssActivity)?.let { activity ->
- binding.geckoWeb.decoViews.add(activity.findViewById(R.id.current_address))
- binding.geckoWeb.decoViews.add(activity.findViewById(R.id.back))
- binding.geckoWeb.decoViews.add(activity.findViewById(R.id.reload))
- binding.geckoWeb.decoViews.add(activity.findViewById(R.id.dl_video))
- }
- binding.geckoWeb.restoreSessionState()
+
+ // 탭 강조를 위한 간단한 함수
+
return binding.root
}
+ private fun highlightTab(selected: TextView) {
+ val tabs = listOf(binding.tabAll, binding.tabVoted, binding.tabPrivate)
+ tabs.forEach {
+ it.setTextColor(resources.getColor(R.color.finestSilver))
+ }
+ selected.setTextColor(resources.getColor(R.color.white))
+ }
fun vote() {
- binding.geckoWeb?.saveMd(true)
+ binding.lunaticBrowser.geckoWeb?.saveMd(true)
}
// TokiFragment 또는 GeckoView를 사용하는 프래그먼트 내부
@@ -620,7 +651,7 @@ internal class RssHome : Fragment() , KeyEventHandler {
super.onHiddenChanged(hidden)
if (hidden) {
// 💡 화면에서 사라질 때: 타이머 중지 및 애니메이션 중지
- binding.geckoWeb?.onPause()
+ binding.lunaticBrowser.geckoWeb?.onPause()
// 일반 WebView라면: webView.onPause() 및 webView.pauseTimers()
} else {
// 💡 다시 나타날 때: 다시 시작
@@ -960,26 +991,6 @@ internal class RssHome : Fragment() , KeyEventHandler {
}
fun randomOrNull() : RssData? = lasted.randomOrNull()
-// fun rett(imageView: ImageView,imageUrl: String){
-// // OkHttp로 직접 이미지 다운로드 후
-// val request = Request.Builder().url(imageUrl).build()
-// val client = OkHttpClient()
-// client.newCall(request).enqueue(object : Callback {
-// override fun onFailure(call: Call, e: IOException) {
-// // 실패 시 기본 이미지 처리 or 로깅
-//
-// }
-//
-// override fun onResponse(call: Call, response: Response) {
-// response.body?.byteStream()?.let { inputStream ->
-// val bitmap = BitmapFactory.decodeStream(inputStream)
-// mainHandler.post({
-// imageView.setImageBitmap(bitmap)
-// })
-// }
-// }
-// })
-// }
}
var toast: Toast? = null
fun Context.toast(string: String) {
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/SystemStatusFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/SystemStatusFragment.kt
index 9d147908..ae17def5 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/home/SystemStatusFragment.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/SystemStatusFragment.kt
@@ -38,6 +38,9 @@ import bums.lunatic.launcher.databinding.FragmentSystemStatusBinding
import bums.lunatic.launcher.utils.Blog
import kotlinx.coroutines.*
import java.util.Calendar
+import com.google.common.util.concurrent.ListenableFuture
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.core.content.ContextCompat
class SystemStatusFragment : Fragment() {
@@ -120,7 +123,8 @@ class SystemStatusFragment : Fragment() {
}
binding.progressBattery.setOnClickListener { binding.textBattery.performClick() }
}
-
+ private var isFlashOn = false
+ private lateinit var cameraManager: android.hardware.camera2.CameraManager
private fun setupQuickControls() {
binding.btnQuickWifi.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -177,9 +181,109 @@ class SystemStatusFragment : Fragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
+ cameraManager = requireContext().getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
+
+ binding.btnQuickFlash.setOnClickListener {
+ toggleFlashlight()
+ }
+ binding.btnQuickMirror.setOnClickListener {
+ if (allPermissionsGranted()) {
+ showMirrorDialog() // 권한이 있으면 거울 실행
+ } else {
+ // 권한이 없으면 사용자에게 요청
+ requestPermissions(arrayOf(CAMERA_PERMISSION), REQUEST_CODE_PERMISSIONS)
+ }
+ }
updateQuickControlUI()
}
+ private val CAMERA_PERMISSION = android.Manifest.permission.CAMERA
+ private val REQUEST_CODE_PERMISSIONS = 1001
+ override fun onRequestPermissionsResult(
+ requestCode: Int, permissions: Array, grantResults: IntArray
+ ) {
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ showMirrorDialog()
+ } else {
+ Toast.makeText(requireContext(), "거울 기능을 사용하려면 카메라 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private var flashTimerJob: Job? = null
+
+ private fun toggleFlashlight() {
+ try {
+ val cameraId = cameraManager.cameraIdList[0]
+ isFlashOn = !isFlashOn
+ cameraManager.setTorchMode(cameraId, isFlashOn)
+
+ // 기존 타이머가 있다면 취소
+ flashTimerJob?.cancel()
+
+ if (isFlashOn) {
+ binding.btnQuickFlash.text = "🔦 ON"
+ binding.btnQuickFlash.setTextColor(Color.YELLOW)
+
+ // 30초 후 자동 종료 타이머 시작
+ flashTimerJob = CoroutineScope(Dispatchers.Main).launch {
+ delay(30000L) // 30초
+ if (isFlashOn) {
+ isFlashOn = false
+ cameraManager.setTorchMode(cameraId, false)
+ binding.btnQuickFlash.text = "🔦 조명"
+ binding.btnQuickFlash.setTextColor(Color.WHITE)
+ Toast.makeText(context, "배터리 보호를 위해 조명을 껐습니다.", Toast.LENGTH_SHORT).show()
+ }
+ }
+ } else {
+ binding.btnQuickFlash.text = "🔦 조명"
+ binding.btnQuickFlash.setTextColor(Color.WHITE)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ // 2. 권한 확인 함수
+ private fun allPermissionsGranted() = androidx.core.content.ContextCompat.checkSelfPermission(
+ requireContext(), CAMERA_PERMISSION) == android.content.pm.PackageManager.PERMISSION_GRANTED
+
+ private fun showMirrorDialog() {
+ val dialog = android.app.Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen)
+ val previewView = androidx.camera.view.PreviewView(requireContext())
+ dialog.setContentView(previewView)
+
+ val cameraProviderFuture = androidx.camera.lifecycle.ProcessCameraProvider.getInstance(requireContext())
+
+ cameraProviderFuture.addListener({
+ val cameraProvider = cameraProviderFuture.get()
+ val preview = androidx.camera.core.Preview.Builder().build()
+
+ // 전면 카메라 선택
+ val cameraSelector = androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
+
+ try {
+ cameraProvider.unbindAll()
+ // 이 다이얼로그의 LifecycleOwner를 Fragment로 지정
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview)
+ preview.setSurfaceProvider(previewView.surfaceProvider)
+ } catch (e: Exception) {
+ Blog.LOGE("거울 실행 실패: ${e.message}")
+ }
+ }, androidx.core.content.ContextCompat.getMainExecutor(requireContext()))
+ CoroutineScope(Dispatchers.Main).launch {
+ delay(30000L)
+ if (dialog.isShowing) {
+ dialog.dismiss()
+ Toast.makeText(context, "거울을 종료합니다.", Toast.LENGTH_SHORT).show()
+ }
+ }
+ // 화면 터치 시 종료
+ previewView.setOnClickListener { dialog.dismiss() }
+ dialog.show()
+ }
private fun updateQuickControlUI() {
val ringerText = when (audioManager.ringerMode) {
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 2177a634..0cd57dee 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
@@ -292,7 +292,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
}
// Novels의 경우 VISIBLE 처리가 있었으나, GONE 처리 후 필요 시 메뉴를 보여주는 흐름으로 통합
if(contentsType == "book") {
- binding.menuWeb.visibility = VISIBLE
+ binding.lunaticBrowser.visibility = VISIBLE
}
}
}
@@ -312,7 +312,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
fun back() {
if (contentsType == "youtube") {
- binding.menuWeb.session?.goBack()
+ binding.lunaticBrowser.geckoWeb.session?.goBack()
} else {
actionNextEvent(false)
}
@@ -431,7 +431,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
super.onHiddenChanged(hidden)
if (hidden) {
// 💡 화면에서 사라질 때: 타이머 중지 및 애니메이션 중지
- binding.menuWeb?.onPause()
+ binding.lunaticBrowser.geckoWeb?.onPause()
// 일반 WebView라면: webView.onPause() 및 webView.pauseTimers()
} else {
// 💡 다시 나타날 때: 다시 시작
@@ -447,7 +447,47 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
): View {
Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}")
binding = BooktokiBinding.inflate(inflater)
- binding.menuWeb.let {
+ binding = BooktokiBinding.inflate(inflater)
+
+ // 💡 1. Toki용 레이아웃 설정
+ binding.lunaticBrowser.setupForToki()
+
+ // 💡 2. GeckoWeb 접근 및 설정
+ val geckoWeb = binding.lunaticBrowser.geckoWeb
+
+
+
+ // 💡 3. Toki 전용 버튼 클릭 리스너 연결
+ binding.lunaticBrowser.binding.btnList.setOnClickListener {
+ lastedUrl?.let {
+ Uri.parse(it).path?.let {
+ HistoryManager.getBookInfos(contentsType,processPageUrl(it), {
+ it?.let {
+ it.pages.sortBy { it.pathUrl }
+ Blog.LOGE("bind ing.btnList it >>> $it")
+ showList(it)
+ }
+ })
+ }
+ }
+ }
+
+ binding.lunaticBrowser.binding.btnSetting.setOnClickListener {
+ activity?.startActivity(Intent(requireContext(), Settings::class.java)) // 필요 시 경로수정
+ }
+
+ binding.lunaticBrowser.binding.btnHistory.setOnClickListener {
+ getHistory()?.let { showHistory(it) }
+ }
+
+ binding.lunaticBrowser.binding.btnHome.setOnClickListener {
+ binding.pagedLayer.visibility = GONE
+ binding.lunaticBrowser.visibility = VISIBLE
+ binding.lunaticBrowser.showBrowserAreaOnly(true)
+ goToHome()
+ }
+
+ geckoWeb.let {
it.sessionTag = webcontentsName
it.visibility = View.VISIBLE
it.lastDomain = getLastedDoamin()
@@ -462,33 +502,13 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
if (lastedUrl?.contains("youtube.com") == true) {
} else {
-// binding.menuWeb.postDelayed({
-//
-// Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}")
-// val message: JSONObject = JSONObject()
-// try {
-// message.put("type", "getList")
-// message.put("event", "sadsadds")
-//// message.put("tab", session.settings.screenId)
-// } catch (ex: JSONException) {
-// throw RuntimeException(ex)
-// }
-//
-// mPort!!.postMessage(message)
-//
-//
-// // 타입별 분기 처리 (기존 when 절 대체)
-// if (contentsType == "comics" || contentsType == "webtoon") {
-// lastInfo
-// }
-// binding.progress.visibility = GONE
-// }, 10L)
+
}
}
}
}
it.onPageStartCallback = { url ->
- binding.progress.visibility = VISIBLE
+// binding.lunaticBrowser.binding.progress.visibility = VISIBLE
}
it.onSessionStateChangeCallback = {sessionState ->
if (contentsType == "comics" || contentsType == "webtoon") {
@@ -508,15 +528,17 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
}
"NotRegistered" -> {
binding.pagedLayer.visibility = GONE
+ binding.lunaticBrowser.visibility = VISIBLE
}
"WebtoonContents"-> {
binding.pagedLayer.visibility = GONE
+ binding.lunaticBrowser.visibility = VISIBLE
}
"MSG" -> {
lPortMessage.msg?.let { Toast.makeText(requireContext(),it, Toast.LENGTH_SHORT).show() }
}
"SHOWVIEWER" -> {
- binding.progress.visibility = GONE
+ binding.lunaticBrowser.binding.internalProgressBar.visibility = GONE
}
"PRIVATES"->{
lPortMessage.privates?.let {
@@ -528,7 +550,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
}
}
- binding.progress.visibility = GONE
+ binding.lunaticBrowser.binding.internalProgressBar.visibility = GONE
} catch (e: Exception) {
e.printStackTrace()
}
@@ -539,7 +561,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
} else {
// url이 현재 로드된 주소입니다.
Blog.LOGE("GeckoView", "현재 주소: $url")
- Blog.LOGE("GeckoView", "현재 session: ${binding.menuWeb.session}")
+ Blog.LOGE("GeckoView", "현재 session: ${binding.lunaticBrowser.geckoWeb.session}")
url?.let { url ->
if (url.split("//").size > 1) {
url.replace("//", "/").replace("https:/", "https://").let {
@@ -570,44 +592,16 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
else -> {}
}
}
- (activity as? NeoRssActivity)?.let { activity ->
it.decoViews.clear()
- it.decoViews.add(activity.findViewById(R.id.current_address))
- it.decoViews.add(activity.findViewById(R.id.back))
- it.decoViews.add(activity.findViewById(R.id.reload))
- it.decoViews.add(activity.findViewById(R.id.dl_video))
- }
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.tv_address))
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_back))
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_reload))
+ it.decoViews.add(binding.lunaticBrowser.findViewById(R.id.btn_dl_video))
it.restoreSessionState()
}
- binding.btnList.setOnClickListener { v ->
- lastedUrl?.let {
- Uri.parse(it).path?.let {
- HistoryManager.getBookInfos(contentsType,processPageUrl(it), {
- it?.let {
- it.pages.sortBy { it.pathUrl }
- Blog.LOGE("bind ing.btnList it >>> $it")
- showList(it)
- }
- })
- }
- }
- }
- binding.btnSetting.setOnClickListener { v ->
- activity?.startActivity(Intent(requireContext(), Settings::class.java))
- }
- binding.btnHistory.setOnClickListener { v ->
- getHistory()?.let {
- showHistory(it)
- }
- }
-
- binding.btnHome.setOnClickListener { v ->
- binding.pagedLayer.visibility = GONE
- goToHome()
- }
val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL)
binding.root.setPointerIcon(nullCursor)
@@ -967,9 +961,9 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
contentsPageInfo.contents?.let { onLoadedContents(it) }
contentsPageInfo.chapterTitle?.let { onFindTitle(it) }
if (contentsPageInfo.pathUrl?.startsWith("http") == true) {
- binding.menuWeb.loadUrl(pathUrl)
+ binding.lunaticBrowser.geckoWeb.loadUrl(pathUrl)
} else {
- binding.menuWeb.loadUrl(getLastedDoamin() + contentsPageInfo.pathUrl!!)
+ binding.lunaticBrowser.geckoWeb.loadUrl(getLastedDoamin() + contentsPageInfo.pathUrl!!)
}
HistoryManager.save(HistoryItem().putHistory(contentsPageInfo, contentsPageInfo.pathUrl!!))
}
@@ -984,9 +978,10 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
applyCurrentBook(it)
} else if(lastInfo != null){
binding.pagedLayer.visibility = GONE
- binding.menuWeb.loadUrl(getLastedDoamin() + lastInfo!!.pageUrl)
+ binding.lunaticBrowser.visibility = VISIBLE
+ binding.lunaticBrowser.geckoWeb.loadUrl(getLastedDoamin() + lastInfo!!.pageUrl)
} else {
- binding.menuWeb.loadUrl(getLastedDoamin())
+ binding.lunaticBrowser.geckoWeb.loadUrl(getLastedDoamin())
}
}
}
@@ -1088,7 +1083,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
}
}
if (saveContinuation) {
- binding.menuWeb.postDelayed({
+ binding.lunaticBrowser.geckoWeb.postDelayed({
moveToNext(
currentPage?.pathUrl ?: lastedUrl?.toUri()?.path
)
@@ -1108,16 +1103,16 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
contents = (contents.replace("\\n", System.getProperty("line.separator")))
view.mPagedTextViewInterface = this@TokiFragment
if (lastInfo != null && lastedUrl?.endsWith(lastInfo!!.pageUrl) == true) {
- binding.progress.visibility = VISIBLE
+ binding.lunaticBrowser.binding.internalProgressBar.visibility = VISIBLE
binding.pagedLayer.postDelayed({
- binding.progress.visibility = GONE
+ binding.lunaticBrowser.binding.internalProgressBar.visibility = GONE
}, 1000)
}
applyReaderConfig()
activity?.runOnUiThread {
view.text = contents
view.visibility = VISIBLE
- binding.menuWeb.visibility = GONE
+ binding.lunaticBrowser.visibility = GONE
}
// view.forceUpdateUI()
lastedUrl?.let {
@@ -1139,7 +1134,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
HistoryManager.getBooPageInfoContentsSave(contentsType,it, contents)
}
if (saveContinuation) {
- binding.menuWeb.postDelayed({moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)}, 10000)
+ binding.lunaticBrowser.geckoWeb.postDelayed({moveToNext(currentPage?.pathUrl ?: lastedUrl?.toUri()?.path)}, 10000)
}
}
@@ -1155,8 +1150,8 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
var currentChapter: Int = 0
fun onFindTitle(contents: String) {
- binding.textviewTitle.text = contents
- binding.textviewTitle.setOnClickListener {
+ binding.lunaticBrowser.binding.tvTitle.text = contents
+ binding.lunaticBrowser.binding.tvTitle.setOnClickListener {
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("Title")
val input = EditText(requireContext())
@@ -1196,7 +1191,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
}
fun onStartLoad() {
- binding.progress.visibility = VISIBLE
+ binding.lunaticBrowser.binding.internalProgressBar.visibility = VISIBLE
}
fun completePageLoad(lastInfo: LastInfo) {
@@ -1305,6 +1300,6 @@ class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
}
private fun goToHome() {
- binding.menuWeb.loadUrl(getLastedDoamin())
+ binding.lunaticBrowser.geckoWeb.loadUrl(getLastedDoamin())
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt
index 414c11a9..f3ea9a23 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt
@@ -23,6 +23,8 @@ import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Bundle
import android.os.Environment
+import android.widget.Toast
+import androidx.lifecycle.lifecycleScope
import bums.lunatic.launcher.BuildConfig
import bums.lunatic.launcher.R
import bums.lunatic.launcher.common.CommonActivity
@@ -39,6 +41,10 @@ import bums.lunatic.launcher.settings.childs.HomeSettings
import bums.lunatic.launcher.settings.childs.Misc
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb
+import kr.gdrive.bums.lunatic.utils.BackupPayload
+import kr.gdrive.bums.lunatic.utils.GDriveBackupTask
+import kr.gdrive.bums.lunatic.utils.GDriveLoginManager
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -46,7 +52,11 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.query
+import kotlinx.coroutines.launch
import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
import kotlin.system.exitProcess
@@ -55,6 +65,9 @@ internal class SettingsActivity : CommonActivity() {
private lateinit var binding: SettingsActivityBinding
private val sourceCode = "https://github.com/iamrasel/lunar-launcher"
+ // 전역 변수로 매니저 선언 (콜백 처리)
+ private lateinit var loginManager: GDriveLoginManager
+
companion object {
@JvmStatic var settingsPrefs: SharedPreferences? = null
}
@@ -70,6 +83,49 @@ internal class SettingsActivity : CommonActivity() {
settingsPrefs = this.getSharedPreferences(PREFS_SETTINGS, 0)
+ loginManager = GDriveLoginManager(this) { isSuccess, account, errorMsg ->
+ if (isSuccess && account != null) {
+ updateUiForLoggedIn(account.email ?: "사용자")
+ } else {
+ updateUiForLoggedOut()
+ Blog.LOGE("로그인 에러: $errorMsg")
+ }
+ }
+
+ val currentAccount = loginManager.getSignedInAccount()
+ if (currentAccount != null) {
+ updateUiForLoggedIn(currentAccount.email ?: "사용자")
+ } else {
+ updateUiForLoggedOut()
+ }
+ binding.btnGoogleLogin.setOnClickListener {
+ if (loginManager.getSignedInAccount() == null) {
+ loginManager.signIn()
+ } else {
+ loginManager.signOut { updateUiForLoggedOut() }
+ }
+ }
+
+ // 2) 수동 백업 버튼
+ binding.btnManualBackup.setOnClickListener {
+ val account = loginManager.getSignedInAccount()
+ if (account != null) {
+ performManualBackup(account)
+ } else {
+ Toast.makeText(this, "먼저 구글에 로그인해주세요.", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+
+ binding.switchAutoBackup.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked && loginManager.getSignedInAccount() == null) {
+ Toast.makeText(this, "자동 백업을 켜려면 구글 로그인이 필요합니다.", Toast.LENGTH_SHORT).show()
+ binding.switchAutoBackup.isChecked = false
+ return@setOnCheckedChangeListener
+ }
+ // TODO: isChecked 값을 SharedPreference에 저장
+ }
+
/* launch child settings dialogs on button clicks */
// binding.timeDate.setOnClickListener {
// TopInfos().show(supportFragmentManager, BOTTOM_SHEET_TAG)
@@ -119,6 +175,46 @@ internal class SettingsActivity : CommonActivity() {
}
}
+ private fun updateUiForLoggedIn(email: String) {
+ binding.btnGoogleLogin.text = "연결 해제 ($email)"
+ binding.btnManualBackup.isEnabled = true
+ binding.switchAutoBackup.isEnabled = true
+ }
+
+ private fun updateUiForLoggedOut() {
+ binding.btnGoogleLogin.text = "구글 드라이브 연결"
+ binding.btnManualBackup.isEnabled = false
+ binding.switchAutoBackup.isChecked = false
+ binding.switchAutoBackup.isEnabled = false
+ }
+
+ private fun performManualBackup(account: GoogleSignInAccount) {
+ android.widget.Toast.makeText(this, "백업을 시작합니다...", Toast.LENGTH_SHORT).show()
+ binding.btnManualBackup.isEnabled = false
+
+ lifecycleScope.launch {
+ // DB 데이터 추출 (앱, 연락처 등)
+ val appsJson = "..." // WorkersDb에서 가져오기
+ val payload = BackupPayload(
+ manifestJson = """{"version": ${WorkersDb.schemaVersion}}""",
+ folderName = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()),
+ files = mapOf("apps_info.json" to appsJson)
+ )
+
+ // 백업 실행
+ val backupTask = GDriveBackupTask(this@SettingsActivity, account)
+ val result = backupTask.executeBackup(payload)
+
+ result.onSuccess { msg ->
+ Toast.makeText(this@SettingsActivity, msg, Toast.LENGTH_LONG).show()
+ }.onFailure {
+ Toast.makeText(this@SettingsActivity, "백업 실패", Toast.LENGTH_LONG).show()
+ }
+
+ binding.btnManualBackup.isEnabled = true
+ }
+ }
+
var cancelCount = Runnable{
clickCount = 0
}
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt b/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt
index 76246fb1..7468d0db 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt
@@ -25,6 +25,7 @@ import android.view.animation.Interpolator
import android.view.animation.OvershootInterpolator
import android.widget.ImageView
import android.widget.ImageView.ScaleType
+import android.widget.TextView
import bums.lunatic.launcher.R
import bums.lunatic.launcher.utils.CommonUtils
@@ -85,7 +86,7 @@ class FloatingActionMenu @JvmOverloads constructor(
private var mLabelsStyle = 0
private var mCustomTypefaceFromFont: Typeface? = null
var isIconAnimated: Boolean = true
- private var mImageToggle: ImageView? = null
+ private var mToggleView: View? = null
private var mMenuButtonShowAnimation: Animation? = null
private var mMenuButtonHideAnimation: Animation? = null
private var mImageToggleShowAnimation: Animation? = null
@@ -198,7 +199,7 @@ class FloatingActionMenu @JvmOverloads constructor(
attr.getInt(R.styleable.FloatingActionMenu_menu_animationDelayPerItem, 50)
mIcon = attr.getDrawable(R.styleable.FloatingActionMenu_menu_icon)
if (mIcon == null) {
- mIcon = getResources().getDrawable(R.drawable.fab_add)
+// mIcon = getResources().getDrawable(R.drawable.fab_add)
}
mLabelsSingleLine =
attr.getBoolean(R.styleable.FloatingActionMenu_menu_labels_singleLine, false)
@@ -314,15 +315,21 @@ class FloatingActionMenu @JvmOverloads constructor(
mMenuButton!!.mFabSize = mMenuFabSize
mMenuButton!!.updateBackground()
mMenuButton!!.labelText = mMenuLabelText
-
- mImageToggle = ImageView(getContext())
- mImageToggle?.scaleType = ScaleType.FIT_CENTER
- mImageToggle?.adjustViewBounds = true
- mImageToggle!!.setImageDrawable(mIcon)
-
addView(mMenuButton, super.generateDefaultLayoutParams())
- addView(mImageToggle)
+ if (mIcon != null) {
+ var imge = ImageView(getContext())
+ imge?.scaleType = ScaleType.FIT_CENTER
+ imge?.adjustViewBounds = true
+ imge?.setImageDrawable(mIcon)
+ mToggleView = imge
+ } else {
+ var v = TextView(getContext())
+ v?.text = mMenuLabelText
+ v?.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLabelsTextSize)
+ mToggleView = v
+ }
+ addView(mToggleView)
createDefaultIconAnimation()
}
@@ -342,14 +349,14 @@ class FloatingActionMenu @JvmOverloads constructor(
}
val collapseAnimator = ObjectAnimator.ofFloat(
- mImageToggle,
+ mToggleView,
"rotation",
collapseAngle,
CLOSED_PLUS_ROTATION
)
val expandAnimator = ObjectAnimator.ofFloat(
- mImageToggle,
+ mToggleView,
"rotation",
CLOSED_PLUS_ROTATION,
expandAngle
@@ -371,12 +378,12 @@ class FloatingActionMenu @JvmOverloads constructor(
mMaxButtonWidth = 0
var maxLabelWidth = 0
- measureChildWithMargins(mImageToggle, widthMeasureSpec, 0, heightMeasureSpec, 0)
+ measureChildWithMargins(mToggleView, widthMeasureSpec, 0, heightMeasureSpec, 0)
for (i in 0.. r - l - mMaxButtonWidth / 2 - paddingRight
+ else -> mMaxButtonWidth / 2 + paddingLeft
+ }
+
+ val openUp = mOpenDirection == OPEN_UP
+ val menuButtonTop = if (openUp) b - t - mMenuButton!!.measuredHeight - paddingBottom else paddingTop
+ val menuButtonLeft = buttonsHorizontalCenter - mMenuButton!!.measuredWidth / 2
+
+ if (mMenuButton!!.left == 0 && mMenuButton!!.top == 0) {
+ mMenuButton!!.layout(menuButtonLeft, menuButtonTop, menuButtonLeft + mMenuButton!!.measuredWidth, menuButtonTop + mMenuButton!!.measuredHeight)
+ }
+
+ // 💡 화면의 너비(r - l)와 높이(b - t)를 함께 넘겨줍니다.
+ layoutChildrenRelative(r - l, b - t)
}
- private fun doLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
- val buttonsHorizontalCenter = if (mLabelsPosition == LABELS_POSITION_LEFT)
- r - l - mMaxButtonWidth / 2 - getPaddingRight()
- else
- mMaxButtonWidth / 2 + getPaddingLeft()
- val openUp = mOpenDirection == OPEN_UP
+ private fun moveElements(v: View?, SetX: Float, SetY: Float, oldX: Float, oldY: Float) {
+ val parentView = (parent as ViewGroup).parent as View
+ // 💡 드래그 중에도 부모의 너비와 높이를 계산해 넘겨줍니다.
+ layoutChildrenRelative(parentView.width, parentView.height)
+ }
- val menuButtonTop = if (openUp)
- b - t - mMenuButton!!.getMeasuredHeight() - getPaddingBottom()
- else
- getPaddingTop()
- val menuButtonLeft = buttonsHorizontalCenter - mMenuButton!!.getMeasuredWidth() / 2
+ // 파라미터에 parentHeight 추가
+ private fun layoutChildrenRelative(parentWidth: Int, parentHeight: Int) {
+ val menuL = mMenuButton!!.x.toInt()
+ val menuT = mMenuButton!!.y.toInt()
+ val menuW = mMenuButton!!.measuredWidth
+ val menuH = mMenuButton!!.measuredHeight
+ val buttonsCenterX = menuL + menuW / 2
+ val buttonsCenterY = menuT + menuH / 2
- mMenuButton!!.layout(
- menuButtonLeft, menuButtonTop, menuButtonLeft + mMenuButton!!.getMeasuredWidth(),
- menuButtonTop + mMenuButton!!.getMeasuredHeight()
- )
+ // 💡 [핵심 복원] 메인 버튼이 화면 하단부에 있으면 위로(openUp = true), 상단부에 있으면 아래로 엽니다.
+ val openUp = buttonsCenterY > (parentHeight / 2)
- val imageLeft = buttonsHorizontalCenter - mImageToggle!!.getMeasuredWidth() / 2
- val imageTop =
- menuButtonTop + mMenuButton!!.getMeasuredHeight() / 2 - mImageToggle!!.getMeasuredHeight() / 2
+ // 1. 토글 아이콘 배치
+ val imageLeft = buttonsCenterX - mToggleView!!.measuredWidth / 2
+ val imageTop = menuT + menuH / 2 - mToggleView!!.measuredHeight / 2
+ mToggleView!!.layout(imageLeft, imageTop, imageLeft + mToggleView!!.measuredWidth, imageTop + mToggleView!!.measuredHeight)
+ mToggleView!!.translationX = 0f
+ mToggleView!!.translationY = 0f
- mImageToggle!!.layout(
- imageLeft, imageTop, imageLeft + mImageToggle!!.getMeasuredWidth(),
- imageTop + mImageToggle!!.getMeasuredHeight()
- )
+ val isMenuOnLeft = buttonsCenterX < parentWidth / 2
- var nextY = if (openUp)
- menuButtonTop + mMenuButton!!.getMeasuredHeight() + mButtonSpacing
- else
- menuButtonTop
+ // 💡 시작 높이: 위로 열릴 땐 메인 버튼의 Top, 아래로 열릴 땐 메인 버튼의 Bottom
+ var nextY = if (openUp) menuT else menuT + menuH
for (i in mButtonsCount - 1 downTo 0) {
val child = getChildAt(i)
-
- if (child === mImageToggle) continue
-
+ if (child === mToggleView) continue
val fab = child as FloatingActionButton
-
- if (fab.getVisibility() == GONE) continue
-
- val childX = buttonsHorizontalCenter - fab.getMeasuredWidth() / 2
- val childY = if (openUp) nextY - fab.getMeasuredHeight() - mButtonSpacing else nextY
+ if (fab.visibility == GONE) continue
if (fab != mMenuButton) {
- fab.layout(
- childX, childY, childX + fab.getMeasuredWidth(),
- childY + fab.getMeasuredHeight()
- )
+ val childX = buttonsCenterX - fab.measuredWidth / 2
- if (!mIsMenuOpening) {
- fab.hide(false)
+ // 💡 상/하 전개 방향에 맞게 Y 좌표 계산
+ val childY = if (openUp) nextY - mButtonSpacing - fab.measuredHeight else nextY + mButtonSpacing
+
+ fab.layout(childX, childY, childX + fab.measuredWidth, childY + fab.measuredHeight)
+ fab.translationX = 0f
+ fab.translationY = 0f
+
+ if (!mIsMenuOpening) fab.hide(false)
+
+ // 2. 라벨 배치
+ val label = fab.getTag(R.id.fab_label) as View?
+ if (label != null) {
+ val labelL: Int
+ val labelT: Int
+
+ if (mLabelsPosition == LABELS_POSITION_CENTER) {
+ labelL = childX + (fab.measuredWidth - label.measuredWidth) / 2
+ labelT = childY + (fab.measuredHeight - label.measuredHeight) / 2
+ } else {
+ val actualSide = if (isMenuOnLeft) LABELS_POSITION_RIGHT else LABELS_POSITION_LEFT
+ val offset = (if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.measuredWidth / 2) + mLabelsMargin
+
+ labelL = if (actualSide == LABELS_POSITION_LEFT) buttonsCenterX - offset - label.measuredWidth else buttonsCenterX + offset
+ labelT = childY - mLabelsVerticalOffset + (fab.measuredHeight - label.measuredHeight) / 2
+ }
+
+ label.layout(labelL, labelT, labelL + label.measuredWidth, labelT + label.measuredHeight)
+ label.translationX = 0f
+ label.translationY = 0f
+
+ if (!mIsMenuOpening) label.visibility = INVISIBLE
}
+
+ // 3. 다음 뷰를 위한 Y 좌표 누적 갱신
+ nextY = if (openUp) childY else childY + fab.measuredHeight
}
-
- val label = fab.getTag(R.id.fab_label) as View?
- if (label != null) {
- val labelLeft: Int
- val labelRight: Int
- val labelTop: Int
-
- // 💡 [수정] Center 옵션일 경우 X,Y축 모두 버튼의 중앙으로 계산
- if (mLabelsPosition == LABELS_POSITION_CENTER) {
- labelLeft = childX + (fab.getMeasuredWidth() - label.getMeasuredWidth()) / 2
- labelRight = labelLeft + label.getMeasuredWidth()
- labelTop = childY + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
- } else {
- val labelsOffset =
- (if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
- val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
- buttonsHorizontalCenter - labelsOffset
- else
- buttonsHorizontalCenter + labelsOffset
-
- val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
- labelXNearButton - label.getMeasuredWidth()
- else
- labelXNearButton + label.getMeasuredWidth()
-
- labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
- labelXAwayFromButton
- else
- labelXNearButton
-
- labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
- labelXNearButton
- else
- labelXAwayFromButton
-
- labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
- }
-
- label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight())
-
- if (!mIsMenuOpening) {
- label.setVisibility(INVISIBLE)
- }
- }
-
- nextY = if (openUp)
- childY - mButtonSpacing
- else
- childY + child.getMeasuredHeight() + mButtonSpacing
}
}
@@ -555,14 +548,14 @@ class FloatingActionMenu @JvmOverloads constructor(
override fun onFinishInflate() {
super.onFinishInflate()
bringChildToFront(mMenuButton)
- bringChildToFront(mImageToggle)
+ bringChildToFront(mToggleView)
mButtonsCount = getChildCount()
createLabels()
}
private fun createLabels() {
for (i in 0.. {
- dX = v.getX() - event.getRawX()
- dY = v.getY() - event.getRawY()
- startX = event.getRawX()
- startY = event.getRawY()
- lastAction = MotionEvent.ACTION_DOWN
+ dX = v.x - event.rawX
+ dY = v.y - event.rawY
+ startX = event.rawX
+ startY = event.rawY
}
MotionEvent.ACTION_MOVE -> {
- if (!isDragMenuDisabled and (checkViewLimits(
- v,
- (event.getRawX() + dX).toInt(),
- (event.getRawY() + dY).toInt()
- ))
- ) {
- v.setY(event.getRawY() + dY)
- v.setX(event.getRawX() + dX)
+ if (!isDragMenuDisabled) {
+ // 1. 드래그하려는 목표 좌표 계산
+ var newX = event.rawX + dX
+ var newY = event.rawY + dY
- moveElements(
- v,
- event.getRawX() + dX,
- event.getRawY() + dY,
- oldX,
- oldY
- )
+ // 2. 화면 전체 너비와 높이 가져오기 (현재 FloatingActionMenu가 match_parent이므로 자체 크기 사용)
+ val menuWidth = this@FloatingActionMenu.width
+ val menuHeight = this@FloatingActionMenu.height
+
+ // 3. 화면을 벗어나지 않도록 좌표 가두기 (패딩 포함)
+ val minX = paddingLeft.toFloat()
+ val maxX = (menuWidth - v.width - paddingRight).toFloat()
+
+ val minY = paddingTop.toFloat()
+ val maxY = (menuHeight - v.height - paddingBottom).toFloat()
+
+ // coerceIn 함수를 사용하여 최소~최대 값 사이로 강제 보정
+ newX = newX.coerceIn(minX, maxX)
+ newY = newY.coerceIn(minY, maxY)
+
+ // 4. 보정된 좌표로 뷰 이동
+ v.x = newX
+ v.y = newY
+
+ // 5. 하위 메뉴들도 바뀐 메인 버튼 위치에 맞춰 다시 그리기
+ moveElements(v, newX, newY, oldX, oldY)
}
- lastAction = MotionEvent.ACTION_MOVE
}
- MotionEvent.ACTION_UP -> if (abs((startX - event.getRawX()).toDouble()) < 10 && abs(
- (startY - event.getRawY()).toDouble()
- ) < 10
- ) {
- toggle(mIsAnimated)
+ MotionEvent.ACTION_UP -> {
+ // 드래그가 아니라 단순 클릭(오차 10픽셀 이내)이었다면 메뉴 토글
+ if (abs(startX - event.rawX) < 10 && abs(startY - event.rawY) < 10) {
+ toggle(mIsAnimated)
+ }
}
-
else -> return false
}
return true
@@ -650,113 +651,6 @@ class FloatingActionMenu @JvmOverloads constructor(
}
}
- private fun moveElements(v: View?, SetX: Float, SetY: Float, oldX: Float, oldY: Float) {
- val parent = (this.getParent() as ViewGroup).getParent() as View
-
- val l = mMenuButton!!.getX().toInt()
- val t = mMenuButton!!.getY().toInt()
- val r = l + mMenuButton!!.getMeasuredWidth()
- val b = t + mMenuButton!!.getMeasuredHeight()
-
- val mMenuH = mMenuButton!!.getMeasuredHeight()
-
- val halfHeight = parent.getHeight() / 2
-
- val buttonsHorizontalCenter = (r - l) / 2
- val openUp = halfHeight < (t)
-
- var nextY = if (openUp) t - getPaddingTop() else t + mMenuH
- var fab: Any?
-
- for (i in mButtonsCount - 1 downTo 0) {
- val child = getChildAt(i)
- if (child.getVisibility() == GONE) continue
-
- val childX: Int
- val childY: Int
-
- if (child === mImageToggle) {
- val imageLeft = l + buttonsHorizontalCenter - mImageToggle!!.getMeasuredWidth() / 2
- val imageTop =
- t + mMenuButton!!.getMeasuredHeight() / 2 - mImageToggle!!.getMeasuredHeight() / 2
- child.layout(
- imageLeft,
- imageTop,
- l + mImageToggle!!.getMeasuredWidth() * 2,
- t + mImageToggle!!.getMeasuredHeight() * 2
- )
- } else {
- childX = l + buttonsHorizontalCenter - child.getMeasuredWidth() / 2
- childY =
- if (openUp) nextY - child.getMeasuredHeight() - mButtonSpacing else nextY + mButtonSpacing
-
- fab = child as FloatingActionButton
- if (fab === mMenuButton) continue
-
- child.layout(
- childX, childY, childX + child.getMeasuredWidth(),
- childY + child.getMeasuredHeight()
- )
-
- if (!mIsMenuOpening) {
- if (child is FloatingActionButton) fab.hide(false)
- }
-
- var label: View? = null
- if (child !== mImageToggle) label = fab.getTag(R.id.fab_label) as View?
-
- if (label != null) {
- val labelLeft: Int
- val labelRight: Int
- val labelTop: Int
-
- // 💡 [수정] Center 옵션일 경우 드래그 중에도 중앙으로 정렬
- if (mLabelsPosition == LABELS_POSITION_CENTER) {
- labelLeft = childX + (fab.getMeasuredWidth() - label.getMeasuredWidth()) / 2
- labelRight = labelLeft + label.getMeasuredWidth()
- labelTop = childY + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
- } else {
- val labelsOffset =
- (if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
- val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
- l + buttonsHorizontalCenter - labelsOffset
- else
- l + buttonsHorizontalCenter + labelsOffset
-
- val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
- labelXNearButton - label.getMeasuredWidth()
- else
- labelXNearButton + label.getMeasuredWidth()
-
- labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
- labelXAwayFromButton
- else
- labelXNearButton
-
- labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
- labelXNearButton
- else
- labelXAwayFromButton
-
- labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
- }
-
- label.layout(
- labelLeft,
- labelTop,
- labelRight,
- labelTop + label.getMeasuredHeight()
- )
-
- if (!mIsMenuOpening) {
- label.setVisibility(INVISIBLE)
- }
- }
- nextY =
- if (openUp) childY - mButtonSpacing else childY + fab.getMeasuredHeight() + mButtonSpacing
- }
- }
- }
private fun addLabel(fab: FloatingActionButton) {
val text = fab.labelText
@@ -821,7 +715,8 @@ class FloatingActionMenu @JvmOverloads constructor(
label.setTypeface(mCustomTypefaceFromFont)
}
label.setText(text)
- label.setOnClickListener(fab.getOnClickListener())
+ label.setOnClickListener {fab.performClick()}
+
addView(label)
fab.setTag(R.id.fab_label, label)
@@ -859,9 +754,9 @@ class FloatingActionMenu @JvmOverloads constructor(
if (!this.isMenuButtonHidden) {
mMenuButton!!.hide(animate)
if (animate) {
- mImageToggle!!.startAnimation(mImageToggleHideAnimation)
+ mToggleView!!.startAnimation(mImageToggleHideAnimation)
}
- mImageToggle!!.setVisibility(INVISIBLE)
+ mToggleView!!.setVisibility(INVISIBLE)
mIsMenuButtonAnimationRunning = false
}
}
@@ -870,9 +765,9 @@ class FloatingActionMenu @JvmOverloads constructor(
if (this.isMenuButtonHidden) {
mMenuButton!!.show(animate)
if (animate) {
- mImageToggle!!.startAnimation(mImageToggleShowAnimation)
+ mToggleView!!.startAnimation(mImageToggleShowAnimation)
}
- mImageToggle!!.setVisibility(VISIBLE)
+ mToggleView!!.setVisibility(VISIBLE)
}
}
@@ -1055,8 +950,8 @@ class FloatingActionMenu @JvmOverloads constructor(
mToggleListener = listener
}
- val menuIconView: ImageView
- get() = mImageToggle!!
+// val menuIconView: ImageView
+// get() = mToggleView!!
fun setMenuButtonShowAnimation(showAnimation: Animation) {
mMenuButtonShowAnimation = showAnimation
@@ -1239,7 +1134,7 @@ class FloatingActionMenu @JvmOverloads constructor(
val viewsToRemove: MutableList = ArrayList()
for (i in 0.. 5) {
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ (binding.tvAddress.tag as? String)?.let { putExtra(Intent.EXTRA_TITLE, it) }
+ putExtra(Intent.EXTRA_TEXT, addr)
+ type = "text/plain"
+ }
+ context.startActivity(Intent.createChooser(intent, "공유"))
+ }
+ }
+
+ // GeckoWeb 내부에서 주소 등 업데이트 시 연동되도록 등록
+ geckoWeb.decoViews.addAll(listOf(
+ binding.tvAddress, binding.btnDlVideo, binding.btnReload, binding.btnBack, binding.tvTitle
+ ))
+ }
+
+ fun setupForRssHome() {
+ binding.btnBookmark.isVisible = true
+ binding.btnSearch.isVisible = true
+ binding.btnHide.isVisible = true
+ binding.btnVote.isVisible = true
+
+ binding.btnList.isVisible = false
+ binding.btnHistory.isVisible = false
+ binding.btnSetting.isVisible = false
+ }
+
+ fun setupForToki() {
+ binding.btnBookmark.isVisible = false
+ binding.btnSearch.isVisible = false
+ binding.btnHide.isVisible = false
+ binding.btnVote.isVisible = false
+
+ binding.btnList.isVisible = true
+ binding.btnHistory.isVisible = true
+ binding.btnSetting.isVisible = true
+ }
+
+ // 💡 소설 뷰어 모드로 전환 시, 웹뷰와 하단바를 숨기고 상단 타이틀바만 남기기 위한 헬퍼
+ fun showBrowserAreaOnly(isVisible: Boolean) {
+ binding.internalGeckoWeb.isVisible = isVisible
+ binding.layoutBottomBar.isVisible = isVisible
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
index 8614de83..59d7506f 100644
--- a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
+++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt
@@ -67,13 +67,15 @@ object WorkersDb {
//RecentCall::class, RecentSms::class,
val clazz : Set> = setOf(
- RssData::class, NotificationItem::class, AppInfo::class,SimpleContact::class, CurrentPlayItem::class,
- TelegramBotUpdate::class, TelegramData::class, TelegramMessage::class, TelegramChat::class, BotCommandEentitie::class, TelegramFrom::class,
- WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class,
+ RssData::class, AppInfo::class, SimpleContact::class,
+ NotificationItem::class,
+ CurrentPlayItem::class,
+// TelegramBotUpdate::class, TelegramData::class, TelegramMessage::class, TelegramChat::class, BotCommandEentitie::class, TelegramFrom::class,
+// WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class,
LocationLog::class,
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
- WidgetData::class,AppUsageLog::class,
-
+ AppUsageLog::class,
+ WidgetData::class,
)
//,UserActionModel::class
diff --git a/app/src/main/res/font/material_symbols.ttf b/app/src/main/res/font/material_symbols.ttf
new file mode 100644
index 00000000..a4b7a0a1
Binary files /dev/null and b/app/src/main/res/font/material_symbols.ttf differ
diff --git a/app/src/main/res/layout/app_menu.xml b/app/src/main/res/layout/app_menu.xml
index fee72d2d..3f85a4a0 100644
--- a/app/src/main/res/layout/app_menu.xml
+++ b/app/src/main/res/layout/app_menu.xml
@@ -70,7 +70,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/eight"
- app:layout_constraintTop_toBottomOf="@id/totalTouch"
+ app:layout_constraintTop_toBottomOf="@id/recommend"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent" />
-
-
diff --git a/app/src/main/res/layout/fragment_system_status.xml b/app/src/main/res/layout/fragment_system_status.xml
index 36904ec6..2806202e 100644
--- a/app/src/main/res/layout/fragment_system_status.xml
+++ b/app/src/main/res/layout/fragment_system_status.xml
@@ -3,13 +3,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
- android:background="#E6121212">
+ android:background="?android:attr/windowBackground">
+ android:padding="12dp">
+ android:weightSum="5">
+
+
-
+
-
+
-
+
+
-
-
-
+
+
+ android:scrollbars="none"/>
-
-
+ android:layout_height="match_parent"
+ android:visibility="gone" />
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_lunatic_browser.xml b/app/src/main/res/layout/layout_lunatic_browser.xml
new file mode 100644
index 00000000..2da79f71
--- /dev/null
+++ b/app/src/main/res/layout/layout_lunatic_browser.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_textviewer.xml b/app/src/main/res/layout/layout_textviewer.xml
index b26241b8..7dff3cce 100644
--- a/app/src/main/res/layout/layout_textviewer.xml
+++ b/app/src/main/res/layout/layout_textviewer.xml
@@ -36,11 +36,13 @@
-
-
-
-
-
-
+ app:layout_constraintEnd_toEndOf="parent" />
-
-
-
-
+ app:fab_label="📰"
+ style="@style/CommonFabStyle"
+ android:id="@+id/feeds"/>
+ app:fab_label="📚"
+ style="@style/CommonFabStyle"
+ android:id="@+id/books"/>
+ app:fab_label="🎨"
+ style="@style/CommonFabStyle"
+ android:id="@+id/webtoons"/>
+ app:fab_label="🗯️"
+ style="@style/CommonFabStyle"
+ android:id="@+id/comics"/>
+ app:fab_label="📺"
+ style="@style/CommonFabStyle"
+ android:id="@+id/youtube"/>
+ app:fab_label="🤖"
+ style="@style/CommonFabStyle"
+ android:id="@+id/perplexity"/>
+ app:fab_label="😂"
+ style="@style/CommonFabStyle"
+ android:id="@+id/zzalbang"/>
+ app:fab_label="🐦"
+ style="@style/CommonFabStyle"
+ android:id="@+id/btn_x"/>
+ app:fab_label="🔞"
+ style="@style/CommonFabStyle"
+ android:id="@+id/btn_i"/>
+ />
+ />
+ style="@style/CommonFabStyle"
+ app:fab_label="❌"
+ android:id="@+id/close"/>
diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
index c784e171..ff83661d 100644
--- a/app/src/main/res/layout/settings_activity.xml
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -29,107 +29,127 @@
android:background="@drawable/rounded_bg_top"
android:backgroundTint="@color/black"
android:paddingHorizontal="@dimen/thirtySix"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/version"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBar">
-
+ android:paddingTop="@dimen/twelve"
+ android:paddingBottom="@dimen/thirtySix">
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:enabled="false"
+ android:text="자동 백업 허용 (주 1회)"
+ android:textColor="@color/white"
+ android:textStyle="bold"
+ app:switchPadding="16dp" />
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 58f69065..b187535a 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -5,6 +5,11 @@
- @drawable/ic_launcher
- 1000
- @style/Theme.LunarLauncher
+ - true
+ - false
+
+ - true
+ - @android:color/transparent
+
+
+
+
+
\ No newline at end of file
diff --git a/gdrive/.gitignore b/gdrive/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/gdrive/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/gdrive/build.gradle.kts b/gdrive/build.gradle.kts
new file mode 100644
index 00000000..2a7631ef
--- /dev/null
+++ b/gdrive/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ id ("com.android.library")
+ id ("kotlin-android")
+}
+
+android {
+ namespace = "kr.bums.lunatic.utils.gdrive"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 24
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.activity:activity-ktx:1.8.2") // ActivityResultLauncher용
+
+ // 구글 로그인
+ implementation("com.google.android.gms:play-services-auth:20.7.0")
+
+ // 구글 드라이브 API
+ implementation("com.google.api-client:google-api-client-android:1.33.0") {
+ exclude(group = "org.apache.httpcomponents")
+ }
+ implementation("com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0") {
+ exclude(group = "org.apache.httpcomponents")
+ }
+// implementation("androidx.core:core-ktx:1.17.0")
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("com.google.android.material:material:1.13.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+}
\ No newline at end of file
diff --git a/gdrive/proguard-rules.pro b/gdrive/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/gdrive/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/gdrive/src/androidTest/java/bums/lunatic/utils/gdrive/ExampleInstrumentedTest.kt b/gdrive/src/androidTest/java/bums/lunatic/utils/gdrive/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..42d31a2e
--- /dev/null
+++ b/gdrive/src/androidTest/java/bums/lunatic/utils/gdrive/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package bums.lunatic.utils.gdrive
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("bums.lunatic.utils.gdrive", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/gdrive/src/main/AndroidManifest.xml b/gdrive/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..1b4f05b9
--- /dev/null
+++ b/gdrive/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveBackupTask.kt b/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveBackupTask.kt
new file mode 100644
index 00000000..9de1d88c
--- /dev/null
+++ b/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveBackupTask.kt
@@ -0,0 +1,76 @@
+package kr.gdrive.bums.lunatic.utils
+
+import android.content.Context
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
+import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
+import com.google.api.client.http.ByteArrayContent
+import com.google.api.client.http.javanet.NetHttpTransport
+import com.google.api.client.json.gson.GsonFactory
+import com.google.api.services.drive.Drive
+import com.google.api.services.drive.DriveScopes
+import com.google.api.services.drive.model.File
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class GDriveBackupTask(
+ private val context: Context,
+ private val account: GoogleSignInAccount
+) {
+ // 💡 백업 실행 (IO 스레드에서 돌아야 함)
+ suspend fun executeBackup(payload: BackupPayload): Result = withContext(Dispatchers.IO) {
+ try {
+ val credential = GoogleAccountCredential.usingOAuth2(context, listOf(DriveScopes.DRIVE_APPDATA))
+ credential.selectedAccount = account.account
+
+ val driveService = Drive.Builder(
+ NetHttpTransport(), GsonFactory.getDefaultInstance(), credential
+ ).setApplicationName("LunaticLauncherBackup").build()
+
+ // 1. 매니페스트 업데이트
+ uploadFile(driveService, "appDataFolder", "manifest.json", payload.manifestJson)
+
+ // 2. 폴더 가져오기 or 생성
+ val folderId = getOrCreateFolder(driveService, payload.folderName)
+
+ // 3. 파일들 업로드
+ payload.files.forEach { (fileName, jsonContent) ->
+ uploadFile(driveService, folderId, fileName, jsonContent)
+ }
+
+ Result.success("백업이 완료되었습니다. (${payload.folderName})")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Result.failure(e)
+ }
+ }
+
+ private fun getOrCreateFolder(driveService: Drive, folderName: String): String {
+ val query = "mimeType='application/vnd.google-apps.folder' and name='$folderName' and 'appDataFolder' in parents and trashed=false"
+ val fileList = driveService.files().list().setSpaces("appDataFolder").setQ(query).execute()
+
+ if (fileList.files.isNotEmpty()) return fileList.files[0].id
+
+ val folderMetadata = File().apply {
+ name = folderName
+ mimeType = "application/vnd.google-apps.folder"
+ parents = listOf("appDataFolder")
+ }
+ return driveService.files().create(folderMetadata).setFields("id").execute().id
+ }
+
+ private fun uploadFile(driveService: Drive, parentId: String, fileName: String, contentStr: String) {
+ val content = ByteArrayContent.fromString("application/json", contentStr)
+ val query = "name='$fileName' and '$parentId' in parents and trashed=false"
+ val fileList = driveService.files().list().setSpaces("appDataFolder").setQ(query).execute()
+
+ if (fileList.files.isNotEmpty()) {
+ driveService.files().update(fileList.files[0].id, null, content).execute()
+ } else {
+ val fileMetadata = File().apply {
+ name = fileName
+ parents = listOf(parentId)
+ }
+ driveService.files().create(fileMetadata, content).execute()
+ }
+ }
+}
\ No newline at end of file
diff --git a/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveLoginManager.kt b/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveLoginManager.kt
new file mode 100644
index 00000000..36ad920a
--- /dev/null
+++ b/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveLoginManager.kt
@@ -0,0 +1,79 @@
+package kr.gdrive.bums.lunatic.utils
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.ComponentActivity
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.common.api.Scope
+import com.google.api.services.drive.DriveScopes
+
+class GDriveLoginManager(
+ private val activity: ComponentActivity,
+ private val onLoginResult: (Boolean, GoogleSignInAccount?, String?) -> Unit
+) {
+ // 필수 권한: 숨겨진 App Data 폴더 접근 권한
+ private val driveScope = Scope(DriveScopes.DRIVE_APPDATA)
+
+ private val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestEmail()
+ .requestScopes(driveScope)
+ .build()
+
+ private val signInClient = GoogleSignIn.getClient(activity, signInOptions)
+
+ // 액티비티 생성 시점에 등록되어야 하는 런처
+ private val signInLauncher: ActivityResultLauncher =
+ activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
+ try {
+ val account = task.getResult(Exception::class.java)
+ if (account != null && GoogleSignIn.hasPermissions(account, driveScope)) {
+ onLoginResult(true, account, null)
+ } else {
+ onLoginResult(false, null, "드라이브 접근 권한이 부족합니다.")
+ }
+ } catch (e: Exception) {
+ onLoginResult(false, null, "로그인 실패: ${e.message}")
+ }
+ } else {
+ onLoginResult(false, null, "로그인이 취소되었습니다.")
+ }
+ }
+
+ /**
+ * 이미 로그인된 유효한 계정이 있는지 확인 (백그라운드에서 유용함)
+ */
+ fun getSignedInAccount(context: Context = activity): GoogleSignInAccount? {
+ val account = GoogleSignIn.getLastSignedInAccount(context)
+ return if (account != null && GoogleSignIn.hasPermissions(account, driveScope)) {
+ account
+ } else {
+ null
+ }
+ }
+
+ /**
+ * 로그인 화면 띄우기
+ */
+ fun signIn() {
+ val account = getSignedInAccount()
+ if (account != null) {
+ onLoginResult(true, account, null)
+ } else {
+ signInLauncher.launch(signInClient.signInIntent)
+ }
+ }
+
+ /**
+ * 로그아웃 (연결 해제)
+ */
+ fun signOut(onComplete: () -> Unit) {
+ signInClient.signOut().addOnCompleteListener { onComplete() }
+ }
+}
\ No newline at end of file
diff --git a/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveModels.kt b/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveModels.kt
new file mode 100644
index 00000000..df078763
--- /dev/null
+++ b/gdrive/src/main/java/kr/gdrive/bums/lunatic/utils/GDriveModels.kt
@@ -0,0 +1,16 @@
+package kr.gdrive.bums.lunatic.utils
+
+sealed class GDriveState {
+ object Idle : GDriveState()
+ object StartingLogin : GDriveState()
+ object Uploading : GDriveState()
+ class Success(val message: String) : GDriveState()
+ class Error(val message: String, val exception: Exception? = null) : GDriveState()
+}
+
+// 메인 앱에서 백업할 데이터를 담아 보낼 데이터 클래스
+data class BackupPayload(
+ val manifestJson: String, // 최상단에 저장될 버전 정보
+ val folderName: String, // 예: "2026-03-05" (날짜 폴더명)
+ val files: Map // 파일명(키)과 JSON 텍스트(값)의 쌍
+)
\ No newline at end of file
diff --git a/gdrive/src/test/java/bums/lunatic/utils/gdrive/ExampleUnitTest.kt b/gdrive/src/test/java/bums/lunatic/utils/gdrive/ExampleUnitTest.kt
new file mode 100644
index 00000000..f309dab8
--- /dev/null
+++ b/gdrive/src/test/java/bums/lunatic/utils/gdrive/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package bums.lunatic.utils.gdrive
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index cbc74b3a..ca960754 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,8 +3,8 @@ pluginManagement {
gradlePluginPortal()
google()
mavenCentral()
-// jcenter()
maven (url = "https://maven.mozilla.org/maven2/")
+
}
}
@@ -17,8 +17,10 @@ dependencyResolutionManagement {
mavenCentral()
maven (url = "https://maven.mozilla.org/maven2/")
maven(url = "https://jitpack.io")
+
}
}
rootProject.name = "LunarLauncher"
include ("app","library","utils")
+include(":gdrive")