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