v0.2.8: Site & Keyword Reordering Support & Slim Bottom Bar Margins
This commit is contained in:
@@ -5,15 +5,15 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.hotdeal.alarm.data.local.db.dao.HotDealDao
|
import com.hotdeal.alarm.data.local.db.dao.HotDealDao
|
||||||
import com.hotdeal.alarm.data.local.db.dao.KeywordDao
|
import com.hotdeal.alarm.data.local.db.dao.KeywordDao
|
||||||
import com.hotdeal.alarm.data.local.db.dao.SiteConfigDao
|
import com.hotdeal.alarm.data.local.db.dao.SiteConfigDao
|
||||||
|
import com.hotdeal.alarm.data.local.db.entity.KeywordEntity
|
||||||
import com.hotdeal.alarm.data.local.db.entity.SiteConfigEntity
|
import com.hotdeal.alarm.data.local.db.entity.SiteConfigEntity
|
||||||
import com.hotdeal.alarm.data.local.preferences.AppSettings
|
import com.hotdeal.alarm.data.local.preferences.AppSettings
|
||||||
|
import com.hotdeal.alarm.domain.model.Keyword
|
||||||
import com.hotdeal.alarm.domain.model.SiteType
|
import com.hotdeal.alarm.domain.model.SiteType
|
||||||
import com.hotdeal.alarm.worker.WorkerScheduler
|
import com.hotdeal.alarm.worker.WorkerScheduler
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -24,28 +24,34 @@ class MainViewModel @Inject constructor(
|
|||||||
private val workerScheduler: WorkerScheduler,
|
private val workerScheduler: WorkerScheduler,
|
||||||
private val appSettings: AppSettings
|
private val appSettings: AppSettings
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<MainUiState>(MainUiState.Loading)
|
private val _uiState = MutableStateFlow<MainUiState>(MainUiState.Loading)
|
||||||
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
// 폴링 주기 (저장된 값 즉시 반영)
|
// 폴링 주기
|
||||||
val pollingInterval: StateFlow<Int> = appSettings.pollingInterval
|
val pollingInterval: StateFlow<Int> = appSettings.pollingInterval
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, 2)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, 2)
|
||||||
|
|
||||||
|
// 사이트 표시 순서
|
||||||
|
val siteOrder: StateFlow<List<SiteType>> = appSettings.siteOrder
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, SiteType.entries)
|
||||||
|
|
||||||
|
private val _toastEvent = MutableSharedFlow<String>()
|
||||||
|
val toastEvent: SharedFlow<String> = _toastEvent.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initializeApp()
|
initializeApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeApp() {
|
private fun initializeApp() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
initializeDefaultSiteConfigs()
|
initializeDefaultSiteConfigs()
|
||||||
loadState()
|
loadState()
|
||||||
// 저장된 폴� 주기로 시작
|
|
||||||
val savedInterval = appSettings.pollingInterval.first()
|
val savedInterval = appSettings.pollingInterval.first()
|
||||||
startPolling(savedInterval.toLong())
|
startPolling(savedInterval.toLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initializeDefaultSiteConfigs() {
|
private suspend fun initializeDefaultSiteConfigs() {
|
||||||
val existingConfigs = siteConfigDao.getAllConfigs()
|
val existingConfigs = siteConfigDao.getAllConfigs()
|
||||||
val existingKeys = existingConfigs.map { it.siteBoardKey }.toSet()
|
val existingKeys = existingConfigs.map { it.siteBoardKey }.toSet()
|
||||||
@@ -67,17 +73,27 @@ class MainViewModel @Inject constructor(
|
|||||||
siteConfigDao.insertConfigs(missingConfigs)
|
siteConfigDao.insertConfigs(missingConfigs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadState() {
|
private suspend fun loadState() {
|
||||||
combine(
|
combine(
|
||||||
hotDealDao.observeAllDeals(),
|
hotDealDao.observeAllDeals(),
|
||||||
siteConfigDao.observeAllConfigs(),
|
siteConfigDao.observeAllConfigs(),
|
||||||
keywordDao.observeAllKeywords()
|
keywordDao.observeAllKeywords(),
|
||||||
) { deals, configs, keywords ->
|
appSettings.keywordOrder
|
||||||
|
) { deals, configs, keywords, keywordOrder ->
|
||||||
|
// 사용자가 설정한 키워드 순서에 따라 정렬
|
||||||
|
val domainKeywords = keywords.map { it.toDomain() }
|
||||||
|
val sortedKeywords = if (keywordOrder.isEmpty()) {
|
||||||
|
domainKeywords
|
||||||
|
} else {
|
||||||
|
val orderMap = keywordOrder.withIndex().associate { it.value to it.index }
|
||||||
|
domainKeywords.sortedBy { orderMap[it.id] ?: Int.MAX_VALUE }
|
||||||
|
}
|
||||||
|
|
||||||
MainUiState.Success(
|
MainUiState.Success(
|
||||||
deals = deals.map { it.toDomain() },
|
deals = deals.map { it.toDomain() },
|
||||||
siteConfigs = configs.map { it.toDomain() },
|
siteConfigs = configs.map { it.toDomain() },
|
||||||
keywords = keywords.map { it.toDomain() }
|
keywords = sortedKeywords
|
||||||
)
|
)
|
||||||
}.catch { e ->
|
}.catch { e ->
|
||||||
_uiState.value = MainUiState.Error(e.message ?: "Unknown error")
|
_uiState.value = MainUiState.Error(e.message ?: "Unknown error")
|
||||||
@@ -85,87 +101,108 @@ class MainViewModel @Inject constructor(
|
|||||||
_uiState.value = state
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) {
|
fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
siteConfigDao.updateEnabled(siteBoardKey, enabled)
|
siteConfigDao.updateEnabled(siteBoardKey, enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reorderSites(fromIndex: Int, toIndex: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val current = siteOrder.value.toMutableList()
|
||||||
|
if (fromIndex in current.indices && toIndex in current.indices) {
|
||||||
|
val item = current.removeAt(fromIndex)
|
||||||
|
current.add(toIndex, item)
|
||||||
|
appSettings.setSiteOrder(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addKeyword(keyword: String) {
|
fun addKeyword(keyword: String) {
|
||||||
if (keyword.isBlank()) return
|
if (keyword.isBlank()) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
keywordDao.insertKeyword(
|
val newId = keywordDao.insertKeyword(
|
||||||
com.hotdeal.alarm.data.local.db.entity.KeywordEntity(
|
KeywordEntity(
|
||||||
keyword = keyword.trim(),
|
keyword = keyword.trim(),
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// 키워드 순서 맨 앞에 추가
|
||||||
|
val currentOrder = appSettings.keywordOrder.first().toMutableList()
|
||||||
|
currentOrder.add(0, newId)
|
||||||
|
appSettings.setKeywordOrder(currentOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeyword(id: Long) {
|
fun deleteKeyword(id: Long) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
keywordDao.deleteKeywordById(id)
|
keywordDao.deleteKeywordById(id)
|
||||||
|
val currentOrder = appSettings.keywordOrder.first().toMutableList()
|
||||||
|
currentOrder.remove(id)
|
||||||
|
appSettings.setKeywordOrder(currentOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleKeyword(id: Long, enabled: Boolean) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
keywordDao.updateEnabled(id, enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
fun toggleKeyword(id: Long, enabled: Boolean) {
|
||||||
* 즐겨찾기 토글
|
viewModelScope.launch {
|
||||||
*/
|
keywordDao.updateEnabled(id, enabled)
|
||||||
fun toggleFavorite(dealId: String) {
|
}
|
||||||
viewModelScope.launch {
|
}
|
||||||
hotDealDao.toggleFavorite(dealId)
|
|
||||||
}
|
fun reorderKeywords(fromIndex: Int, toIndex: Int) {
|
||||||
}
|
val state = uiState.value as? MainUiState.Success ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
val current = state.keywords.toMutableList()
|
||||||
|
if (fromIndex in current.indices && toIndex in current.indices) {
|
||||||
|
val item = current.removeAt(fromIndex)
|
||||||
|
current.add(toIndex, item)
|
||||||
|
appSettings.setKeywordOrder(current.map { it.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleFavorite(dealId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
hotDealDao.toggleFavorite(dealId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFavorite(dealId: String, isFavorite: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
hotDealDao.setFavorite(dealId, isFavorite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 즐겨찾기 설정
|
|
||||||
*/
|
|
||||||
fun setFavorite(dealId: String, isFavorite: Boolean) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
hotDealDao.setFavorite(dealId, isFavorite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
workerScheduler.executeOnce()
|
workerScheduler.executeOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 폴� 시작 (주기 저장)
|
|
||||||
*/
|
|
||||||
fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) {
|
fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appSettings.setPollingInterval(intervalMinutes.toInt())
|
appSettings.setPollingInterval(intervalMinutes.toInt())
|
||||||
workerScheduler.schedulePeriodicPolling(intervalMinutes)
|
workerScheduler.schedulePeriodicPolling(intervalMinutes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopPolling() {
|
fun stopPolling() {
|
||||||
workerScheduler.cancelPolling()
|
workerScheduler.cancelPolling()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 파싱 핫딜 데이터 전체 삭제 및 사용자 피드백 트리거
|
|
||||||
private val _toastEvent = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
|
||||||
val toastEvent = _toastEvent.asSharedFlow()
|
|
||||||
|
|
||||||
fun deleteAllParsedData() {
|
fun deleteAllParsedData() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
hotDealDao.deleteAllDeals()
|
try {
|
||||||
_toastEvent.emit("파싱 데이터가 삭제되었습니다")
|
hotDealDao.deleteAllDeals()
|
||||||
|
_toastEvent.emit("모든 수집 데이터가 삭제되었습니다")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_toastEvent.emit("데이터 삭제 중 오류가 발생했습니다: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class MainUiState {
|
sealed class MainUiState {
|
||||||
data object Loading : MainUiState()
|
object Loading : MainUiState()
|
||||||
data class Success(
|
data class Success(
|
||||||
val deals: List<com.hotdeal.alarm.domain.model.HotDeal>,
|
val deals: List<com.hotdeal.alarm.domain.model.HotDeal>,
|
||||||
val siteConfigs: List<com.hotdeal.alarm.domain.model.SiteConfig>,
|
val siteConfigs: List<com.hotdeal.alarm.domain.model.SiteConfig>,
|
||||||
|
|||||||
Reference in New Issue
Block a user