This commit is contained in:
lunaticbum 2026-03-05 18:11:08 +09:00
parent 83546e5e10
commit 70869d7e1e
33 changed files with 1359 additions and 935 deletions

View File

@ -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"

View File

@ -58,11 +58,9 @@
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name=".LunaticLauncher"
android:icon="@drawable/ic_launcher"

View File

@ -33,7 +33,10 @@ abstract class CommonActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
supportActionBar?.hide()
actionBar?.hide()
} catch (e: Exception) {}
WindowCompat.setDecorFitsSystemWindows(window, false)
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
var forWhite = false

View File

@ -184,7 +184,7 @@ open class GeckoWeb @JvmOverloads constructor(
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
decoViews.filter { it.id > -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

View File

@ -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<Int, Fragment>()
// private val fragmentMap = mutableMapOf<Int, Fragment>()
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)
}

View File

@ -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<RssData>().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<RssData>().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<TextView>(R.id.current_address))
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.back))
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(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) {

View File

@ -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<String>, 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) {

View File

@ -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<TextView>(R.id.current_address))
it.decoViews.add(activity.findViewById<ImageButton>(R.id.back))
it.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
it.decoViews.add(activity.findViewById<ImageButton>(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())
}
}

View File

@ -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
}

View File

@ -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..<mButtonsCount) {
val child = getChildAt(i)
if (child.getVisibility() == GONE || child === mImageToggle) continue
if (child.getVisibility() == GONE || child === mToggleView) continue
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
mMaxButtonWidth =
@ -387,7 +394,7 @@ class FloatingActionMenu @JvmOverloads constructor(
var usedWidth = 0
val child = getChildAt(i)
if (child.getVisibility() == GONE || child === mImageToggle) continue
if (child.getVisibility() == GONE || child === mToggleView) continue
usedWidth += child.getMeasuredWidth()
height += child.getMeasuredHeight()
@ -439,112 +446,98 @@ class FloatingActionMenu @JvmOverloads constructor(
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
doLayout(changed, l, t, r, b)
val buttonsHorizontalCenter = when (mLabelsPosition) {
LABELS_POSITION_LEFT, LABELS_POSITION_CENTER -> 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..<mButtonsCount) {
if (getChildAt(i) === mImageToggle) continue
if (getChildAt(i) === mToggleView) continue
val fab = getChildAt(i) as FloatingActionButton
@ -582,47 +575,55 @@ class FloatingActionMenu @JvmOverloads constructor(
var dY: Float = 0f
var startX: Float = 0f
var startY: Float = 0f
var lastAction: Int = 0
override fun onTouch(v: View, event: MotionEvent): Boolean {
val oldX = v.getX()
val oldY = v.getY()
when (event.getActionMasked()) {
val oldX = v.x
val oldY = v.y
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
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<FloatingActionButton> = ArrayList<FloatingActionButton>()
for (i in 0..<getChildCount()) {
val v = getChildAt(i)
if (v !== mMenuButton && v !== mImageToggle && v is FloatingActionButton) {
if (v !== mMenuButton && v !== mToggleView && v is FloatingActionButton) {
viewsToRemove.add(v)
}
}

View File

@ -0,0 +1,72 @@
package bums.lunatic.launcher.view
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import bums.lunatic.launcher.databinding.LayoutLunaticBrowserBinding
import bums.lunatic.launcher.home.GeckoWeb
class LunaticBrowserLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
val binding: LayoutLunaticBrowserBinding =
LayoutLunaticBrowserBinding.inflate(LayoutInflater.from(context), this)
val geckoWeb: GeckoWeb get() = binding.internalGeckoWeb
init {
geckoWeb.progress = binding.internalProgressBar
// 공통 하단바 로직
binding.btnBack.setOnClickListener { geckoWeb.session?.goBack() }
binding.btnReload.setOnClickListener { geckoWeb.session?.reload() }
binding.btnShare.setOnClickListener {
val addr = binding.tvAddress.text.toString()
if (addr.length > 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
}
}

View File

@ -67,13 +67,15 @@ object WorkersDb {
//RecentCall::class, RecentSms::class,
val clazz : Set<KClass<out BaseRealmObject>> = 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

Binary file not shown.

View File

@ -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"/>
<TextView

View File

@ -8,112 +8,21 @@
android:layout_height="match_parent"
>
<bums.lunatic.launcher.home.GeckoWeb
android:id="@+id/menu_web"
android:layout_margin="5dp"
<bums.lunatic.launcher.view.LunaticBrowserLayout
android:id="@+id/lunaticBrowser"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_title" />
<androidx.constraintlayout.utils.widget.ImageFilterButton
android:id="@+id/btn_home"
android:layout_width="wrap_content"
android:layout_height="@dimen/main_top_height"
android:background="@android:color/transparent"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/home"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/textview_title"
android:layout_width="0dp"
android:layout_height="@dimen/main_top_height"
android:text="@string/app_name"
android:gravity="center"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintRight_toLeftOf="@id/btn_list"
app:layout_constraintLeft_toRightOf="@id/btn_setting"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterButton
android:background="@android:color/transparent"
android:id="@+id/btn_list"
android:layout_width="wrap_content"
android:layout_height="@dimen/main_top_height"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:src="@drawable/bookmark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/textview_title"
app:layout_constraintRight_toLeftOf="@+id/btn_history"
app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<androidx.constraintlayout.utils.widget.ImageFilterButton
android:id="@+id/btn_history"
android:layout_width="wrap_content"
android:layout_height="@dimen/main_top_height"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/saved"
android:background="@android:color/transparent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<androidx.constraintlayout.utils.widget.ImageFilterButton
android:id="@+id/btn_setting"
android:layout_width="wrap_content"
android:src="@drawable/settings"
android:layout_height="@dimen/main_top_height"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
app:layout_constraintLeft_toRightOf="@+id/btn_home"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
/>
android:layout_height="match_parent" />
<bums.lunatic.launcher.home.tokiz.view.PagedTextLayout
android:id="@+id/paged_layer"
android:layout_margin="1dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:visibility="gone"
android:elevation="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_title" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginTop="48dp"
android:indeterminate="false"
android:max="100"
android:progressBackgroundTint="#FBE7C6"
android:progressDrawable="@drawable/circle_progressbar"
android:progressTint="#edbf41"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp">
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
@ -25,8 +25,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#1AFFFFFF"
android:padding="12dp"
android:background="#1fff"
android:padding="6dp"
android:layout_marginBottom="24dp">
<TextView
@ -42,7 +42,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp"
android:weightSum="3">
android:weightSum="5">
<TextView
android:id="@+id/btnQuickWifi"
android:layout_width="0dp"
@ -76,6 +76,28 @@
android:text="🔔 소리"
android:textColor="#FFFFFF"
android:textStyle="bold" />
<TextView
android:id="@+id/btnQuickFlash"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:padding="8dp"
android:text="🔦 조명"
android:textColor="#FFFFFF"
android:textStyle="bold" />
<TextView
android:id="@+id/btnQuickMirror"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:padding="8dp"
android:text="🪞 거울"
android:textColor="#FFFFFF"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout

View File

@ -7,7 +7,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@android:color/transparent"
android:orientation="vertical"
android:id="@+id/mainFragmentsContainer"
>
<FrameLayout
android:id="@+id/widget_container"

View File

@ -6,109 +6,75 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
app:layout_constraintTop_toTopOf="@id/home"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="@id/home"
android:id="@+id/search"
android:alpha="0.5"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:visibility="visible"
android:background="@null"
android:src="@drawable/search"
android:layout_width="30dp"
android:tint="@color/finestSilver"
android:foregroundTint="@color/finestSilver"
tools:ignore="ContentDescription,UseAppTint"
android:layout_height="30dp" />
<LinearLayout
android:id="@+id/layout_tabs"
android:layout_width="match_parent"
android:layout_height="45dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:id="@+id/vote"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:visibility="visible"
android:background="@null"
android:src="@drawable/saved"
android:layout_width="@dimen/main_top_height"
tools:ignore="ContentDescription"
android:layout_height="@dimen/main_top_height" />
<TextView
style="@style/MaterialIconButtonStyle"
android:id="@+id/tab_all"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="dynamic_feed"
android:gravity="center"
android:textColor="@color/white"
android:background="?attr/selectableItemBackground"/>
<ImageButton
android:id="@+id/hide"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/vote"
android:src="@drawable/ic_delete"
android:tintMode="multiply"
android:layout_marginLeft="12dp"
android:scaleType="fitCenter"
android:background="@null"
android:layout_width="@dimen/main_top_height"
android:visibility="visible"
android:adjustViewBounds="true"
tools:ignore="ContentDescription"
android:layout_height="@dimen/main_top_height"
/>
<TextView
style="@style/MaterialIconButtonStyle"
android:id="@+id/tab_voted"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="grade"
android:gravity="center"
android:textColor="@color/white"
android:background="?attr/selectableItemBackground"/>
<TextView
style="@style/MaterialIconButtonStyle"
android:id="@+id/tab_private"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="visibility_off"
android:gravity="center"
android:textColor="@color/white"
android:background="?attr/selectableItemBackground"/>
<ImageButton
android:id="@+id/home"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:src="@drawable/home"
android:tintMode="multiply"
android:scaleType="fitCenter"
android:background="@null"
android:layout_width="@dimen/main_top_height"
android:adjustViewBounds="true"
android:layout_height="@dimen/main_top_height"
app:tint="@color/white"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@+id/bookmark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/home"
android:src="@drawable/bookmark"
android:scaleType="fitCenter"
android:background="@null"
android:layout_width="@dimen/main_top_height"
android:adjustViewBounds="true"
tools:ignore="ContentDescription"
android:layout_height="@dimen/main_top_height"/>
<TextView
style="@style/MaterialIconButtonStyle"
android:id="@+id/tab_serarch"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="search"
android:gravity="center"
android:textColor="@color/finestSilver"
android:background="?attr/selectableItemBackground"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_margin="@dimen/default_layout_margin"
android:id="@+id/infoList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/layout_tabs"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="@dimen/default_layout_margin"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:overScrollMode="never"
android:padding="@dimen/default_padding"
android:scrollbars="none"
android:background="#AB000000"
android:visibility="visible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
/>
android:scrollbars="none"/>
<bums.lunatic.launcher.home.GeckoWeb
android:id="@+id/geckoWeb"
android:visibility="gone"
<bums.lunatic.launcher.view.LunaticBrowserLayout
android:id="@+id/lunaticBrowser"
android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
android:layout_height="0dp"
/>
android:layout_height="match_parent"
android:visibility="gone" />
<include layout="@layout/layout_rss_summary"
android:id="@+id/layout_rss_summary"
@ -117,21 +83,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/bookmark"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="0dp"/>
<ProgressBar
app:layout_constraintTop_toBottomOf="@id/home"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="4dp"
android:max="100"
android:progress="0"
android:visibility="visible"
android:indeterminate="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/layout_top_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="#CC000000"
app:layout_constraintTop_toTopOf="parent">
<TextView android:id="@+id/btn_home" android:text="home" style="@style/MaterialIconButtonStyle" />
<TextView android:id="@+id/btn_bookmark" android:text="bookmarks" style="@style/MaterialIconButtonStyle" />
<TextView android:id="@+id/btn_setting" android:text="settings" style="@style/MaterialIconButtonStyle" android:visibility="gone" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/white"
android:textSize="14sp"
android:ellipsize="end"
android:singleLine="true"
tools:text="Page Title" />
<TextView android:id="@+id/btn_list"
android:text="list"
style="@style/MaterialIconButtonStyle" android:visibility="gone" />
<TextView android:id="@+id/btn_history"
android:text="history"
style="@style/MaterialIconButtonStyle" android:visibility="gone" />
<TextView android:id="@+id/btn_search"
android:text="search"
style="@style/MaterialIconButtonStyle" />
<TextView android:id="@+id/btn_hide"
android:text="block"
style="@style/MaterialIconButtonStyle" />
<TextView android:id="@+id/btn_vote"
android:text="favorite"
style="@style/MaterialIconButtonStyle" />
</LinearLayout>
<bums.lunatic.launcher.home.GeckoWeb
android:id="@+id/internal_gecko_web"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/layout_top_bar"
app:layout_constraintBottom_toTopOf="@id/layout_bottom_bar" />
<ProgressBar
android:id="@+id/internal_progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
app:layout_constraintTop_toBottomOf="@id/layout_top_bar" />
<LinearLayout
android:id="@+id/layout_bottom_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="#CC000000"
app:layout_constraintBottom_toBottomOf="parent">
<TextView android:id="@+id/btn_back" android:text="arrow_back" style="@style/MaterialIconButtonStyle" />
<TextView android:id="@+id/btn_reload" android:text="refresh" style="@style/MaterialIconButtonStyle" />
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="#BBFFFFFF"
android:textSize="11sp"
android:ellipsize="middle"
android:singleLine="true" />
<TextView android:id="@+id/btn_dl_video" android:text="download" style="@style/MaterialIconButtonStyle" android:visibility="gone" />
<TextView android:id="@+id/btn_share" android:text="share" style="@style/MaterialIconButtonStyle" />
</LinearLayout>
</merge>

View File

@ -36,11 +36,13 @@
</LinearLayout>
<TextView
android:id="@+id/current_chapter"
android:layout_marginLeft="30dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginRight="30dp"
android:id="@+id/current_page"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -3,198 +3,95 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:padding="0dp"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@android:color/transparent"
android:orientation="vertical"
android:id="@+id/mainFragmentsContainer"
>
<LinearLayout
android:id="@+id/fragment_layer"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:visibility="visible"
android:layout_marginBottom="15dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" >
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:visibility="visible"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/controll_panel"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="40dp">
<ImageButton
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/fragment_container"
app:layout_constraintLeft_toLeftOf="parent"
android:id="@+id/back"
android:src="@drawable/back_vector"
tools:ignore="ContentDescription"
style="@style/CommonBottom" />
<ImageButton
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/fragment_container"
app:layout_constraintLeft_toRightOf="@id/back"
android:id="@+id/reload"
android:src="@drawable/ic_refresh"
tools:ignore="ContentDescription"
style="@style/CommonBottom"/>
<TextView
android:text="asdasdsadasd"
android:layout_weight="1"
android:id="@+id/current_address"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintRight_toLeftOf="@id/dl_video"
app:layout_constraintLeft_toRightOf="@id/reload"
android:textColor="@color/white"
android:gravity="center"
android:textSize="@dimen/_12sp"
android:ellipsize="middle"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="0dp"
android:layout_height="@dimen/main_top_height"/>
app:layout_constraintEnd_toEndOf="parent" />
<ImageButton
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintRight_toLeftOf="@id/share"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/dl_video"
android:src="@drawable/dl_vid"
tools:ignore="ContentDescription"
style="@style/CommonBottom"/>
<ImageButton
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginRight="60dp"
android:id="@+id/share"
android:foregroundTint="@color/white"
android:src="@drawable/ic_share"
tools:ignore="ContentDescription"
style="@style/CommonBottom"/>
</LinearLayout>
</LinearLayout>
<bums.lunatic.launcher.view.FloatingActionMenu
android:id="@+id/floating_action_menu"
android:layout_margin="5dp"
android:visibility="visible"
app:menu_colorNormal="#80FF0000"
app:menu_colorNormal="#2550"
app:menu_fab_size="mini"
app:menu_icon="@drawable/ic_add"
app:menu_fab_label="✨"
app:menu_icon="@null"
app:menu_labels_textSize="28sp"
app:menu_labels_position="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu_shadowColor="@color/finestSilver"
app:menu_showShadow="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="feeds"
android:id="@+id/feeds"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="📰"
style="@style/CommonFabStyle"
android:id="@+id/feeds"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="booktoki"
android:id="@+id/books"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="📚"
style="@style/CommonFabStyle"
android:id="@+id/books"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="newtoki"
android:id="@+id/webtoons"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="🎨"
style="@style/CommonFabStyle"
android:id="@+id/webtoons"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="manatoki"
android:id="@+id/comics"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="🗯️"
style="@style/CommonFabStyle"
android:id="@+id/comics"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="youtube"
android:id="@+id/youtube"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="📺"
style="@style/CommonFabStyle"
android:id="@+id/youtube"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="perplexity"
android:id="@+id/perplexity"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="🤖"
style="@style/CommonFabStyle"
android:id="@+id/perplexity"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="짤방"
android:id="@+id/zzalbang"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="😂"
style="@style/CommonFabStyle"
android:id="@+id/zzalbang"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="X"
android:id="@+id/btn_x"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="🐦"
style="@style/CommonFabStyle"
android:id="@+id/btn_x"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="I"
android:id="@+id/btn_i"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
app:fab_label="🔞"
style="@style/CommonFabStyle"
android:id="@+id/btn_i"/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="torrent"
app:fab_label="🧲"
style="@style/CommonFabStyle"
android:id="@+id/btn_torrent"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="system"
app:fab_label="📊"
style="@style/CommonFabStyle"
android:id="@+id/btn_info"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
/>
<bums.lunatic.launcher.view.FloatingActionButton
app:fab_label="close"
android:id="@+id/close"
app:fab_showShadow="true"
app:fab_size="mini"
android:onClick="floatClick"
android:layout_width="wrap_content"
android:layout_height="20dp"/>
style="@style/CommonFabStyle"
app:fab_label="❌"
android:id="@+id/close"/>
</bums.lunatic.launcher.view.FloatingActionMenu>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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">
<com.google.android.material.button.MaterialButtonToggleGroup
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:singleSelection="true"
android:layout_gravity="center">
android:paddingTop="@dimen/twelve"
android:paddingBottom="@dimen/thirtySix">
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/timeDate"-->
<!-- style="@style/Widget.Material3.Button.ElevatedButton"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Display Info"-->
<!-- android:textAllCaps="true"-->
<!-- android:textStyle="bold" />-->
<com.google.android.material.button.MaterialButtonToggleGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:singleSelection="true">
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/weather"-->
<!-- style="@style/Widget.Material3.Button.ElevatedButton"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="@string/weather"-->
<!-- android:textAllCaps="true"-->
<!-- android:textStyle="bold" />-->
<com.google.android.material.button.MaterialButton
android:id="@+id/todo"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home"
android:textAllCaps="true"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/apps"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_drawer"
android:textAllCaps="true"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/misc"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/misc"
android:textAllCaps="true"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/about"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/about"
android:textAllCaps="true"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/support"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/support"
android:textAllCaps="true"
android:textStyle="bold" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
android:background="#33FFFFFF" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="데이터 백업 및 복원"
android:textColor="@color/white"
android:textStyle="bold"
style="@style/TextAppearance.Material3.TitleMedium" />
<com.google.android.material.button.MaterialButton
android:id="@+id/todo"
android:id="@+id/btnGoogleLogin"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home"
android:textAllCaps="true"
android:text="구글 드라이브 연결"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/apps"
android:id="@+id/btnManualBackup"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_drawer"
android:textAllCaps="true"
android:enabled="false"
android:text="지금 데이터 백업하기"
android:textStyle="bold" />
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/appearances"-->
<!-- style="@style/Widget.Material3.Button.ElevatedButton"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="@string/appearances"-->
<!-- android:textAllCaps="true"-->
<!-- android:textStyle="bold" />-->
<com.google.android.material.button.MaterialButton
android:id="@+id/misc"
style="@style/Widget.Material3.Button.ElevatedButton"
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchAutoBackup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/misc"
android:textAllCaps="true"
android:textStyle="bold" />
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:enabled="false"
android:text="자동 백업 허용 (주 1회)"
android:textColor="@color/white"
android:textStyle="bold"
app:switchPadding="16dp" />
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/advance"-->
<!-- style="@style/Widget.Material3.Button.ElevatedButton"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="@string/advance"-->
<!-- android:textAllCaps="true"-->
<!-- android:textStyle="bold" />-->
<com.google.android.material.button.MaterialButton
android:id="@+id/about"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/about"
android:textAllCaps="true"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/support"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/support"
android:textAllCaps="true"
android:textStyle="bold" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
</ScrollView>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/twelve"
android:layout_marginBottom="@dimen/twelve"
android:textColor="#88FFFFFF"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,6 +5,11 @@
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher</item>
<item name="windowSplashScreenAnimationDuration">1000</item>
<item name="postSplashScreenTheme">@style/Theme.LunarLauncher</item>
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowShowWallpaper">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="Slider" parent="Widget.Material3.Slider">
@ -139,4 +144,37 @@
<item name="android:layout_height">@dimen/main_top_height</item>
</style>
<style name="EmojiButtonStyle">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:gravity">center</item>
<item name="android:includeFontPadding">false</item>
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
</style>
<style name="MaterialIconButtonStyle" parent="Widget.AppCompat.Button.Borderless">
<item name="android:layout_width">40dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:gravity">center</item>
<item name="android:textColor">@color/white</item>
<item name="android:fontFamily">@font/material_symbols</item>
<item name="android:textSize">40sp</item>
<item name="android:padding">1dp</item>
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
</style>
<style name="CommonFabStyle">
<item name="fab_showShadow">true</item>
<item name="fab_size">mini</item>
<item name="menu_labels_textSize">28sp</item>
<item name="fab_colorNormal">#2550</item>
<item name="fab_shadowColor">@color/finestSilver</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">28dp</item>
<item name="android:onClick">floatClick</item>
</style>
</resources>

1
gdrive/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

53
gdrive/build.gradle.kts Normal file
View File

@ -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")
}

21
gdrive/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
<manifest package="kr.bums.lunatic.utils.gdrive"/>

View File

@ -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<String> = 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()
}
}
}

View File

@ -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<Intent> =
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() }
}
}

View File

@ -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<String, String> // 파일명(키)과 JSON 텍스트(값)의 쌍
)

View File

@ -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)
}
}

View File

@ -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")