package com.rizoma.orbita.ui import android.app.Application import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.Uri import android.provider.Settings import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.rizoma.orbita.data.AppRepository import com.rizoma.orbita.data.SkyRepository import com.rizoma.orbita.data.UsageStore import com.rizoma.orbita.data.Weather import com.rizoma.orbita.model.AppNode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.util.Calendar class OrbitaViewModel(app: Application) : AndroidViewModel(app) { private val repo = AppRepository(app) private val usage = UsageStore(app) private val rawApps = MutableStateFlow>(emptyList()) /** Nodos ya ponderados y ordenados: fijados primero, luego por calor, luego alfabético. */ val nodes: StateFlow> = combine( rawApps, usage.counts(), usage.pinned(), usage.penalties(), usage.hidden(), ) { apps, counts, pinned, penalties, hidden -> apps.map { node -> node.copy( score = (counts[node.key] ?: 0).toFloat(), pinned = node.key in pinned, hidden = node.key in hidden, ) }.sortedWith( // El orden define la órbita: fijados al centro, luego por calor efectivo // (calor menos penalización por "alejar"), luego alfabético. compareByDescending { it.pinned } .thenByDescending { it.score - (penalties[it.key] ?: 0).toFloat() } .thenBy { it.label.lowercase() } ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) // --- Cielo del fondo: hora del día + clima local. --- private val weather = MutableStateFlow(null) /** Marca la hora actual (con fracción) y se reemite cada minuto. */ private val clock = flow { while (true) { emit(currentHourFraction()) delay(60_000L) } } val sky: StateFlow = combine(clock, weather) { hour, w -> skyPaletteFor(hour, w) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), skyPaletteFor(currentHourFraction(), null), ) /** Refresca el clima local (best-effort). Llamar tras conceder ubicación. */ fun refreshWeather() { viewModelScope.launch(Dispatchers.IO) { SkyRepository(getApplication()).fetch()?.let { weather.value = it } } } private fun currentHourFraction(): Float { val c = Calendar.getInstance() return c.get(Calendar.HOUR_OF_DAY) + c.get(Calendar.MINUTE) / 60f } private val pkgReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) = refresh() } init { refresh() val filter = IntentFilter().apply { addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_REMOVED) addAction(Intent.ACTION_PACKAGE_CHANGED) addDataScheme("package") } getApplication().registerReceiver(pkgReceiver, filter) } fun refresh() { viewModelScope.launch(Dispatchers.IO) { rawApps.value = repo.loadApps() } } fun launch(node: AppNode) { val intent = Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) .setComponent(ComponentName(node.packageName, node.activityName)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) runCatching { getApplication().startActivity(intent) } viewModelScope.launch { usage.record(node.key) } } fun togglePin(node: AppNode) { viewModelScope.launch { usage.setPinned(node.key, !node.pinned) } } /** Retira el nodo del átomo (o lo devuelve). No desinstala: la app sigue ahí. */ fun toggleHidden(node: AppNode) { viewModelScope.launch { usage.setHidden(node.key, !node.hidden) } } /** Aleja el nodo a órbitas externas (usa su calor actual como referencia). */ fun pushAway(node: AppNode) { viewModelScope.launch { usage.pushAway(node.key, node.score.toInt()) } } fun openInfo(node: AppNode) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", node.packageName, null)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) runCatching { getApplication().startActivity(intent) } } fun uninstall(node: AppNode) { @Suppress("DEPRECATION") val intent = Intent(Intent.ACTION_DELETE) .setData(Uri.fromParts("package", node.packageName, null)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) runCatching { getApplication().startActivity(intent) } } override fun onCleared() { runCatching { getApplication().unregisterReceiver(pkgReceiver) } } }