fix: 키워드 카드 디자인 단순화 및 APK 다이렉트 다운로드 구현
- 키워드 카드 배경 단일화 (2중 배경 제거) - '내 키워드' 배지 디자인 단순화 - 알림 설정 UI 간소화 - APK 다이렉트 다운로드 기능 추가 - FileProvider 설정 추가 - version.json updateUrl을 실제 APK 다운로드 링크로 변경
This commit is contained in:
@@ -75,6 +75,17 @@
|
|||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/widget_info" />
|
android:resource="@xml/widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
</application>
|
|
||||||
|
<!-- FileProvider for APK installation -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -160,26 +160,26 @@ fun DealItem(
|
|||||||
// 키워드 매칭 배지
|
// 키워드 매칭 배지
|
||||||
if (deal.isKeywordMatch) {
|
if (deal.isKeywordMatch) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.height(28.dp)
|
modifier = Modifier.height(24.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.padding(horizontal = 10.dp)
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Star,
|
imageVector = Icons.Filled.Star,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(12.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "내 키워드",
|
text = "내 키워드",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
color = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
package com.hotdeal.alarm.presentation.main
|
package com.hotdeal.alarm.presentation.main
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.hotdeal.alarm.ui.theme.HotDealTheme
|
import com.hotdeal.alarm.ui.theme.HotDealTheme
|
||||||
|
import com.hotdeal.alarm.util.ApkDownloadManager
|
||||||
import com.hotdeal.alarm.util.UpdateInfo
|
import com.hotdeal.alarm.util.UpdateInfo
|
||||||
import com.hotdeal.alarm.util.VersionManager
|
import com.hotdeal.alarm.util.VersionManager
|
||||||
import com.hotdeal.alarm.worker.WorkerScheduler
|
import com.hotdeal.alarm.worker.WorkerScheduler
|
||||||
@@ -44,7 +31,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
// 2분 간격으로 폴<EFBFBD> 시작
|
// 2분 간격으로 폴링 시작
|
||||||
workerScheduler.schedulePeriodicPolling(2)
|
workerScheduler.schedulePeriodicPolling(2)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
@@ -58,6 +45,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
// 업데이트 체크
|
// 업데이트 체크
|
||||||
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
|
var updateInfo by remember { mutableStateOf<UpdateInfo?>(null) }
|
||||||
var showUpdateDialog by remember { mutableStateOf(false) }
|
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||||
|
var downloadProgress by remember { mutableStateOf(0) }
|
||||||
|
var isDownloading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
checkForUpdate { info ->
|
checkForUpdate { info ->
|
||||||
@@ -72,10 +61,40 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (showUpdateDialog && updateInfo != null) {
|
if (showUpdateDialog && updateInfo != null) {
|
||||||
UpdateDialog(
|
UpdateDialog(
|
||||||
updateInfo = updateInfo!!,
|
updateInfo = updateInfo!!,
|
||||||
onDismiss = { showUpdateDialog = false },
|
isDownloading = isDownloading,
|
||||||
|
downloadProgress = downloadProgress,
|
||||||
|
onDismiss = {
|
||||||
|
if (!isDownloading) showUpdateDialog = false
|
||||||
|
},
|
||||||
onUpdate = {
|
onUpdate = {
|
||||||
openUpdateUrl(updateInfo!!.updateUrl)
|
// APK 다이렉트 다운로드
|
||||||
|
isDownloading = true
|
||||||
|
val downloadId = ApkDownloadManager.downloadApk(
|
||||||
|
this@MainActivity,
|
||||||
|
updateInfo!!
|
||||||
|
)
|
||||||
|
|
||||||
|
// 다운로드 진행률 모니터링
|
||||||
|
lifecycleScope.launch {
|
||||||
|
while (isDownloading) {
|
||||||
|
downloadProgress = ApkDownloadManager.getDownloadProgress(
|
||||||
|
this@MainActivity,
|
||||||
|
downloadId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ApkDownloadManager.isDownloadComplete(
|
||||||
|
this@MainActivity,
|
||||||
|
downloadId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
isDownloading = false
|
||||||
showUpdateDialog = false
|
showUpdateDialog = false
|
||||||
|
ApkDownloadManager.installApk(this@MainActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -109,18 +128,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 업데이트 URL 열기
|
|
||||||
*/
|
|
||||||
private fun openUpdateUrl(url: String) {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, "브라우저를 열 수 없습니다", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,6 +136,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun UpdateDialog(
|
fun UpdateDialog(
|
||||||
updateInfo: UpdateInfo,
|
updateInfo: UpdateInfo,
|
||||||
|
isDownloading: Boolean,
|
||||||
|
downloadProgress: Int,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onUpdate: () -> Unit
|
onUpdate: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -136,9 +145,9 @@ fun UpdateDialog(
|
|||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("업데이트 가능") },
|
title = { Text("업데이트 가능") },
|
||||||
text = {
|
text = {
|
||||||
androidx.compose.foundation.layout.Column {
|
Column {
|
||||||
Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.")
|
Text("새로운 버전 ${updateInfo.version}이(가) 출시되었습니다.")
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
"변경사항:",
|
"변경사항:",
|
||||||
style = MaterialTheme.typography.labelMedium
|
style = MaterialTheme.typography.labelMedium
|
||||||
@@ -146,17 +155,35 @@ fun UpdateDialog(
|
|||||||
updateInfo.changelog.take(3).forEach { change ->
|
updateInfo.changelog.take(3).forEach { change ->
|
||||||
Text("• $change", style = MaterialTheme.typography.bodySmall)
|
Text("• $change", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDownloading) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = downloadProgress / 100f,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"다운로드 중... $downloadProgress%",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(onClick = onUpdate) {
|
Button(
|
||||||
Text("업데이트")
|
onClick = onUpdate,
|
||||||
|
enabled = !isDownloading
|
||||||
|
) {
|
||||||
|
Text(if (isDownloading) "다운로드 중..." else "업데이트")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
if (!isDownloading) {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("나중에")
|
Text("나중에")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,14 +212,14 @@ private fun NotificationSettingsHeader(
|
|||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(20.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
// 헤더
|
// 헤더
|
||||||
Row(
|
Row(
|
||||||
@@ -228,7 +228,7 @@ private fun NotificationSettingsHeader(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(44.dp)
|
.size(40.dp)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.primaryContainer,
|
MaterialTheme.colorScheme.primaryContainer,
|
||||||
CircleShape
|
CircleShape
|
||||||
@@ -239,14 +239,14 @@ private fun NotificationSettingsHeader(
|
|||||||
imageVector = Icons.Filled.Notifications,
|
imageVector = Icons.Filled.Notifications,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = "알림 설정",
|
text = "알림 설정",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -259,19 +259,15 @@ private fun NotificationSettingsHeader(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 알림 권한 상태 카드
|
// 알림 권한 상태
|
||||||
NotificationStatusCard(
|
NotificationStatusRow(
|
||||||
icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning,
|
icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning,
|
||||||
title = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요",
|
title = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요",
|
||||||
description = if (permissionStatus.hasNotificationPermission)
|
|
||||||
"키워드 매칭 시 즉시 알림을 받을 수 있습니다"
|
|
||||||
else
|
|
||||||
"알림을 받으려면 권한을 허용해주세요",
|
|
||||||
isOk = permissionStatus.hasNotificationPermission,
|
isOk = permissionStatus.hasNotificationPermission,
|
||||||
action = if (!permissionStatus.hasNotificationPermission) {
|
action = if (!permissionStatus.hasNotificationPermission) {
|
||||||
{ onRequestPermission() }
|
{ onRequestPermission() }
|
||||||
} else null,
|
} else null,
|
||||||
actionLabel = "권한 허용하기",
|
actionLabel = "권한 허용",
|
||||||
secondaryAction = if (permissionStatus.hasNotificationPermission) {
|
secondaryAction = if (permissionStatus.hasNotificationPermission) {
|
||||||
{ onOpenSystemSettings() }
|
{ onOpenSystemSettings() }
|
||||||
} else null,
|
} else null,
|
||||||
@@ -281,103 +277,49 @@ private fun NotificationSettingsHeader(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
// 사이트 선택 상태
|
// 사이트 선택 상태
|
||||||
NotificationStatusCard(
|
NotificationStatusRow(
|
||||||
icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
||||||
title = if (hasEnabledSites) "사이트 선택 완료" else "사이트 선택 필요",
|
title = if (hasEnabledSites) "사이트 선택 완료" else "사이트 선택 필요",
|
||||||
description = if (hasEnabledSites)
|
|
||||||
"모니터링할 사이트가 선택되었습니다"
|
|
||||||
else
|
|
||||||
"최소 1개 이상의 사이트를 선택해주세요",
|
|
||||||
isOk = hasEnabledSites
|
isOk = hasEnabledSites
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotificationStatusCard(
|
private fun NotificationStatusRow(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
|
||||||
isOk: Boolean,
|
isOk: Boolean,
|
||||||
action: (() -> Unit)? = null,
|
action: (() -> Unit)? = null,
|
||||||
actionLabel: String = "",
|
actionLabel: String = "",
|
||||||
secondaryAction: (() -> Unit)? = null,
|
secondaryAction: (() -> Unit)? = null,
|
||||||
secondaryActionLabel: String = ""
|
secondaryActionLabel: String = ""
|
||||||
) {
|
) {
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
color = if (isOk)
|
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action != null || secondaryAction != null) {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
Button(
|
TextButton(onClick = action) {
|
||||||
onClick = action,
|
Text(actionLabel, style = MaterialTheme.typography.labelMedium)
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Notifications,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text(actionLabel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (secondaryAction != null) {
|
if (secondaryAction != null) {
|
||||||
OutlinedButton(
|
TextButton(onClick = secondaryAction) {
|
||||||
onClick = secondaryAction,
|
Text(secondaryActionLabel, style = MaterialTheme.typography.labelMedium)
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Settings,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text(secondaryActionLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -748,26 +690,13 @@ private fun EnhancedKeywordCard(
|
|||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (keyword.isEnabled) 1f else 0.98f,
|
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
|
||||||
label = "keyword_scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
shape = RoundedCornerShape(12.dp),
|
||||||
.scale(scale),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (keyword.isEnabled)
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||||
defaultElevation = if (keyword.isEnabled) 2.dp else 0.dp
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -777,25 +706,21 @@ private fun EnhancedKeywordCard(
|
|||||||
) {
|
) {
|
||||||
// 키워드 아이콘
|
// 키워드 아이콘
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.size(40.dp)
|
||||||
.size(44.dp)
|
|
||||||
.background(
|
.background(
|
||||||
if (keyword.isEnabled)
|
if (keyword.isEnabled)
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||||
CircleShape
|
CircleShape
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (keyword.isEnabled) Icons.Filled.Tag else Icons.Outlined.Tag,
|
imageVector = Icons.Filled.Tag,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (keyword.isEnabled)
|
tint = Color.White,
|
||||||
MaterialTheme.colorScheme.onPrimary
|
modifier = Modifier.size(20.dp)
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.outline,
|
|
||||||
modifier = Modifier.size(22.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,14 +731,11 @@ private fun EnhancedKeywordCard(
|
|||||||
Text(
|
Text(
|
||||||
text = keyword.keyword,
|
text = keyword.keyword,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.Medium,
|
||||||
color = if (keyword.isEnabled)
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (keyword.isEnabled) "알림 활성화됨" else "알림 비활성화",
|
text = if (keyword.isEnabled) "알림 활성화" else "알림 비활성화",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = if (keyword.isEnabled)
|
color = if (keyword.isEnabled)
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
@@ -832,7 +754,7 @@ private fun EnhancedKeywordCard(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
// 삭제 버튼
|
// 삭제 버튼
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
124
app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt
Normal file
124
app/src/main/java/com/hotdeal/alarm/util/ApkDownloadManager.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package com.hotdeal.alarm.util
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK 다운로드 및 설치 관리자
|
||||||
|
*/
|
||||||
|
object ApkDownloadManager {
|
||||||
|
|
||||||
|
private const val APK_FILE_NAME = "hotdeal-alarm-update.apk"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK 다운로드 시작
|
||||||
|
*/
|
||||||
|
fun downloadApk(context: Context, updateInfo: UpdateInfo): Long {
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
|
// 기존 파일 삭제
|
||||||
|
val outputFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), APK_FILE_NAME)
|
||||||
|
if (outputFile.exists()) {
|
||||||
|
outputFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = DownloadManager.Request(Uri.parse(updateInfo.updateUrl)).apply {
|
||||||
|
setTitle("핫딜 알람 업데이트")
|
||||||
|
setDescription("버전 ${updateInfo.version} 다운로드 중...")
|
||||||
|
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, APK_FILE_NAME)
|
||||||
|
setAllowedOverMetered(true)
|
||||||
|
setAllowedOverRoaming(true)
|
||||||
|
setMimeType("application/vnd.android.package-archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadId = downloadManager.enqueue(request)
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"업데이트 다운로드 시작...",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
|
||||||
|
return downloadId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK 파일 설치
|
||||||
|
*/
|
||||||
|
fun installApk(context: Context) {
|
||||||
|
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), APK_FILE_NAME)
|
||||||
|
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
Toast.makeText(context, "APK 파일을 찾을 수 없습니다", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val apkUri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
apkFile
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "설치를 시작할 수 없습니다: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 다운로드 완료 여부 확인
|
||||||
|
*/
|
||||||
|
fun isDownloadComplete(context: Context, downloadId: Long): Boolean {
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
|
||||||
|
val cursor = downloadManager.query(query)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||||
|
val status = cursor.getInt(statusIndex)
|
||||||
|
cursor.close()
|
||||||
|
return status == DownloadManager.STATUS_SUCCESSFUL
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 다운로드 진행률 가져오기
|
||||||
|
*/
|
||||||
|
fun getDownloadProgress(context: Context, downloadId: Long): Int {
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
|
||||||
|
val cursor = downloadManager.query(query)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val bytesDownloadedIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||||
|
val bytesTotalIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||||
|
|
||||||
|
val bytesDownloaded = cursor.getLong(bytesDownloadedIndex)
|
||||||
|
val bytesTotal = cursor.getLong(bytesTotalIndex)
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if (bytesTotal > 0) {
|
||||||
|
return ((bytesDownloaded * 100) / bytesTotal).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/main/res/xml/file_paths.xml
Normal file
12
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<external-files-path
|
||||||
|
name="downloads"
|
||||||
|
path="Download" />
|
||||||
|
<cache-path
|
||||||
|
name="cache"
|
||||||
|
path="." />
|
||||||
|
<files-path
|
||||||
|
name="files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"minSdk": 31,
|
"minSdk": 31,
|
||||||
"targetSdk": 35,
|
"targetSdk": 35,
|
||||||
"forceUpdate": false,
|
"forceUpdate": false,
|
||||||
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases",
|
"updateUrl": "https://git.webpluss.net/attachments/02ab65b3-22f3-4f8e-b422-6c44588764b0",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"아이콘 디자인 개선 (알림 벨 + 불꽃)",
|
"아이콘 디자인 개선 (알림 벨 + 불꽃)",
|
||||||
"메인 화면 헤더 제거로 화면 넓게 사용",
|
"메인 화면 헤더 제거로 화면 넓게 사용",
|
||||||
|
|||||||
Reference in New Issue
Block a user