3 changed files with 125 additions and 3 deletions
@ -0,0 +1,110 @@
|
||||
package com.utun.chat.ui.screens |
||||
|
||||
import android.graphics.Bitmap |
||||
import androidx.compose.foundation.Image |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.border |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material3.* |
||||
import androidx.compose.runtime.* |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.asImageBitmap |
||||
import androidx.compose.ui.layout.ContentScale |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.Dialog |
||||
import androidx.compose.ui.window.DialogProperties |
||||
import com.utun.chat.ChatApplication |
||||
import com.utun.chat.R |
||||
import com.utun.chat.data.LogManager |
||||
import com.utun.chat.ui.components.ZoomableImage |
||||
import kotlinx.coroutines.* |
||||
|
||||
/** Каталог содержит описания аватарок; оригинал загружается только для выбранной картинки. */ |
||||
@Composable |
||||
fun AvatarViewerScreen(group: String?, owner: Long, name: String, onClose: () -> Unit) { |
||||
val repository=ChatApplication.instance.avatars |
||||
val members by repository.members.collectAsState() |
||||
val revision by repository.revision.collectAsState() |
||||
val account=repository.accountId |
||||
val source=remember(members,group,owner) { repository.gallerySource(group,owner) } |
||||
var files by remember(group,owner,account) { mutableStateOf(emptyList<String>()) } |
||||
var selected by rememberSaveable(group,owner,account) { mutableStateOf("") } |
||||
var previews by remember(group,owner,account) { mutableStateOf(emptyMap<String,Bitmap>()) } |
||||
var original by remember { mutableStateOf<Bitmap?>(null) } |
||||
var failed by remember { mutableStateOf(false) } |
||||
var catalogFailed by remember { mutableStateOf(false) } |
||||
var retry by remember { mutableStateOf(0) } |
||||
LaunchedEffect(account,revision) { if (account==0L) onClose() } |
||||
LaunchedEffect(source?.first,source?.second?.avatarServer,source?.second?.avatarFile,retry) { |
||||
val (channel,member)=source ?: return@LaunchedEffect |
||||
files=listOf(member.avatarFile); if (selected !in files) selected=member.avatarFile |
||||
catalogFailed=false |
||||
try { files=repository.catalog(channel,member) } |
||||
catch (e: CancellationException) { throw e } |
||||
catch (e: Exception) { catalogFailed=true; LogManager.addLog("WARN","Avatar","Catalog failed owner=$owner: ${e.message}") } |
||||
} |
||||
val index=files.indexOf(selected) |
||||
LaunchedEffect(source?.first,source?.second?.avatarServer,selected,files,retry) { |
||||
original=null; failed=false |
||||
val (channel,member)=source ?: return@LaunchedEffect |
||||
if (index<0) return@LaunchedEffect |
||||
val visible=files.subList((index-2).coerceAtLeast(0),(index+3).coerceAtMost(files.size)) |
||||
previews=previews.filterKeys { it in visible } |
||||
coroutineScope { |
||||
launch { |
||||
try { previews=previews+(selected to repository.loadAvatar(channel,member.copy(avatarFile=selected),false)) } |
||||
catch (e: CancellationException) { throw e } |
||||
catch (e: Exception) { failed=true; LogManager.addLog("WARN","Avatar","Preview failed file=$selected: ${e.message}") } |
||||
} |
||||
launch { |
||||
try { original=repository.loadAvatar(channel,member.copy(avatarFile=selected),true) } |
||||
catch (e: CancellationException) { throw e } |
||||
catch (e: Exception) { failed=true; LogManager.addLog("WARN","Avatar","Original failed file=$selected: ${e.message}") } |
||||
} |
||||
visible.filter { it!=selected && it !in previews }.forEach { file -> launch { |
||||
try { previews=previews+(file to repository.loadAvatar(channel,member.copy(avatarFile=file),false)) } |
||||
catch (e: CancellationException) { throw e } |
||||
catch (e: Exception) { LogManager.addLog("WARN","Avatar","Neighbor failed file=$file: ${e.message}") } |
||||
} } |
||||
} |
||||
} |
||||
fun move(delta: Int) { val next=index+delta; if (next in files.indices) selected=files[next] } |
||||
Dialog(onDismissRequest=onClose,properties=DialogProperties(usePlatformDefaultWidth=false)) { |
||||
Column(Modifier.fillMaxSize().background(Color.Black).systemBarsPadding()) { |
||||
Row(Modifier.fillMaxWidth(),verticalAlignment=Alignment.CenterVertically) { |
||||
Text(name,color=Color.White,modifier=Modifier.weight(1f).padding(12.dp)) |
||||
TextButton(onClick=onClose) { Text(stringResource(R.string.close)) } |
||||
} |
||||
Box(Modifier.weight(1f).fillMaxWidth(),contentAlignment=Alignment.Center) { |
||||
val image=original ?: previews[selected] |
||||
if (image!=null) ZoomableImage(image,Modifier.fillMaxSize(),onSwipe={ move(it) }) |
||||
else if (source!=null && !failed) CircularProgressIndicator() |
||||
else Text(stringResource(if (source==null) R.string.avatar_none else R.string.avatar_load_failed),color=Color.White) |
||||
} |
||||
if (failed || catalogFailed) TextButton(onClick={ retry++ },modifier=Modifier.align(Alignment.CenterHorizontally)) { |
||||
Text(stringResource(R.string.avatar_retry)) |
||||
} else if (original==null && source!=null) LinearProgressIndicator(Modifier.fillMaxWidth()) |
||||
if (index>=0) Text("${index+1} / ${files.size}",color=Color.White,modifier=Modifier.align(Alignment.CenterHorizontally)) |
||||
Row(Modifier.fillMaxWidth().padding(vertical=8.dp),verticalAlignment=Alignment.CenterVertically, |
||||
horizontalArrangement=Arrangement.Center) { |
||||
TextButton(enabled=index>0,onClick={ move(-1) },contentPadding=PaddingValues(4.dp),modifier=Modifier.width(32.dp)) { Text("←") } |
||||
repeat(5) { slot -> |
||||
val item=index+slot-2; val file=files.getOrNull(item) |
||||
Box(Modifier.weight(1f).aspectRatio(1f).padding(2.dp) |
||||
.border(if (slot==2) 2.dp else 0.dp,if (slot==2) Color(0xFF77AAFF) else Color.Transparent) |
||||
.clickable(enabled=file!=null) { if (file!=null) selected=file },contentAlignment=Alignment.Center) { |
||||
previews[file]?.let { Image(it.asImageBitmap(),contentDescription=null,contentScale=ContentScale.Crop, |
||||
modifier=Modifier.fillMaxSize()) } |
||||
} |
||||
} |
||||
TextButton(enabled=index>=0 && index+1<files.size,onClick={ move(1) },contentPadding=PaddingValues(4.dp), |
||||
modifier=Modifier.width(32.dp)) { Text("→") } |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue