diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt index 916f5c41..674061c5 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt @@ -65,6 +65,7 @@ import androidx.work.NetworkType import androidx.work.workDataOf import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString +import bums.lunatic.launcher.model.QuoteItem import bums.lunatic.launcher.model.WallContentGroup import bums.lunatic.launcher.workers.WorkersDb import com.google.common.reflect.TypeToken @@ -77,6 +78,87 @@ import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.util.Calendar +data class ZenQuoteResponse(val q: String, val a: String) +data class KorAdviceResponse(val author: String, val message: String) + +class QuoteFetchWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + return try { + fetchAndSaveQuotes() + Result.success() + } catch (e: Exception) { + Blog.LOGE("QuoteFetchWorker Failed", e) + Result.retry() // 실패 시 재시도 + } + } + + private suspend fun fetchAndSaveQuotes() { + withContext(Dispatchers.IO) { + Blog.LOGE("fetchAndSaveQuotes") + val client = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .build() + val gson = Gson() + val realm = WorkersDb.getRealm() + + // 5회 반복 호출 + for (i in 1..5) { + try { + // 1. ZenQuotes (영어 명언) 호출 - 배열 형태로 응답옴 + val zenRequest = Request.Builder().url("https://zenquotes.io/api/random").build() + val zenResponse = client.newCall(zenRequest).execute() + + if (zenResponse.isSuccessful) { + zenResponse.body?.string()?.let { json -> + val listType = object : TypeToken>() {}.type + val zenList: List = gson.fromJson(json, listType) + if (zenList.isNotEmpty()) { + val quote = zenList[0] + realm.writeBlocking { + copyToRealm(QuoteItem().apply { + textEn = quote.q + authorEn = quote.a + sourceApi = "ZENQUOTES" + isTranslated = false + timestamp = System.currentTimeMillis() + }) + } + } + } + } + + // 2. Korean Advice (한국어 명언) 호출 - 객체 형태로 응답옴 + val korRequest = Request.Builder().url("https://korean-advice-open-api.vercel.app/api/advice").build() + val korResponse = client.newCall(korRequest).execute() + + if (korResponse.isSuccessful) { + korResponse.body?.string()?.let { json -> + val korQuote = gson.fromJson(json, KorAdviceResponse::class.java) + realm.writeBlocking { + copyToRealm(QuoteItem().apply { + textKo = korQuote.message + authorKo = korQuote.author + sourceApi = "KOR_ADVICE" + isTranslated = false + timestamp = System.currentTimeMillis() + }) + } + } + } + + // 무료 API Rate Limit(호출 제한) 방어를 위해 1.5초 대기 + delay(1500) + + } catch (e: Exception) { + Blog.LOGE("Quote Fetch Iteration $i Failed", e) + } + } + } + } +} + class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val folderPath = inputData.getString("FOLDER_PATH") ?: return Result.failure() @@ -652,6 +734,22 @@ class ForeGroundService : Service() { wallpaperRequest ) + val quoteRequest = PeriodicWorkRequestBuilder( + 1, TimeUnit.HOURS // 1시간 간격 설정 + ) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) // 네트워크가 연결된 상태에서만 실행 + .build() + ) + .build() + + workManager.enqueueUniquePeriodicWork( + "QuoteFetchWork", + ExistingPeriodicWorkPolicy.KEEP, + quoteRequest + ) + } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/model/QuoteItem.kt b/app/src/main/kotlin/bums/lunatic/launcher/model/QuoteItem.kt new file mode 100644 index 00000000..083607a8 --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/model/QuoteItem.kt @@ -0,0 +1,19 @@ +package bums.lunatic.launcher.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey +import org.mongodb.kbson.ObjectId + +class QuoteItem : RealmObject { + @PrimaryKey + var _id: ObjectId = ObjectId() + + var textEn: String = "" // 영문 텍스트 + var textKo: String = "" // 한글 텍스트 + var authorEn: String = "" // 영문 작가명 + var authorKo: String = "" // 한글 작가명 + + var sourceApi: String = "" // "ZENQUOTES" 또는 "KOR_ADVICE" + var isTranslated: Boolean = false // AI를 통해 양방향 번역이 완료되었는지 여부 + var timestamp: Long = System.currentTimeMillis() +} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt index f3ea9a23..e34fd84b 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/settings/SettingsActivity.kt @@ -35,12 +35,14 @@ import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS import bums.lunatic.launcher.helpers.PrefBoolean import bums.lunatic.launcher.helpers.PrefHelper import bums.lunatic.launcher.model.AppInfo +import bums.lunatic.launcher.model.QuoteItem import bums.lunatic.launcher.model.SimpleContact import bums.lunatic.launcher.settings.childs.Apps 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 bums.lunatic.launcher.workers.WorkersDb.getRealm import kr.gdrive.bums.lunatic.utils.BackupPayload import kr.gdrive.bums.lunatic.utils.GDriveBackupTask import kr.gdrive.bums.lunatic.utils.GDriveLoginManager @@ -175,6 +177,14 @@ internal class SettingsActivity : CommonActivity() { } } + fun getUntranslatedQuotesAsJson(): String { + val realm = getRealm() + val untranslatedList = realm.query("isTranslated == false").find() + + // 리스트를 통째로 Gson을 사용해 JSON String으로 변환 (Logcat이나 파일로 출력) + return Gson().toJson(untranslatedList) + } + private fun updateUiForLoggedIn(email: String) { binding.btnGoogleLogin.text = "연결 해제 ($email)" binding.btnManualBackup.isEnabled = true diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt index 8815cbe7..eeacf49c 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -26,6 +26,7 @@ import android.os.Handler import android.os.HandlerThread import android.os.ParcelFileDescriptor import android.service.wallpaper.WallpaperService +import android.text.SpannableStringBuilder import android.util.Log import android.view.Gravity import android.view.SurfaceHolder @@ -35,10 +36,12 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.core.graphics.alpha import bums.lunatic.launcher.R +import bums.lunatic.launcher.model.QuoteItem import bums.lunatic.launcher.model.Translation import bums.lunatic.launcher.model.WallContentGroup import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.workers.LocationUpdateService +import bums.lunatic.launcher.workers.WorkersDb import com.google.common.reflect.TypeToken import com.google.gson.Gson import java.io.File @@ -60,7 +63,7 @@ class MyWallpaperService : WallpaperService() { data class BatteryInfo( val percent: Int, val isCharging: Boolean, - val speedText: String, +// val speedText: String, val detailsText: String // "9.0V 1.5A (13.5W)" 같은 상세 정보 ) @@ -76,16 +79,16 @@ class MyWallpaperService : WallpaperService() { val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL - var speedText = "" +// var speedText = "" var detailsText = "" if (isCharging) { - speedText = when (plugged) { - BatteryManager.BATTERY_PLUGGED_AC -> "고속" - BatteryManager.BATTERY_PLUGGED_USB -> "저속" - BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선" - else -> "" - } +// speedText = when (plugged) { +// BatteryManager.BATTERY_PLUGGED_AC -> "고속" +// BatteryManager.BATTERY_PLUGGED_USB -> "저속" +// BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선" +// else -> "" +// } // 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV) val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0 @@ -108,7 +111,7 @@ class MyWallpaperService : WallpaperService() { } } - return BatteryInfo(batteryPct, isCharging, speedText, detailsText) + return BatteryInfo(batteryPct, isCharging, detailsText) } inner class NativeRenderEngine : Engine() { @@ -405,40 +408,139 @@ class MyWallpaperService : WallpaperService() { } } - private fun drawBitmapFromText(weatherLines: List, isCharging: Boolean = false): Bitmap? { - // 1. 데이터 준비 - val dateFull = SimpleDateFormat("yyyy.MM.dd (E)", Locale.KOREAN).format(Date()) - val ramInfo = getRamUsage() - val tempInfo = getDeviceTemperature() - val batteryInfo = getBatteryStatus().let { info -> - if (info.isCharging && info.detailsText.isNotEmpty()) { - "${info.percent}% ⚡${info.speedText} [${info.detailsText}]" - } else if (info.isCharging) { - "${info.percent}% ⚡${info.speedText}" - } else { - "${info.percent}%" - } - } + private fun getKoreanWeatherCondition(condition: String): String { + val lower = condition.trim().lowercase() + return when { + lower == "sunny" || lower == "clear" -> "맑음" + lower == "partly cloudy" -> "구름 조금" + lower == "cloudy" -> "구름 많음" + lower == "overcast" -> "흐림" + lower.contains("mist") || lower.contains("fog") -> "안개" + lower.contains("blizzard") || lower.contains("blowing snow") -> "눈보라" + + // 뇌우(번개) 계열 + lower.contains("thunder") && lower.contains("snow") -> "천둥/눈" + lower.contains("thunder") && lower.contains("rain") -> "천둥/비" + lower.contains("thunder") -> "뇌우" + + // 얼음 섞인 비/눈 계열 + lower.contains("sleet") -> "진눈깨비" + lower.contains("ice pellets") -> "싸락눈" + + // 비 계열 (우선순위: 폭우 -> 소나기 -> 강한 비 -> 이슬비 -> 비) + lower.contains("torrential") -> "폭우" + lower.contains("heavy") && lower.contains("shower") -> "강한 소나기" + lower.contains("shower") -> "소나기" + lower.contains("heavy rain") -> "강한 비" + lower.contains("drizzle") -> "이슬비" + lower.contains("rain") -> "비" + + // 눈 계열 (우선순위: 폭설 -> 눈) + lower.contains("heavy snow") -> "폭설" + lower.contains("snow") -> "눈" + + // 매핑되지 않은 기본값 (혹시 모를 예외 대비) + else -> condition + } + } + val fontz = arrayListOf( + R.font.cafe24ohsquareair, + R.font.cafe24oneprettynight, + R.font.cafe24ssukssukregular, + R.font.dovemayo, + R.font.ebs_r, + R.font.gabia_solmee, + R.font.godomaum, + R.font.jsarirang_hon, + R.font.jsarirang_ppuri, + R.font.jsdongkang_regular, + R.font.kcc_kimhoon, + R.font.kcc_sonkeechung, + R.font.kccahnjunggeun, + R.font.kotra_bold, + R.font.kotra_songeulssi, + R.font.kyobo_handwriting_2019, + R.font.kyobo_handwriting_2021sjy, + R.font.kyobohandwriting2024psw, + R.font.mapoagape, + R.font.mapobackpacking, + R.font.mapodacapo, + R.font.mapodpp, + R.font.mapodpp_2, + R.font.mapoflowerisland, + R.font.mapogoldenpier, + R.font.mapomaponaru, + R.font.mapopeacefull, + R.font.material_symbols, + R.font.nnsgc_brhp, + R.font.nnsgc_gd_an_gd, + R.font.nnsgc_md, + R.font.nnsgc_wsjidyp, + R.font.nnsgc_yjc, + R.font.on_jsuhl, + R.font.on_jsuhr, + R.font.on_sbsjl, + R.font.on_sbsjr, + R.font.on_treeususimgul, + R.font.on_treeususimgul_r, + R.font.on_wibsr, + R.font.on_wisbl, + R.font.on_ychyuhl, + R.font.on_ychyuhr, + R.font.ssshinb7, + R.font.taebaek_milkyway, + R.font.taefont_tsthlml, + R.font.tvn_jguiyg_light, + R.font.tvn_jguiyg_medium, + R.font.wandohoper, + R.font.ylee_mortal_heart_immortal_memory, + ) + + var monoBold = resources.getFont(fontz.random()) + var quote = resources.getFont(fontz.random()) + var local = resources.getFont(fontz.random()) + var numbers = resources.getFont(fontz.random()) + + private fun drawBitmapFromText(weatherLines: List, isCharging: Boolean = false): Bitmap? { + // 💡 1. 원의 크기와 내부 여백 계산 + val displayMetrics = resources.displayMetrics + var sizeRate = 1.1f + val circleSize = ((displayMetrics.widthPixels * 0.48f).toInt() * sizeRate).toInt() + val padding = 5 + val maxContentWidth = circleSize - (padding * 2) + + if (circleSize <= 0) return null + + // 💡 2. 텍스트 크기 3단계 변수 정의 (가독성과 감성을 모두 잡은 안정적인 비율) + val iconRate = 1.3f + val sizeLarge = 24f // 메인 날씨 상태 + val sizeMedium = 15f // 날짜, 주소, 온도, 명언 본문 + val sizeSmall = 13f // 시스템 정보, 명언 번역, 작가 + + // 3. 데이터 준비 + val dateFull = SimpleDateFormat("yyyy.MM.dd(E)", Locale.ENGLISH).format(Date()) val weatherCondition = if (weatherLines.isNotEmpty()) weatherLines[0] else "" val weatherTemp = if (weatherLines.size > 1) weatherLines[1] else "" val weatherHum = if (weatherLines.size > 2) weatherLines[2] else "" val address = if (weatherLines.size > 3) weatherLines[3] else "" val wrapContent = ViewGroup.LayoutParams.WRAP_CONTENT - val monoBold = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD) - // 2. 뷰 생성 헬퍼 - fun createTextView(textToSet: String, textSizeDp: Float, font: Typeface, alphaValue: Float = 1.0f): TextView { + + + // 4. 뷰 생성 헬퍼 + fun createTextView(textToSet: CharSequence, textSizeDp: Float, font: Typeface, alphaValue: Float = 1.0f): TextView { return TextView(this@MyWallpaperService).apply { layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent) + maxWidth = maxContentWidth text = textToSet textSize = textSizeDp typeface = font setTextColor(Color.WHITE) alpha = alphaValue gravity = Gravity.CENTER - setShadowLayer(8f, 2f, 2f, Color.parseColor("#99000000")) + setShadowLayer(6f, 3f, 3f, Color.parseColor("#AA000000")) } } @@ -447,51 +549,108 @@ class MyWallpaperService : WallpaperService() { layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent) orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER + setPadding(1, 1, 1, 1) } } - - // 3. 각 줄(Row)을 생성하여 리스트에 담기 + val sysAlpha = 0.9f + // 5. 각 줄(Row) 생성 val rowsToArrange = mutableListOf() // ① 날씨 (아이콘 + 상태) if (weatherCondition.isNotEmpty()) { val weatherRow = createHorizontalLayout() - weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), 50f, materialIconFont)) - weatherRow.addView(createTextView(weatherCondition, 28f, monoBold).apply { setPadding(20, 0, 0, 0) }) + // 아이콘은 Large 텍스트보다 살짝 크게 배율 고정 + weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), sizeLarge * iconRate, materialIconFont)) + weatherRow.addView(createTextView(" " + getKoreanWeatherCondition(weatherCondition), sizeLarge, monoBold)) rowsToArrange.add(weatherRow) } // ② 온도 & 습도 if (weatherTemp.isNotEmpty()) { val tempRow = createHorizontalLayout() - val tempStr = weatherTemp.trim() val humStr = weatherHum.trim() - tempRow.addView(createTextView("thermostat", 20f, materialIconFont, 0.9f)) - tempRow.addView(createTextView(tempStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 30, 0) }) + tempRow.addView(createTextView("thermostat", sizeMedium * iconRate, materialIconFont, sysAlpha)) + tempRow.addView(createTextView(tempStr, sizeMedium, numbers,sysAlpha)) if (humStr.isNotEmpty()) { - tempRow.addView(createTextView("water_drop", 20f, materialIconFont, 0.9f)) - tempRow.addView(createTextView(humStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 0, 0) }) + tempRow.addView(createTextView("water_drop", sizeMedium * iconRate, materialIconFont, sysAlpha)) + tempRow.addView(createTextView(humStr, sizeMedium, numbers, sysAlpha)) } rowsToArrange.add(tempRow) } // ③ 주소 if (address.isNotEmpty()) { - rowsToArrange.add(createTextView(address, 16f, monoBold, 0.95f)) + rowsToArrange.add(createTextView(address, sizeMedium, local, sysAlpha)) } // ④ 날짜 - rowsToArrange.add(createTextView(dateFull, 14f, monoBold, 0.8f)) + rowsToArrange.add(createTextView(dateFull, sizeMedium, local)) - // ⑤ 시스템 정보 (한 줄로 묶음) - rowsToArrange.add(createTextView("$ramInfo | $tempInfo | Battery $batteryInfo", 12f, monoBold, 0.6f)) + // ⑤ 시스템 정보 + val sysInfoRow = createHorizontalLayout() - // 4. 가로 길이 측정 후 다이아몬드 형태로 재정렬 - val unspecifiedSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - rowsToArrange.forEach { it.measure(unspecifiedSpec, unspecifiedSpec) } + + val ramValue = getRamUsage().replace("RAM: ", "") + sysInfoRow.addView(createTextView("memory", sizeSmall * iconRate, materialIconFont, sysAlpha)) + sysInfoRow.addView(createTextView(ramValue, sizeSmall, numbers, sysAlpha).apply { setPadding(0, 0, 15, 0) }) + + val tempValue = getDeviceTemperature().replace("Temp: ", "") + sysInfoRow.addView(createTextView("device_thermostat", sizeSmall * iconRate, materialIconFont, sysAlpha)) + sysInfoRow.addView(createTextView(tempValue, sizeSmall, numbers, sysAlpha).apply { setPadding(0, 0, 15, 0) }) + + val batteryInfo = getBatteryStatus() + val batteryIcon = if (batteryInfo.isCharging) "battery_charging_full" else "battery_full" + val batteryText = "${batteryInfo.percent}%" + + sysInfoRow.addView(createTextView(batteryIcon, sizeSmall * iconRate, materialIconFont, sysAlpha)) + sysInfoRow.addView(createTextView(batteryText, sizeSmall, numbers, sysAlpha)) + rowsToArrange.add(sysInfoRow) + + // ⑥ 명언 + randomQuote?.let { randomQuote -> + val builder = SpannableStringBuilder() + + fun appendSpanned(text: String, sizeDp: Float, font: Typeface, alpha: Float) { + val start = builder.length + builder.append(text) + val end = builder.length + + builder.setSpan(android.text.style.AbsoluteSizeSpan(sizeDp.toInt(), true), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val alphaInt = (alpha * 255).toInt() + val color = Color.argb(alphaInt, 255, 255, 255) + builder.setSpan(android.text.style.ForegroundColorSpan(color), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + builder.setSpan(object : android.text.style.MetricAffectingSpan() { + override fun updateDrawState(tp: android.text.TextPaint) { tp.typeface = font } + override fun updateMeasureState(tp: android.text.TextPaint) { tp.typeface = font } + }, start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + if (randomQuote.isTranslated) { + appendSpanned("\"${randomQuote.textEn}\"", sizeMedium, quote, 1.0f) + builder.append("\n") + appendSpanned(randomQuote.textKo, sizeSmall, monoBold, sysAlpha) + val author = if (randomQuote.authorKo.isNotEmpty()) randomQuote.authorKo else randomQuote.authorEn + appendSpanned(" - $author -", sizeSmall, monoBold, sysAlpha) + } else { + val mainText = if (randomQuote.textKo.isNotEmpty()) randomQuote.textKo else randomQuote.textEn + val author = if (randomQuote.authorKo.isNotEmpty()) randomQuote.authorKo else randomQuote.authorEn + appendSpanned("\"$mainText\"", sizeMedium, quote, 1.0f) + appendSpanned(" - $author -", sizeSmall, monoBold, sysAlpha) + } + + val quoteView = createTextView(builder, sizeMedium, quote, 1.0f).apply { +// setLineSpacing(0f, 1.2f) + } + rowsToArrange.add(quoteView) + } + + // 💡 6. 정렬 및 다이아몬드 배열 + val widthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.AT_MOST) + val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + rowsToArrange.forEach { it.measure(widthSpec, heightSpec) } val sortedRows = rowsToArrange.sortedBy { it.measuredWidth } val arrangedRows = arrayOfNulls(sortedRows.size) @@ -503,49 +662,32 @@ class MyWallpaperService : WallpaperService() { else arrangedRows[bottomIndex--] = sortedRows[i] } - // 5. 최종 부모 레이아웃 조립 (원형/타원형 배경) + // 7. 35% 고정 크기 원형 틀 생성 val rootLayout = LinearLayout(this@MyWallpaperService).apply { - layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent) + layoutParams = ViewGroup.LayoutParams(circleSize, circleSize) orientation = LinearLayout.VERTICAL gravity = Gravity.CENTER - // 💡 원형 안에 글씨가 넉넉히 들어가도록 패딩을 충분히 줍니다. - setPadding(20, 20, 20, 20) background = WavyOvalDrawable( - bgColor = Color.parseColor("#33000000"), - strokeColor = Color.parseColor("#22000000"), // 물결이 잘 보이도록 불투명도를 살짝 올림 - strokeThick = 30f + bgColor = Color.parseColor("#40000000"), + strokeColor = Color.parseColor("#09000000"), + strokeThick = 40f ) } arrangedRows.forEach { rowView -> if (rowView != null) { val lp = LinearLayout.LayoutParams(wrapContent, wrapContent).apply { - setMargins(0, 10, 0, 10) + setMargins(0, 2, 0, 2) } rootLayout.addView(rowView, lp) } } - // 6. 💡 완벽한 정원형(Perfect Circle)을 위한 2단계 측정(Measure) 로직 - val displayMetrics = resources.displayMetrics - val maxWidthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, View.MeasureSpec.AT_MOST) - - // 1차 측정: 내용물들이 자연스럽게 늘어났을 때의 크기(너비, 높이)를 구합니다. - rootLayout.measure(maxWidthSpec, unspecifiedSpec) - - // 너비와 높이 중 '더 큰 값'을 찾아 원의 지름(Size)으로 삼습니다. - val circleSize = Math.max(rootLayout.measuredWidth, rootLayout.measuredHeight) - - if (circleSize <= 0) return null - - // 2차 측정: 너비와 높이를 원의 지름 크기로 '정확히(EXACTLY)' 덮어씌웁니다. + // 8. 강제 측정, 렌더링 및 캡처 val exactSpec = View.MeasureSpec.makeMeasureSpec(circleSize, View.MeasureSpec.EXACTLY) rootLayout.measure(exactSpec, exactSpec) - - // 레이아웃 배치 rootLayout.layout(0, 0, circleSize, circleSize) - // 7. 비트맵 캡처 val bitmap = Bitmap.createBitmap(circleSize, circleSize, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) rootLayout.draw(canvas) @@ -598,12 +740,20 @@ class MyWallpaperService : WallpaperService() { } val mediaDir = File(File(this@MyWallpaperService.getExternalFilesDir(null), "completed_torrents"), "Images") val supportedExtensions = listOf("mp4", "mkv", "avi", "mov","webm", "jpg", "jpeg", "png", "bmp", "webp", "gif") - + var randomQuote : QuoteItem? = null private val nextMediaCallback = object : NativeRenderer.NextMediaCallback { override fun onNextMediaRequested() { loadFiles() val nextFile = mediaFiles.random() Log.d(TAG, "Callback: Preloading next random media: ${nextFile.absolutePath}") +// ⑥ 명언 (DB 랜덤 추출) + val realm = WorkersDb.getRealm() + val quotes = realm.query(QuoteItem::class).find().shuffled() + randomQuote = if (quotes.isNotEmpty()) quotes.random() else null + monoBold = resources.getFont(fontz.random()) + quote = resources.getFont(fontz.random()) + local = resources.getFont(fontz.random()) + numbers = resources.getFont(fontz.random()) getFdFromPath(nextFile.absolutePath)?.let { fd -> nativeRenderer?.startNextPreload(fd) @@ -682,7 +832,7 @@ class WavyOvalDrawable( private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = strokeColor style = Paint.Style.STROKE - strokeWidth = strokeThick + strokeWidth = 100f strokeCap = Paint.Cap.ROUND strokeJoin = Paint.Join.ROUND @@ -706,14 +856,14 @@ class WavyOvalDrawable( if (radius > 0) { // 바깥쪽 색상: 입력받은 bgColor에서 투명도(Alpha)만 0으로 날려버린 색 // 이렇게 해야 검은색->흰색으로 깨지는 현상 없이 자연스럽게 투명해집니다. - val transparentColor = bgColor and 0x11FFFFFF + val transparentColor = bgColor and 0x10FFFFFF bgPaint.shader = RadialGradient( centerX, centerY, radius, intArrayOf(bgColor, transparentColor), // 중앙 -> 바깥 색상 배열 - floatArrayOf(0.4f, 1.0f), // 중심에서 40% 지점부터 투명해지기 시작 + floatArrayOf(0.4f, 1.1f), // 중심에서 40% 지점부터 투명해지기 시작 Shader.TileMode.CLAMP ) } @@ -722,15 +872,17 @@ class WavyOvalDrawable( override fun draw(canvas: Canvas) { val rectF = RectF(bounds) - // 1. 방사형 그라데이션 배경 채우기 - canvas.drawOval(rectF, bgPaint) + // 2. 선 잘림 방지용 여백 계산 - val inset = amplitude + 3f + val inset = 8f rectF.inset(inset, inset) // 3. 물결 테두리 선 그리기 canvas.drawOval(rectF, strokePaint) + + // 1. 방사형 그라데이션 배경 채우기 + canvas.drawOval(rectF, bgPaint) } override fun setAlpha(alpha: Int) { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt index d75db690..77c7bb0f 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt @@ -49,6 +49,7 @@ class LocationUpdateService : Service(), LocationListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { geocoder.getFromLocation(lat, long, 1) { addresses -> addresses.first()?.let { + addresses.get(0) WorkersDb.getRealm()?.apply { LocationLog().let { loc -> loc.fillData(it) @@ -123,7 +124,9 @@ class LocationUpdateService : Service(), LocationListener { if (body != null) { var result = body.string() var w = Gson().fromJson(result,CurrentWeather::class.java) - w.addr = addresses.first().getAddressLine(0).replace("대한민국", "") + var address =addresses.first() + + w.addr = address.getAddressLine(0) lastWeather.clear() lastWeather.addAll(w.getSummaryInfo()) Blog.LOGE("Location >>> ${result}\n${lastWeather}") @@ -265,7 +268,7 @@ open class Location { open class CurrentWeather { var addr: String? = null var current: Current? = null - + var location : bums.lunatic.launcher.workers.Location? = null // 아이콘 URL을 배열의 4번째(index 3) 요소로 추가 전달합니다. fun getSummaryInfo(): List { val iconUrl = current?.condition?.icon?.let { "https:$it" } ?: "" @@ -273,7 +276,7 @@ open class CurrentWeather { "${current?.condition?.text}", // 0: 날씨 상태 "${current?.temp_c}", // 1: 온도 및 습도 "${current?.humidity}", - "$addr", // 2: 주소 + "${addr}", // 2: 주소 iconUrl // 3: 아이콘 URL ) } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt index 2bd12268..f926f522 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -117,35 +117,35 @@ class TorrentService : Service() { var speedText = "" var detailsText = "" - if (isCharging) { - speedText = when (plugged) { - BatteryManager.BATTERY_PLUGGED_AC -> "고속" - BatteryManager.BATTERY_PLUGGED_USB -> "저속" - BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선" - else -> "" - } - - // 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV) - val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0 - val voltageV = voltageMv / 1000.0f - - // 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회 - val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager - // 안드로이드 표준 단위는 마이크로암페어(µA) 입니다. - val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW) - - // 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환 - val currentA = Math.abs(currentUa) / 1000000.0f - - // 3. 전력(Watt) 계산: W = V * A - val wattage = voltageV * currentA - - // 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어) - if (voltageV > 0f && currentA > 0f) { - detailsText = String.format("%.1fV %.1fA (%.1fW)", voltageV, currentA, wattage) - } - } - Blog.LOGE("detailsText >>> $detailsText") +// if (isCharging) { +// speedText = when (plugged) { +// BatteryManager.BATTERY_PLUGGED_AC -> "고속" +// BatteryManager.BATTERY_PLUGGED_USB -> "저속" +// BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선" +// else -> "" +// } +// +// // 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV) +// val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0 +// val voltageV = voltageMv / 1000.0f +// +// // 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회 +// val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager +// // 안드로이드 표준 단위는 마이크로암페어(µA) 입니다. +// val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW) +// +// // 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환 +// val currentA = Math.abs(currentUa) / 1000000.0f +// +// // 3. 전력(Watt) 계산: W = V * A +// val wattage = voltageV * currentA +// +// // 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어) +// if (voltageV > 0f && currentA > 0f) { +// detailsText = String.format("%.1fV %.1fA (%.1fW)", voltageV, currentA, wattage) +// } +// } +// Blog.LOGE("detailsText >>> $detailsText") updateSessionState() } @@ -159,10 +159,8 @@ class TorrentService : Service() { networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - if (isWifiConnected != isWifi) { - isWifiConnected = isWifi - updateSessionState() - } + isWifiConnected = isWifi + updateSessionState() } } connectivityManager.registerNetworkCallback(request, networkCallback!!) @@ -602,5 +600,5 @@ class TorrentService : Service() { e.printStackTrace() } } - + } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt index 5d479901..fccd0e83 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/WorkersDb.kt @@ -36,6 +36,7 @@ import bums.lunatic.launcher.home.tokiz.LastInfo import bums.lunatic.launcher.home.tokiz.ReaderConfig import bums.lunatic.launcher.model.ExpressionItem import bums.lunatic.launcher.model.ExpressionWord +import bums.lunatic.launcher.model.QuoteItem import bums.lunatic.launcher.model.WallContentGroup import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.JamoUtils @@ -79,7 +80,7 @@ object WorkersDb { LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class, AppUsageLog::class, WidgetData::class, - ExpressionItem::class,ExpressionWord::class + ExpressionItem::class,ExpressionWord::class,QuoteItem::class ) //,UserActionModel::class diff --git a/app/src/main/res/font/cafe24ohsquareair.ttf b/app/src/main/res/font/cafe24ohsquareair.ttf new file mode 100644 index 00000000..a0a5c11b Binary files /dev/null and b/app/src/main/res/font/cafe24ohsquareair.ttf differ diff --git a/app/src/main/res/font/cafe24oneprettynight.ttf b/app/src/main/res/font/cafe24oneprettynight.ttf index 02524ed8..db78f058 100644 Binary files a/app/src/main/res/font/cafe24oneprettynight.ttf and b/app/src/main/res/font/cafe24oneprettynight.ttf differ diff --git a/app/src/main/res/font/cafe24ssukssukregular.ttf b/app/src/main/res/font/cafe24ssukssukregular.ttf new file mode 100644 index 00000000..ea734425 Binary files /dev/null and b/app/src/main/res/font/cafe24ssukssukregular.ttf differ diff --git a/app/src/main/res/font/kyobo_handwriting_2019.ttf b/app/src/main/res/font/kyobo_handwriting_2019.ttf new file mode 100644 index 00000000..ed9354dd Binary files /dev/null and b/app/src/main/res/font/kyobo_handwriting_2019.ttf differ diff --git a/app/src/main/res/font/kyobohandwriting2024psw.ttf b/app/src/main/res/font/kyobohandwriting2024psw.ttf new file mode 100644 index 00000000..390067fd Binary files /dev/null and b/app/src/main/res/font/kyobohandwriting2024psw.ttf differ