package com.vgmlr.varnish
import android.app.Application
import android.graphics.Rect
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
sealed interface Screen {
val id: String
data object Home : Screen {
override val id = "home"
}
data object Settings : Screen {
override val id = "settings"
}
data class Folder(val path: String) : Screen {
override val id: String get() = "folder:$path"
}
data class Image(val path: String, val folder: String?) : Screen {
override val id: String get() = "image:${folder ?: "search"}"
}
data class Crop(val path: String) : Screen {
override val id: String get() = "crop:$path"
}
}
@OptIn(FlowPreview::class)
class VarnishViewModel(app: Application) : AndroidViewModel(app) {
val repo = VarnishRepo(app)
private val prefs = VarnishPrefs(app)
val settings: StateFlow<VarnishSettings> =
prefs.settings.stateIn(viewModelScope, SharingStarted.Eagerly, VarnishSettings())
private val _access = MutableStateFlow(repo.hasAccess())
val access: StateFlow<Boolean> = _access.asStateFlow()
private val _scan = MutableStateFlow(ScanResult())
val scan: StateFlow<ScanResult> = _scan.asStateFlow()
private val _scanning = MutableStateFlow(false)
val scanning: StateFlow<Boolean> = _scanning.asStateFlow()
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
private val _stack = MutableStateFlow<List<Screen>>(listOf(Screen.Home))
val stack: StateFlow<List<Screen>> = _stack.asStateFlow()
private val _folderFiles = MutableStateFlow<List<MediaFile>>(emptyList())
val folderFiles: StateFlow<List<MediaFile>> = _folderFiles.asStateFlow()
val folderBlur: StateFlow<Set<String>> = marks(MarkKind.FOLDER_BLUR)
val fileBlur: StateFlow<Set<String>> = marks(MarkKind.FILE_BLUR)
val favorites: StateFlow<Set<String>> = marks(MarkKind.FAVORITE)
val listFolders: StateFlow<Set<String>> = marks(MarkKind.LIST_VIEW)
private val favFiles: StateFlow<List<MediaFile>> =
combine(favorites, settings, _scan) { f, s, _ -> f to s }
.map { (f, s) -> repo.mediaAt(f, s) }
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val folders: StateFlow<List<MediaFolder>> = combine(_scan, favFiles) { r, f ->
if (f.isEmpty()) r.folders
else listOf(
repo.virtualFolder(VarnishConfig.FAV_PATH, VarnishConfig.FAV_NAME, f)
) + r.folders
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val searchResults: StateFlow<List<MediaFile>> = combine(
_scan,
_query.debounce(150),
settings.map { it.sort }.distinctUntilChanged()
) { r, q, sort -> repo.search(r, q, sort) }
.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
private var current: VarnishSettings? = null
private var folderPath: String? = null
private var scanJob: Job? = null
var topFor: List<MediaFile>? = null
init {
viewModelScope.launch {
var key: Any? = null
var sort: SortMode? = null
prefs.settings.collect { s ->
current = s
val k = Triple(s.scan, s.types, s.showHidden)
if (k != key) {
key = k
rescan(s)
} else if (s.sort != sort) {
val sorted = repo.sortFiles(_folderFiles.value, s.sort)
topFor = sorted
_folderFiles.value = sorted
}
sort = s.sort
}
}
viewModelScope.launch {
favFiles.collect { if (folderPath == VarnishConfig.FAV_PATH) _folderFiles.value = it }
}
}
// scan
fun refresh() {
val s = current
if (s == null) _access.value = repo.hasAccess() else rescan(s)
}
private fun rescan(s: VarnishSettings) {
_access.value = repo.hasAccess()
if (!_access.value) return
scanJob?.cancel()
scanJob = viewModelScope.launch {
_scanning.value = true
try {
reloadFolderNow()
_scan.value = repo.scan(s)
} finally {
if (scanJob == coroutineContext[Job]) _scanning.value = false
}
}
}
private suspend fun reloadFolderNow() {
val p = folderPath ?: return
if (p == VarnishConfig.FAV_PATH) {
_folderFiles.value = favFiles.value
return
}
val list = repo.folderFiles(p, settingsNow())
if (folderPath == p) _folderFiles.value = list
}
private suspend fun changed() {
reloadFolderNow()
refresh()
}
private fun settingsNow() = current ?: settings.value
private fun marks(kind: Int): StateFlow<Set<String>> =
repo.marks(kind).stateIn(viewModelScope, SharingStarted.Eagerly, emptySet())
// navigation
fun open(s: Screen) {
_stack.update { it + s }
when (s) {
is Screen.Folder -> focusFolder(s.path, force = true)
is Screen.Image -> s.folder?.let { focusFolder(it, force = false) }
else -> Unit
}
}
fun back(): Boolean {
val st = _stack.value
if (st.size <= 1) return false
_stack.value = st.dropLast(1)
when (val t = _stack.value.last()) {
is Screen.Folder -> focusFolder(t.path, force = false)
is Screen.Image -> t.folder?.let { focusFolder(it, force = false) }
else -> Unit
}
return true
}
fun openExternal(uri: Uri) = op {
val path = repo.resolve(uri) ?: return@op
val folder = File(path).parent ?: return@op
_stack.value = listOf(Screen.Image(path, folder))
focusFolder(folder, force = true)
}
private fun focusFolder(path: String, force: Boolean) {
if (!force && path == folderPath) return
if (path != folderPath) _folderFiles.value = emptyList()
folderPath = path
viewModelScope.launch { reloadFolderNow() }
}
// state
fun setQuery(q: String) {
_query.value = q
}
fun updateSettings(block: (VarnishSettings) -> VarnishSettings) {
viewModelScope.launch { prefs.update(block) }
}
fun sortAlpha() = updateSettings { it.copy(sort = it.sort.byAlpha()) }
fun sortDate() = updateSettings { it.copy(sort = it.sort.byDate()) }
fun setFolderBlur(path: String, on: Boolean) = op {
repo.setMarks(MarkKind.FOLDER_BLUR, listOf(path), on)
}
fun setFileBlur(paths: Collection<String>, on: Boolean) = op {
repo.setMarks(MarkKind.FILE_BLUR, paths, on)
}
fun setFavorite(path: String, on: Boolean) = op {
repo.setMarks(MarkKind.FAVORITE, listOf(path), on)
}
fun setListView(path: String, on: Boolean) = op {
repo.setMarks(MarkKind.LIST_VIEW, listOf(path), on)
}
// file ops
fun deleteFiles(paths: List<String>, done: (List<String>) -> Unit = {}) = op {
val gone = repo.deleteFiles(paths)
changed()
done(gone)
}
fun deleteFolder(path: String, done: (Boolean) -> Unit = {}) = op {
val ok = repo.deleteFolder(path)
changed()
done(ok)
}
fun renameFile(path: String, base: String, done: (String?) -> Unit = {}) = op {
val to = repo.renameFile(path, base)
if (to != null) changed()
done(to)
}
fun renameFolder(path: String, name: String, done: (String?) -> Unit = {}) = op {
val to = repo.renameFolder(path, name)
if (to != null) {
_stack.update { st ->
st.map { if (it is Screen.Folder && it.path == path) Screen.Folder(to) else it }
}
if (folderPath == path) folderPath = to
changed()
}
done(to)
}
fun createFolder(parent: String, name: String, done: (String?) -> Unit = {}) = op {
val dir = repo.createFolder(parent, name)
if (dir != null) refresh()
done(dir)
}
fun moveFile(path: String, destDir: String, done: (String?) -> Unit = {}) = op {
val to = repo.moveFile(path, destDir)
if (to != null) changed()
done(to)
}
fun crop(path: String, rect: Rect, done: (Boolean) -> Unit = {}) = op {
val out = repo.cropSave(path, rect, settingsNow())
if (out != null) {
val folder = File(out).parent.orEmpty()
folderPath = folder
reloadFolderNow()
_stack.update { st ->
st.dropLastWhile { it is Screen.Crop || it is Screen.Image } +
Screen.Image(out, folder)
}
refresh()
}
done(out != null)
}
fun info(path: String, done: (MediaInfo) -> Unit) = op { done(repo.info(path)) }
private fun op(block: suspend () -> Unit) {
viewModelScope.launch { block() }
}
}