...
This commit is contained in:
parent
9526104a4a
commit
e9e585bad0
@ -9,6 +9,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import bums.lunatic.launcher.apps.SearchMenu
|
import bums.lunatic.launcher.apps.SearchMenu
|
||||||
import bums.lunatic.launcher.helpers.PrefBoolean
|
import bums.lunatic.launcher.helpers.PrefBoolean
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
|
|||||||
@ -60,10 +60,22 @@ import android.os.Vibrator
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.core.content.ContentProviderCompat.requireContext
|
import androidx.core.content.ContentProviderCompat.requireContext
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.NetworkType
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString
|
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString
|
||||||
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString
|
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString
|
||||||
|
import bums.lunatic.launcher.model.WallContentGroup
|
||||||
|
import bums.lunatic.launcher.workers.WorkersDb
|
||||||
|
import com.google.common.reflect.TypeToken
|
||||||
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
@ -159,6 +171,155 @@ class AggregatedNewsWorker(private val context: Context, params: WorkerParameter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WallContentsWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
// 1. 필요한 Gemini 응답 데이터 클래스 추가
|
||||||
|
data class GeminiResponse(val candidates: List<Candidate>)
|
||||||
|
data class Candidate(val content: Content)
|
||||||
|
data class Content(val parts: List<Part>)
|
||||||
|
data class Part(val text: String)
|
||||||
|
|
||||||
|
// 불필요한 최상단 따옴표를 제거한 깔끔한 프롬프트
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
refreshGeminiContent()
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Blog.LOGE("WallContentsWorker Failed", e)
|
||||||
|
Result.retry() // 일시적 오류일 수 있으므로 재시도 반환
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTimeBasedContext(): String {
|
||||||
|
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
||||||
|
|
||||||
|
return when (hour) {
|
||||||
|
in 6..10 -> "지금은 아침 시간이야. 카페에서 커피/브런치를 주문하거나, 상쾌하게 아침 인사를 나누는 상황에 맞는 가벼운 스몰토크를 알려줘."
|
||||||
|
in 11..16 -> "지금은 낮 시간이야. 관광지에서 길을 묻거나, 점심 식사 주문, 상점에서 물건을 구매할 때 쓰는 활동적인 실생활 표현을 알려줘."
|
||||||
|
in 17..21 -> "지금은 저녁 시간이야. 분위기 있는 식당에서 저녁을 먹거나, 펍에서 맥주 한잔하며 현지인과 가볍게 나누는 스몰토크를 알려줘."
|
||||||
|
else -> "지금은 늦은 밤이야. 숙소(호텔) 프론트에 문의하거나, 편의점에서 야식을 사고, 내일 일정을 묻는 등 밤 시간에 필요한 실용적인 표현을 알려줘."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshGeminiContent() {
|
||||||
|
val apiKey = PrefString.geminiApiKey.get("") // 설정에서 가져온 키
|
||||||
|
if (apiKey.isEmpty()) {
|
||||||
|
Blog.LOGE("Gemini API Key is empty.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultJson = callGeminiApi(apiKey, getDynamicPrompt()) ?: return
|
||||||
|
Blog.LOGE("resultJson >>> $resultJson")
|
||||||
|
val newGroups = parseGeminiResponse(resultJson) ?: return
|
||||||
|
Blog.LOGE("newGroups.size ${newGroups.size}")
|
||||||
|
WorkersDb.insertExpressions(newGroups)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDynamicPrompt(): String {
|
||||||
|
val myStyle = PrefString.travelStyle.get("주로 현지 로컬 맛집을 찾아다니고, 가벼운 스몰토크를 즐기는 편이야. 흡연 및 음주를 즐기는 편이고, 여행은 항상 부인과 같이 다녀")
|
||||||
|
val timeContext = getTimeBasedContext()
|
||||||
|
|
||||||
|
return """
|
||||||
|
너는 여행 및 외국어 학습 전문가야. 아래의 [나의 스타일]과 [현재 상황]을 반영해서, 당장 써먹을 수 있는 유용한 실용 회화 표현 12가지를 선정해줘.
|
||||||
|
|
||||||
|
[나의 스타일]: $myStyle
|
||||||
|
[현재 상황]: $timeContext
|
||||||
|
|
||||||
|
각 표현에 대해 영어(EN), 스페인어(ES), 일본어(JA), 중국어(ZH) 네 가지 버전을 제공해.
|
||||||
|
|
||||||
|
중요 규칙:
|
||||||
|
- 모든 외국어 문장의 'pron'에는 최대한 정확한 한국어 발음을 적어줘.
|
||||||
|
- 일본어(JA)는 한자와 가나 혼용, 중국어(ZH)는 간체자를 쓰고 'pron'에 병음과 한글 발음을 적어줘.
|
||||||
|
- 각 번역본마다 그 문장에서 가장 중요한 '핵심 단어나 숙어' 2~3개를 뽑아서 `words` 배열에 원단어(word)와 뜻(meaning)으로 정리해줘.
|
||||||
|
|
||||||
|
반드시 아래 JSON 배열 형식으로만 응답해:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"topic": "주제",
|
||||||
|
"translations": [
|
||||||
|
{
|
||||||
|
"lang": "EN",
|
||||||
|
"main": "Sentence",
|
||||||
|
"sub": "한국어 뜻",
|
||||||
|
"pron": "한글 발음",
|
||||||
|
"words": [
|
||||||
|
{"word": "단어1", "meaning": "뜻1", "pron": "단어1 발음"},
|
||||||
|
{"word": "단어2", "meaning": "뜻2", "pron": "단어2 발음"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
... (ES, JA, ZH도 동일한 구조)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// (이 형식으로 총 12개 세트를 제공할 것)
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun callGeminiApi(apiKey: String, prompt: String): String? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // 서버 연결 대기 시간
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS) // 데이터 전달받는 시간 (중요!)
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS) // 데이터 전송 시간
|
||||||
|
.build()
|
||||||
|
val url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=$apiKey"
|
||||||
|
|
||||||
|
// 3. Gson을 사용하여 JSON 구조를 안전하게 생성 (따옴표 깨짐 방지)
|
||||||
|
val requestMap = mapOf(
|
||||||
|
"contents" to listOf(
|
||||||
|
mapOf("parts" to listOf(mapOf("text" to prompt)))
|
||||||
|
),
|
||||||
|
"generationConfig" to mapOf(
|
||||||
|
"responseMimeType" to "application/json"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val jsonRequest = Gson().toJson(requestMap)
|
||||||
|
|
||||||
|
val body = jsonRequest.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body?.string()
|
||||||
|
} else {
|
||||||
|
Blog.LOGE("Gemini API Error: ${response.code} ${response.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseGeminiResponse(jsonString: String): List<WallContentGroup>? {
|
||||||
|
return try {
|
||||||
|
val gson = Gson()
|
||||||
|
val fullResponse = gson.fromJson(jsonString, GeminiResponse::class.java)
|
||||||
|
val innerJson = fullResponse.candidates[0].content.parts[0].text
|
||||||
|
|
||||||
|
val groupType = object : TypeToken<List<WallContentGroup>>() {}.type
|
||||||
|
gson.fromJson(innerJson, groupType)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Blog.LOGE("Parsing Error: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class VibrationWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
class VibrationWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator
|
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator
|
||||||
@ -473,6 +634,24 @@ class ForeGroundService : Service() {
|
|||||||
// 기존의 수많은 enqueue 코드를 이 두 개로 대체
|
// 기존의 수많은 enqueue 코드를 이 두 개로 대체
|
||||||
workManager.enqueueUniquePeriodicWork("AggregatedSystemWork", ExistingPeriodicWorkPolicy.KEEP, systemRequest)
|
workManager.enqueueUniquePeriodicWork("AggregatedSystemWork", ExistingPeriodicWorkPolicy.KEEP, systemRequest)
|
||||||
workManager.enqueueUniquePeriodicWork("AggregatedNewsWork", ExistingPeriodicWorkPolicy.KEEP, newsRequest)
|
workManager.enqueueUniquePeriodicWork("AggregatedNewsWork", ExistingPeriodicWorkPolicy.KEEP, newsRequest)
|
||||||
|
|
||||||
|
|
||||||
|
val wallpaperRequest = PeriodicWorkRequestBuilder<WallContentsWorker>(
|
||||||
|
120, java.util.concurrent.TimeUnit.MINUTES // 💡 2시간 간격으로 설정
|
||||||
|
)
|
||||||
|
.setConstraints(
|
||||||
|
Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED) // 네트워크 연결 시에만
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
"WallContentsWork",
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP,
|
||||||
|
wallpaperRequest
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -648,4 +827,10 @@ class ForeGroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GeminiResponse(val candidates: List<Candidate>)
|
||||||
|
data class Candidate(val content: Content)
|
||||||
|
data class Content(val parts: List<Part>)
|
||||||
|
data class Part(val text: String)
|
||||||
|
|||||||
@ -10,10 +10,12 @@ enum class PrefString : PrefKey<String> {
|
|||||||
defaultTimeFormat,
|
defaultTimeFormat,
|
||||||
defaultDateFormat,
|
defaultDateFormat,
|
||||||
weatherApiKey,
|
weatherApiKey,
|
||||||
|
geminiApiKey,
|
||||||
locationApi,
|
locationApi,
|
||||||
telegramBotApi,
|
telegramBotApi,
|
||||||
telegramMyId,
|
telegramMyId,
|
||||||
telegramSendTarget,
|
telegramSendTarget,
|
||||||
|
travelStyle,
|
||||||
carName;
|
carName;
|
||||||
override fun set(value: String) {PrefHelper.putString(this.name, value)}
|
override fun set(value: String) {PrefHelper.putString(this.name, value)}
|
||||||
override fun get(def : String?) : String = PrefHelper.getString(this.name, def as? String ?: "") ?: ""
|
override fun get(def : String?) : String = PrefHelper.getString(this.name, def as? String ?: "") ?: ""
|
||||||
|
|||||||
@ -0,0 +1,258 @@
|
|||||||
|
package bums.lunatic.launcher.home
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import bums.lunatic.launcher.R
|
||||||
|
import bums.lunatic.launcher.model.ExpressionItem
|
||||||
|
import bums.lunatic.launcher.workers.WorkersDb
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
import io.realm.kotlin.ext.query
|
||||||
|
|
||||||
|
class LearningFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var recyclerExpressions: RecyclerView
|
||||||
|
private lateinit var layoutTopics: ViewGroup
|
||||||
|
private lateinit var layoutLanguages: ViewGroup
|
||||||
|
|
||||||
|
private val adapter = ExpressionAdapter()
|
||||||
|
private var currentTopic: String = ""
|
||||||
|
private var currentLang: String = "EN" // 기본 영어
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_learning, container, false)
|
||||||
|
|
||||||
|
recyclerExpressions = view.findViewById(R.id.recycler_expressions)
|
||||||
|
layoutTopics = view.findViewById(R.id.layout_topics)
|
||||||
|
layoutLanguages = view.findViewById(R.id.layout_languages)
|
||||||
|
|
||||||
|
recyclerExpressions.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
recyclerExpressions.adapter = adapter
|
||||||
|
|
||||||
|
setupFilters()
|
||||||
|
loadExpressions()
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFilters() {
|
||||||
|
val realm = WorkersDb.getRealm()
|
||||||
|
|
||||||
|
// 1. 언어 필터 세팅
|
||||||
|
val langs = listOf(
|
||||||
|
"EN" to "영어",
|
||||||
|
"ES" to "스페인어",
|
||||||
|
"JA" to "일본어",
|
||||||
|
"ZH" to "중국어" // 추가된 부분!
|
||||||
|
)
|
||||||
|
layoutLanguages.removeAllViews()
|
||||||
|
langs.forEach { (code, name) ->
|
||||||
|
val btn = Button(requireContext()).apply {
|
||||||
|
text = name
|
||||||
|
setBackgroundColor(if (currentLang == code) Color.DKGRAY else Color.TRANSPARENT)
|
||||||
|
setTextColor(if (currentLang == code) Color.WHITE else Color.LTGRAY)
|
||||||
|
setOnClickListener {
|
||||||
|
currentLang = code
|
||||||
|
setupFilters() // 색상 갱신을 위해 재호출
|
||||||
|
loadExpressions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutLanguages.addView(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 주제(Topic) 필터 세팅 (DB에서 고유값 가져오기)
|
||||||
|
// 참고: Realm Kotlin은 distinct를 바로 지원하진 않으므로 전체를 가져와서 Set으로 필터링합니다.
|
||||||
|
val allItems = realm.query<ExpressionItem>().find()
|
||||||
|
val topics = allItems.map { it.topic }.distinct()
|
||||||
|
|
||||||
|
layoutTopics.removeAllViews()
|
||||||
|
|
||||||
|
// '전체' 보기 버튼
|
||||||
|
val allBtn = Button(requireContext()).apply {
|
||||||
|
text = "전체"
|
||||||
|
setBackgroundColor(if (currentTopic.isEmpty()) Color.DKGRAY else Color.TRANSPARENT)
|
||||||
|
setTextColor(if (currentTopic.isEmpty()) Color.WHITE else Color.LTGRAY)
|
||||||
|
setOnClickListener {
|
||||||
|
currentTopic = ""
|
||||||
|
setupFilters()
|
||||||
|
loadExpressions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutTopics.addView(allBtn)
|
||||||
|
|
||||||
|
topics.forEach { topic ->
|
||||||
|
val btn = Button(requireContext()).apply {
|
||||||
|
text = topic
|
||||||
|
setBackgroundColor(if (currentTopic == topic) Color.DKGRAY else Color.TRANSPARENT)
|
||||||
|
setTextColor(if (currentTopic == topic) Color.WHITE else Color.LTGRAY)
|
||||||
|
setOnClickListener {
|
||||||
|
currentTopic = topic
|
||||||
|
setupFilters()
|
||||||
|
loadExpressions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutTopics.addView(btn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadExpressions() {
|
||||||
|
val realm = WorkersDb.getRealm()
|
||||||
|
|
||||||
|
var query = realm.query<ExpressionItem>("lang == $0", currentLang)
|
||||||
|
if (currentTopic.isNotEmpty()) {
|
||||||
|
query = query.query("topic == $0", currentTopic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최신순으로 정렬
|
||||||
|
val results = query.sort("timestamp", io.realm.kotlin.query.Sort.DESCENDING).find()
|
||||||
|
adapter.submitList(results.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 내부 RecyclerView 어댑터 ---
|
||||||
|
inner class ExpressionAdapter : RecyclerView.Adapter<ExpressionAdapter.ViewHolder>() {
|
||||||
|
private var items: List<ExpressionItem> = emptyList()
|
||||||
|
|
||||||
|
fun submitList(newItems: List<ExpressionItem>) {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
// 1. 전체 카드 컨테이너
|
||||||
|
val view = LinearLayout(parent.context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(40, 40, 40, 40)
|
||||||
|
layoutParams = ViewGroup.MarginLayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply { setMargins(0, 0, 0, 24) }
|
||||||
|
// 모서리가 둥근 반투명 배경 효과 (간단히 Color 설정)
|
||||||
|
setBackgroundColor(Color.parseColor("#22FFFFFF"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 문장 텍스트
|
||||||
|
val mainText = TextView(parent.context).apply {
|
||||||
|
textSize = 22f
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
setTypeface(null, android.graphics.Typeface.BOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 발음 텍스트
|
||||||
|
val pronText = TextView(parent.context).apply {
|
||||||
|
textSize = 15f
|
||||||
|
setTextColor(Color.parseColor("#80DEEA")) // Cyan 계열
|
||||||
|
setPadding(0, 12, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 뜻 텍스트
|
||||||
|
val subText = TextView(parent.context).apply {
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(Color.LTGRAY)
|
||||||
|
setPadding(0, 8, 0, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 5. 단어들을 담을 ChipGroup (자동 줄바꿈 지원)
|
||||||
|
val chipGroup = ChipGroup(parent.context).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
isSingleLine = false // 공간이 부족하면 아래로 자동 줄바꿈
|
||||||
|
chipSpacingHorizontal = 16
|
||||||
|
chipSpacingVertical = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
view.addView(mainText)
|
||||||
|
view.addView(pronText)
|
||||||
|
view.addView(subText)
|
||||||
|
view.addView(chipGroup) // ChipGroup 추가
|
||||||
|
|
||||||
|
return ViewHolder(view, mainText, pronText, subText, chipGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
|
||||||
|
// 언어별 폰트 느낌 살리기 (옵션)
|
||||||
|
when (item.lang) {
|
||||||
|
"ZH" -> holder.mainText.textLocale = java.util.Locale.SIMPLIFIED_CHINESE
|
||||||
|
"JA" -> holder.mainText.textLocale = java.util.Locale.JAPAN
|
||||||
|
else -> holder.mainText.textLocale = java.util.Locale.US
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.mainText.text = item.mainText
|
||||||
|
holder.pronText.text = "[${item.pronunciation}]"
|
||||||
|
holder.subText.text = item.subText
|
||||||
|
|
||||||
|
// 💡 단어 Chip 생성 로직
|
||||||
|
holder.chipGroup.removeAllViews() // 재사용 방지를 위해 기존 칩 지우기
|
||||||
|
|
||||||
|
item.words.forEach { wordItem ->
|
||||||
|
val chip = Chip(holder.itemView.context).apply {
|
||||||
|
text = wordItem.word
|
||||||
|
isCheckable = false
|
||||||
|
isClickable = true
|
||||||
|
setChipBackgroundColorResource(android.R.color.darker_gray)
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
|
||||||
|
var isShowingMeaning = false
|
||||||
|
setOnClickListener {
|
||||||
|
isShowingMeaning = !isShowingMeaning
|
||||||
|
if (isShowingMeaning) {
|
||||||
|
// 💡 터치 시: 단어 [발음] : 뜻 형태로 보여줌
|
||||||
|
text = "${wordItem.word} [${wordItem.pronunciation}] : ${wordItem.meaning}"
|
||||||
|
setChipBackgroundColorResource(android.R.color.holo_blue_dark)
|
||||||
|
} else {
|
||||||
|
// 다시 터치 시: 원단어만 보임
|
||||||
|
text = wordItem.word
|
||||||
|
setChipBackgroundColorResource(android.R.color.darker_gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
// 언어별 사전 URL 분기 처리
|
||||||
|
val baseUrl = when (item.lang) {
|
||||||
|
"EN" -> "https://en.dict.naver.com/#/search?query="
|
||||||
|
"JA" -> "https://ja.dict.naver.com/#/search?query="
|
||||||
|
"ZH" -> "https://zh.dict.naver.com/#/search?query="
|
||||||
|
"ES" -> "https://dict.naver.com/eskodict/#/search?query="
|
||||||
|
else -> "https://dict.naver.com/search.dict?dicQuery=" // 예비용 통합 검색
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchUrl = baseUrl + wordItem.word
|
||||||
|
|
||||||
|
// 브라우저 띄우기 Intent 생성
|
||||||
|
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(searchUrl))
|
||||||
|
|
||||||
|
// 혹시 액티비티 밖에서 실행될 경우를 대비한 플래그 (어댑터 안에서는 보통 필요 없지만 안전하게 추가)
|
||||||
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
holder.itemView.context.startActivity(intent)
|
||||||
|
|
||||||
|
// true를 반환해야 일반 클릭(setOnClickListener)이 동시에 실행되지 않음
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.chipGroup.addView(chip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
inner class ViewHolder(
|
||||||
|
view: View,
|
||||||
|
val mainText: TextView,
|
||||||
|
val pronText: TextView,
|
||||||
|
val subText: TextView,
|
||||||
|
val chipGroup: ChipGroup // ChipGroup 참조 추가
|
||||||
|
) : RecyclerView.ViewHolder(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -552,6 +552,7 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
R.id.btn_torrent -> TorrentListFragment()
|
R.id.btn_torrent -> TorrentListFragment()
|
||||||
R.id.btn_info -> SystemStatusFragment()
|
R.id.btn_info -> SystemStatusFragment()
|
||||||
R.id.btn_completed_files -> CompletedFilesFragment()
|
R.id.btn_completed_files -> CompletedFilesFragment()
|
||||||
|
R.id.btn_learn -> LearningFragment()
|
||||||
R.id.close -> {
|
R.id.close -> {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
package bums.lunatic.launcher.model
|
||||||
|
|
||||||
|
import io.realm.kotlin.ext.realmListOf
|
||||||
|
import io.realm.kotlin.types.RealmList
|
||||||
|
import io.realm.kotlin.types.RealmObject
|
||||||
|
import io.realm.kotlin.types.annotations.PrimaryKey
|
||||||
|
import org.mongodb.kbson.ObjectId
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class WordItem(
|
||||||
|
val word: String,
|
||||||
|
val meaning: String,
|
||||||
|
val pron: String // 💡 발음 필드 추가!
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Translation(
|
||||||
|
val lang: String,
|
||||||
|
val main: String,
|
||||||
|
val sub: String,
|
||||||
|
val pron: String,
|
||||||
|
val words: List<WordItem> = emptyList() // 💡 단어장 배열 추가!
|
||||||
|
)
|
||||||
|
|
||||||
|
data class WallContentGroup(
|
||||||
|
val topic: String,
|
||||||
|
val translations: List<Translation>
|
||||||
|
)
|
||||||
|
|
||||||
|
class ExpressionWord : RealmObject {
|
||||||
|
var word: String = ""
|
||||||
|
var meaning: String = ""
|
||||||
|
var pronunciation: String = "" // 💡 발음 필드 추가!
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpressionItem : RealmObject {
|
||||||
|
@PrimaryKey
|
||||||
|
var _id: ObjectId = ObjectId()
|
||||||
|
var topic: String = ""
|
||||||
|
var lang: String = "EN"
|
||||||
|
var mainText: String = ""
|
||||||
|
var subText: String = ""
|
||||||
|
var pronunciation: String = ""
|
||||||
|
|
||||||
|
// 💡 DB에 단어 리스트를 저장할 수 있도록 RealmList 추가
|
||||||
|
var words: RealmList<ExpressionWord> = realmListOf()
|
||||||
|
|
||||||
|
var useCount: Int = 0
|
||||||
|
var isMemorized: Boolean = false
|
||||||
|
var timestamp: Long = System.currentTimeMillis()
|
||||||
|
}
|
||||||
@ -24,16 +24,27 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import bums.lunatic.launcher.R
|
import bums.lunatic.launcher.R
|
||||||
import bums.lunatic.launcher.databinding.SettingsPrivitServiceBinding
|
import bums.lunatic.launcher.databinding.SettingsPrivitServiceBinding
|
||||||
import bums.lunatic.launcher.helpers.Constants.Companion.KEY_RSS_URL
|
import bums.lunatic.launcher.helpers.Constants.Companion.KEY_RSS_URL
|
||||||
import bums.lunatic.launcher.helpers.Constants.Companion.KEY_RSS_URL2
|
import bums.lunatic.launcher.helpers.Constants.Companion.KEY_RSS_URL2
|
||||||
import bums.lunatic.launcher.helpers.PrefString
|
import bums.lunatic.launcher.helpers.PrefString
|
||||||
import bums.lunatic.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
import bums.lunatic.launcher.settings.SettingsActivity.Companion.settingsPrefs
|
||||||
|
import bums.lunatic.launcher.utils.Blog
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.common.reflect.TypeToken
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
@ -88,9 +99,30 @@ internal class Misc : SettingChild() {
|
|||||||
PrefString.weatherApiKey.set(text.toString())
|
PrefString.weatherApiKey.set(text.toString())
|
||||||
settingsChanged = true
|
settingsChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
binding.inputgemini.setText(PrefString.geminiApiKey.get(""))
|
||||||
|
binding.inputgemini.doOnTextChanged { text, start, before, count ->
|
||||||
|
text?.let {
|
||||||
|
if (text.length > 38 && isValidGeminiApiKey(text.toString())) {
|
||||||
|
PrefString.geminiApiKey.set(text.toString())
|
||||||
|
settingsChanged = true
|
||||||
|
testGeminiApiCommunication()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
fun isValidGeminiApiKey(apiKey: String): Boolean {
|
||||||
|
val trimmedKey = apiKey.trim()
|
||||||
|
|
||||||
|
// 구글 API 키 표준 정규식 검사 (AIza로 시작하고 총 39자리)
|
||||||
|
val regex = Regex("^AIza[a-zA-Z0-9_\\-]{35}\$")
|
||||||
|
|
||||||
|
return regex.matches(trimmedKey)
|
||||||
|
}
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
(requireDialog() as BottomSheetDialog).dismissWithAnimation = true
|
||||||
@ -106,4 +138,158 @@ internal class Misc : SettingChild() {
|
|||||||
Objects.requireNonNull(binding.inputFeedUrl2.text).toString().trim { it <= ' ' }).apply()
|
Objects.requireNonNull(binding.inputFeedUrl2.text).toString().trim { it <= ' ' }).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class GeminiResponse(val candidates: List<Candidate>)
|
||||||
|
data class Candidate(val content: Content)
|
||||||
|
data class Content(val parts: List<Part>)
|
||||||
|
data class Part(val text: String)
|
||||||
|
|
||||||
|
data class WallContentGroup(
|
||||||
|
val topic: String,
|
||||||
|
val translations: List<Translation>
|
||||||
|
)
|
||||||
|
data class Translation(
|
||||||
|
val lang: String,
|
||||||
|
val main: String,
|
||||||
|
val sub: String,
|
||||||
|
val pron: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private val TEST_PROMPT = """
|
||||||
|
너는 여행 및 외국어 학습 전문가야. 해외 여행지나 일상적인 스몰토크에서 바로 사용할 수 있는 유용한 표현 5가지를 선정해줘.
|
||||||
|
각 표현에 대해 영어(EN), 스페인어(ES), 일본어(JA) 세 가지 버전으로 제공해야 해.
|
||||||
|
|
||||||
|
중요 규칙:
|
||||||
|
- 모든 외국어 문장의 'pron' 필드에는 한국어 사용자가 읽기 편하도록 최대한 정확한 한국어 발음을 적어줘.
|
||||||
|
- 일본어(JA)는 한자와 가나를 혼용하고, 'pron'에는 한글 발음을 적어줘.
|
||||||
|
- 주제(topic)는 '식당에서', '길 묻기', '자기소개', '날씨 이야기' 등 구체적으로 정해줘.
|
||||||
|
|
||||||
|
반드시 아래 JSON 배열 형식으로만 응답해:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"topic": "주제",
|
||||||
|
"translations": [
|
||||||
|
{"lang": "EN", "main": "Sentence", "sub": "한국어 뜻", "pron": "한글 발음"},
|
||||||
|
{"lang": "ES", "main": "Frase", "sub": "한국어 뜻", "pron": "한글 발음"},
|
||||||
|
{"lang": "JA", "main": "문장", "sub": "한국어 뜻", "pron": "한글 발음"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
// 2. 테스트 함수
|
||||||
|
private fun testGeminiApiCommunication() {
|
||||||
|
val apiKey = PrefString.geminiApiKey.get("").trim()
|
||||||
|
if (apiKey.isEmpty()) {
|
||||||
|
Blog.LOGE("테스트 실패: API 키가 입력되지 않았습니다.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// BottomSheetDialogFragment는 viewLifecycleOwner.lifecycleScope를 사용할 수 있습니다.
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Blog.LOGE("==== Gemini API 통신 테스트 시작 ====")
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // 서버 연결 대기 시간
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS) // 데이터 전달받는 시간 (중요!)
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS) // 데이터 전송 시간
|
||||||
|
.build()
|
||||||
|
val listUrl = "https://generativelanguage.googleapis.com/v1beta/models?key=$apiKey"
|
||||||
|
val listRequest = Request.Builder().url(listUrl).get().build()
|
||||||
|
val listResponse = client.newCall(listRequest).execute()
|
||||||
|
|
||||||
|
if (listResponse.isSuccessful) {
|
||||||
|
val listJson = listResponse.body?.string() ?: ""
|
||||||
|
val targetModelName = getBestModelName(listJson)
|
||||||
|
?: "models/gemini-1.5-flash" // 실패 시 기본값 fallback
|
||||||
|
|
||||||
|
Blog.LOGE("사용 결정된 모델명: $targetModelName")
|
||||||
|
val finalUrl = "https://generativelanguage.googleapis.com/v1beta/$targetModelName:generateContent?key=$apiKey"
|
||||||
|
|
||||||
|
Blog.LOGE("요청 URL: $finalUrl")
|
||||||
|
val requestMap = mapOf(
|
||||||
|
"contents" to listOf(
|
||||||
|
mapOf("parts" to listOf(mapOf("text" to TEST_PROMPT)))
|
||||||
|
),
|
||||||
|
"generationConfig" to mapOf(
|
||||||
|
"responseMimeType" to "application/json"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val jsonRequest = Gson().toJson(requestMap)
|
||||||
|
val body = jsonRequest.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
|
val request = Request.Builder().url(finalUrl).post(body).build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val rawBody = response.body?.string() ?: ""
|
||||||
|
Blog.LOGE("1. 통신 성공! Raw Response 길이: ${rawBody.length}")
|
||||||
|
Blog.LOGE("Raw Data: $rawBody")
|
||||||
|
|
||||||
|
// 파싱 테스트
|
||||||
|
val gson = Gson()
|
||||||
|
val fullResponse = gson.fromJson(rawBody, GeminiResponse::class.java)
|
||||||
|
val innerJson = fullResponse.candidates[0].content.parts[0].text
|
||||||
|
|
||||||
|
val groupType = object : TypeToken<List<WallContentGroup>>() {}.type
|
||||||
|
val parsedList: List<WallContentGroup> = gson.fromJson(innerJson, groupType)
|
||||||
|
|
||||||
|
Blog.LOGE("2. 파싱 성공! 파싱된 세트 개수: ${parsedList.size}")
|
||||||
|
parsedList.forEachIndexed { index, group ->
|
||||||
|
Blog.LOGE(" [$index] 주제: ${group.topic} | 대표 문장(EN): ${group.translations.find { it.lang == "EN" }?.main}")
|
||||||
|
}
|
||||||
|
Blog.LOGE("==== 테스트 완료 ====")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Blog.LOGE("통신 실패: HTTP ${response.code} / ${response.message}")
|
||||||
|
Blog.LOGE("에러 바디: ${response.body?.string()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 최종 URL 조립 (모델명에 이미 'models/'가 포함되어 있으므로 경로 주의)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Blog.LOGE("통신 중 Exception 발생", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBestModelName(jsonString: String): String? {
|
||||||
|
return try {
|
||||||
|
val gson = Gson()
|
||||||
|
Blog.LOGE("jsonString $jsonString")
|
||||||
|
val response = gson.fromJson(jsonString, GeminiModelListResponse::class.java)
|
||||||
|
Blog.LOGE("response $response")
|
||||||
|
// 1. 조건에 맞는 모델 필터링
|
||||||
|
// - 이름에 'gemini-1.5-flash'가 포함됨
|
||||||
|
// - 'generateContent' 기능을 지원함
|
||||||
|
val matchedModel = response.models.find { model ->
|
||||||
|
model.name.contains("flash") &&
|
||||||
|
model.supportedGenerationMethods.contains("generateContent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 찾은 모델의 name 반환 (없으면 null)
|
||||||
|
matchedModel?.name
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Blog.LOGE("모델 파싱 중 오류 발생", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GeminiModelListResponse(
|
||||||
|
val models: List<GeminiModelInfo>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 개별 모델의 정보를 담는 클래스
|
||||||
|
data class GeminiModelInfo(
|
||||||
|
val name: String, // 예: "models/gemini-1.5-flash"
|
||||||
|
val version: String, // 예: "002"
|
||||||
|
val displayName: String,
|
||||||
|
val description: String,
|
||||||
|
val inputTokenLimit: Int,
|
||||||
|
val outputTokenLimit: Int,
|
||||||
|
val supportedGenerationMethods: List<String> // 예: ["generateContent", "countTokens"]
|
||||||
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package bums.lunatic.launcher.wall
|
package bums.lunatic.launcher.wall
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.app.WallpaperManager
|
import android.app.WallpaperManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
@ -9,6 +10,7 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
|
import android.graphics.Typeface.BOLD
|
||||||
import android.os.BatteryManager
|
import android.os.BatteryManager
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
@ -16,8 +18,12 @@ import android.os.ParcelFileDescriptor
|
|||||||
import android.service.wallpaper.WallpaperService
|
import android.service.wallpaper.WallpaperService
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
|
import bums.lunatic.launcher.model.Translation
|
||||||
|
import bums.lunatic.launcher.model.WallContentGroup
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
import bums.lunatic.launcher.workers.LocationUpdateService
|
import bums.lunatic.launcher.workers.LocationUpdateService
|
||||||
|
import com.google.common.reflect.TypeToken
|
||||||
|
import com.google.gson.Gson
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -279,73 +285,107 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getRamUsage(): String {
|
||||||
|
val mi = ActivityManager.MemoryInfo()
|
||||||
|
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
activityManager.getMemoryInfo(mi)
|
||||||
|
val availableMegs = mi.availMem / 1048576L
|
||||||
|
val totalMegs = mi.totalMem / 1048576L
|
||||||
|
val usedMegs = totalMegs - availableMegs
|
||||||
|
val percent = (usedMegs.toDouble() / totalMegs.toDouble() * 100).toInt()
|
||||||
|
return "RAM: $usedMegs / ${totalMegs}MB ($percent%)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 배터리/기기 온도 가져오기 (Celsius)
|
||||||
|
private fun getDeviceTemperature(): String {
|
||||||
|
val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||||
|
val temp = intent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0
|
||||||
|
return "Temp: ${temp / 10.0}°C"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSystemTypeface(lang: String): Typeface {
|
||||||
|
return when (lang) {
|
||||||
|
"JA" -> Typeface.create("sans-serif", Typeface.BOLD) // 일본어 한자/가나 대응
|
||||||
|
else -> Typeface.create("sans-serif-medium", Typeface.NORMAL) // 영문/스페인어
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? {
|
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? {
|
||||||
val dateFull = SimpleDateFormat("yyyy.MM.dd", Locale.KOREAN).format(Date())
|
val dateFull = SimpleDateFormat("yyyy.MM.dd (E)", Locale.KOREAN).format(Date())
|
||||||
// val timeFull = SimpleDateFormat(if (isCharging) "HH:mm:ss" else "HH:mm", Locale.KOREAN).format(Date())
|
val ramInfo = getRamUsage()
|
||||||
|
val tempInfo = getDeviceTemperature()
|
||||||
|
val batteryInfo = getBatteryStatus().let { "${it.first}% ${if(it.second) "⚡" else ""}" }
|
||||||
|
|
||||||
val finalLines = mutableListOf<String>()
|
val finalLines = mutableListOf<String>()
|
||||||
if (weatherLines.isNotEmpty()) finalLines.add(weatherLines[0]) // Index 0: 날씨
|
|
||||||
|
|
||||||
// finalLines.add(timeFull) // Index 2: 시간
|
// 1. 날씨 정보 (최상단)
|
||||||
|
if (weatherLines.isNotEmpty()) finalLines.add(weatherLines[0])
|
||||||
|
|
||||||
|
// 2. 날짜 (중간 강조)
|
||||||
|
finalLines.add(dateFull)
|
||||||
|
|
||||||
|
// 3. 시스템 정보 (온도, 메모리, 배터리 통합 한 줄)
|
||||||
|
finalLines.add("$tempInfo | $ramInfo | $batteryInfo")
|
||||||
|
|
||||||
|
// 4. 주소 정보 (최하단 작게)
|
||||||
if (weatherLines.size > 1) {
|
if (weatherLines.size > 1) {
|
||||||
for (i in 1 until weatherLines.size - 1) finalLines.add(weatherLines[i])
|
finalLines.add(weatherLines.last())
|
||||||
finalLines.add(weatherLines.last()) // 마지막: 주소
|
|
||||||
}
|
}
|
||||||
finalLines.add(dateFull) // Index 1: 날짜
|
|
||||||
|
|
||||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
color = Color.WHITE
|
color = Color.WHITE
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
|
typeface = Typeface.create(Typeface.MONOSPACE, BOLD) // 가독성을 위해 BOLD
|
||||||
setShadowLayer(10f, 3f, 3f, Color.BLACK)
|
setShadowLayer(12f, 4f, 4f, Color.argb(180, 0, 0, 0)) // 가독성을 위한 그림자 강화
|
||||||
}
|
}
|
||||||
|
|
||||||
val titleSize = 80f
|
// 영역별 폰트 사이즈 설정
|
||||||
val dateSize = 50f
|
val titleSize = 75f // 날씨
|
||||||
val addressSize = 30f
|
val dateSize = 55f // 날짜
|
||||||
val lineSpacingMultiplier = 0.2f // 폰트 크기의 20%를 순수 여백으로 사용
|
val systemSize = 35f // 온도/메모리
|
||||||
|
val addressSize = 28f // 주소
|
||||||
|
val lineSpacingMultiplier = 0.3f
|
||||||
|
|
||||||
// --- 4. 크기 측정 및 라인별 높이 저장 ---
|
|
||||||
var maxWidth = 0f
|
var maxWidth = 0f
|
||||||
var totalHeight = 40f // 상단 여백
|
var totalHeight = 50f
|
||||||
val lineHeights = mutableListOf<Float>() // 각 줄이 차지하는 총 높이
|
val lineBaselines = mutableListOf<Float>()
|
||||||
val lineBaselines = mutableListOf<Float>() // 각 줄의 글자가 그려질 Baseline 위치
|
|
||||||
|
|
||||||
finalLines.forEachIndexed { index, line ->
|
finalLines.forEachIndexed { index, line ->
|
||||||
paint.textSize = when (index) {
|
paint.textSize = when (index) {
|
||||||
0 -> titleSize
|
0 -> titleSize
|
||||||
2 -> addressSize
|
1 -> dateSize
|
||||||
else -> dateSize
|
2 -> systemSize
|
||||||
|
else -> addressSize
|
||||||
}
|
}
|
||||||
|
|
||||||
val width = paint.measureText(line)
|
val width = paint.measureText(line)
|
||||||
if (width > maxWidth) maxWidth = width
|
if (width > maxWidth) maxWidth = width
|
||||||
|
|
||||||
val metrics = paint.fontMetrics
|
val metrics = paint.fontMetrics
|
||||||
val fontHeight = metrics.descent - metrics.ascent // 실제 글자 높이
|
val fontHeight = metrics.descent - metrics.ascent
|
||||||
val leading = paint.textSize * lineSpacingMultiplier // 동적 행간
|
val leading = paint.textSize * lineSpacingMultiplier
|
||||||
|
|
||||||
val fullLineHeight = fontHeight + leading
|
|
||||||
|
|
||||||
// 현재 줄의 Baseline 계산: 이전까지의 높이 + 글자의 ascent 절대값
|
|
||||||
lineBaselines.add(totalHeight - metrics.ascent)
|
lineBaselines.add(totalHeight - metrics.ascent)
|
||||||
totalHeight += fullLineHeight
|
totalHeight += (fontHeight + leading)
|
||||||
lineHeights.add(fullLineHeight)
|
|
||||||
}
|
}
|
||||||
totalHeight += 20f // 하단 여백
|
totalHeight += 30f
|
||||||
|
|
||||||
// --- 5. 비트맵 생성 및 그리기 ---
|
val bitmap = Bitmap.createBitmap((maxWidth + 120).toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888)
|
||||||
val bitmap = Bitmap.createBitmap((maxWidth + 100).toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888)
|
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
val centerX = bitmap.width / 2f
|
val centerX = bitmap.width / 2f
|
||||||
|
|
||||||
finalLines.forEachIndexed { index, line ->
|
finalLines.forEachIndexed { index, line ->
|
||||||
paint.textSize = when (index) {
|
paint.textSize = when (index) {
|
||||||
0 -> titleSize
|
0 -> titleSize
|
||||||
2 -> addressSize
|
1 -> dateSize
|
||||||
else -> dateSize
|
2 -> systemSize
|
||||||
|
else -> addressSize
|
||||||
}
|
}
|
||||||
// 미리 계산된 Baseline에 그리기만 하면 끝!
|
// 시스템 정보 줄(index 2)은 약간 불투명하게 처리하여 대비를 줄 수도 있습니다.
|
||||||
|
paint.alpha = if (index == 2) 200 else 255
|
||||||
|
|
||||||
canvas.drawText(line, centerX, lineBaselines[index], paint)
|
canvas.drawText(line, centerX, lineBaselines[index], paint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -165,7 +165,7 @@ class TorrentService : Service() {
|
|||||||
|
|
||||||
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
|
// 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬
|
||||||
if (isCharging) {
|
if (isCharging) {
|
||||||
val maxSlots = if (isWifiConnected) 5 else 1
|
val maxSlots = if (isWifiConnected) 8 else 1
|
||||||
val sortedByPriority = torrentsWithMetadata.sortedBy { it.second }
|
val sortedByPriority = torrentsWithMetadata.sortedBy { it.second }
|
||||||
|
|
||||||
sortedByPriority.forEachIndexed { index, pair ->
|
sortedByPriority.forEachIndexed { index, pair ->
|
||||||
|
|||||||
@ -34,6 +34,9 @@ import bums.lunatic.launcher.home.tokiz.ContentsPageInfo
|
|||||||
import bums.lunatic.launcher.home.tokiz.HistoryItem
|
import bums.lunatic.launcher.home.tokiz.HistoryItem
|
||||||
import bums.lunatic.launcher.home.tokiz.LastInfo
|
import bums.lunatic.launcher.home.tokiz.LastInfo
|
||||||
import bums.lunatic.launcher.home.tokiz.ReaderConfig
|
import bums.lunatic.launcher.home.tokiz.ReaderConfig
|
||||||
|
import bums.lunatic.launcher.model.ExpressionItem
|
||||||
|
import bums.lunatic.launcher.model.ExpressionWord
|
||||||
|
import bums.lunatic.launcher.model.WallContentGroup
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
import bums.lunatic.launcher.utils.JamoUtils
|
import bums.lunatic.launcher.utils.JamoUtils
|
||||||
import bums.lunatic.launcher.utils.afterDay
|
import bums.lunatic.launcher.utils.afterDay
|
||||||
@ -76,10 +79,42 @@ object WorkersDb {
|
|||||||
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
|
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
|
||||||
AppUsageLog::class,
|
AppUsageLog::class,
|
||||||
WidgetData::class,
|
WidgetData::class,
|
||||||
|
ExpressionItem::class,ExpressionWord::class
|
||||||
)
|
)
|
||||||
//,UserActionModel::class
|
//,UserActionModel::class
|
||||||
|
|
||||||
|
|
||||||
|
fun insertExpressions(groups: List<WallContentGroup>) {
|
||||||
|
getRealm().writeBlocking {
|
||||||
|
groups.forEach { group ->
|
||||||
|
group.translations.forEach { trans ->
|
||||||
|
|
||||||
|
// 1. 단어들을 RealmObject 리스트로 변환
|
||||||
|
val realmWords = trans.words.map { parsedWord ->
|
||||||
|
ExpressionWord().apply {
|
||||||
|
word = parsedWord.word
|
||||||
|
meaning = parsedWord.meaning
|
||||||
|
pronunciation = parsedWord.pron
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ExpressionItem 생성 및 저장
|
||||||
|
copyToRealm(ExpressionItem().apply {
|
||||||
|
topic = group.topic
|
||||||
|
lang = trans.lang
|
||||||
|
mainText = trans.main
|
||||||
|
subText = trans.sub
|
||||||
|
pronunciation = trans.pron
|
||||||
|
|
||||||
|
// 💡 변환한 단어 리스트 넣기
|
||||||
|
words.addAll(realmWords)
|
||||||
|
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// [추가] 앱/연락처 사용 시 로그 저장 (기존 updateAppUse 대신 이거 호출)
|
// [추가] 앱/연락처 사용 시 로그 저장 (기존 updateAppUse 대신 이거 호출)
|
||||||
fun logAppUsage(key: String, type: UsageLogType = UsageLogType.APP, datetime: UsageUpdateType) {
|
fun logAppUsage(key: String, type: UsageLogType = UsageLogType.APP, datetime: UsageUpdateType) {
|
||||||
|
|||||||
38
app/src/main/res/layout/fragment_learning.xml
Normal file
38
app/src/main/res/layout/fragment_learning.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="#DD121212"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scrollbars="none">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_languages"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingBottom="8dp" />
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scrollbars="none">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_topics"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingBottom="16dp" />
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_expressions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="80dp" /> </LinearLayout>
|
||||||
@ -126,6 +126,12 @@
|
|||||||
<!-- style="@style/CommonFabStyle"-->
|
<!-- style="@style/CommonFabStyle"-->
|
||||||
<!-- android:id="@+id/btn_img4"-->
|
<!-- android:id="@+id/btn_img4"-->
|
||||||
<!-- />-->
|
<!-- />-->
|
||||||
|
|
||||||
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
|
app:fab_label="🌍"
|
||||||
|
style="@style/CommonFabStyle"
|
||||||
|
android:id="@+id/btn_learn" />
|
||||||
|
|
||||||
<bums.lunatic.launcher.view.FloatingActionButton
|
<bums.lunatic.launcher.view.FloatingActionButton
|
||||||
style="@style/CommonFabStyle"
|
style="@style/CommonFabStyle"
|
||||||
app:fab_label="❌"
|
app:fab_label="❌"
|
||||||
|
|||||||
@ -134,6 +134,27 @@
|
|||||||
android:singleLine="true" />
|
android:singleLine="true" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/geminiInputLayout"
|
||||||
|
android:layout_width="@dimen/threeTwentyFour"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/twelve"
|
||||||
|
android:hint="@string/owm_key"
|
||||||
|
app:endIconMode="clear_text"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/owmInputLayout">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/inputgemini"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:singleLine="true" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<!-- <com.google.android.material.textview.MaterialTextView-->
|
<!-- <com.google.android.material.textview.MaterialTextView-->
|
||||||
<!-- android:id="@+id/doubleTapLock"-->
|
<!-- android:id="@+id/doubleTapLock"-->
|
||||||
<!-- android:layout_width="wrap_content"-->
|
<!-- android:layout_width="wrap_content"-->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user