From 353a30efe426f69af94c40a20943d595392c5e8c Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Tue, 11 Jun 2024 16:46:49 +0100 Subject: [PATCH 01/21] Introduce MediaServersViewModel. Basic things for now. --- .../mediaServers/MediaServersViewModel.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt new file mode 100644 index 000000000..1c14ee4b4 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers + +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.Nip96MediaServers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.czeal.rfc3986.URIReference + +class MediaServersViewModel : ViewModel() { + lateinit var account: Account + + private val _fileServers = MutableStateFlow>(emptyList()) + val fileServers = _fileServers.asStateFlow() + var isModified = false + + fun load(account: Account) { + this.account = account + refresh() + } + + fun refresh() { + isModified = false + _fileServers.update { + val obtainedFileServers = account.getFileServersList()?.servers() ?: emptyList() + obtainedFileServers.map { serverUrl -> + Nip96MediaServers + .ServerName( + URIReference.parse(serverUrl).host.value, + serverUrl, + ) + } + } + } + + fun addServer(serverUrl: String) { + } + + fun removeServer( + name: String = "", + serverUrl: String, + ) { + } + + fun removeAll() { + _fileServers.update { emptyList() } + isModified = true + } +} From 76e3b5abe78ee10f2bc8406c920d98bebf6f4a78 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 13 Jun 2024 02:19:18 +0100 Subject: [PATCH 02/21] Add rudimentary implementations for addServer() and removeServer(). --- .../mediaServers/MediaServersViewModel.kt | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index 1c14ee4b4..d4b70759a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -43,7 +43,7 @@ class MediaServersViewModel : ViewModel() { fun refresh() { isModified = false _fileServers.update { - val obtainedFileServers = account.getFileServersList()?.servers() ?: emptyList() + val obtainedFileServers = obtainFileServers() ?: emptyList() obtainedFileServers.map { serverUrl -> Nip96MediaServers .ServerName( @@ -55,16 +55,40 @@ class MediaServersViewModel : ViewModel() { } fun addServer(serverUrl: String) { + account.sendFileServersList(listOf(serverUrl)) + val serverNameReference = URIReference.parse(serverUrl).host.value + _fileServers.update { + it.plus( + Nip96MediaServers.ServerName(serverNameReference, serverUrl), + ) + } + isModified = true } fun removeServer( name: String = "", serverUrl: String, ) { + // Here, there are two approaches: either use the internal fileServers flow value to update, + // Or re-read the account's saved file servers. They are the same most of the time, but I am + // not sure that assumption always holds. Prefer reading from account instead. + // TODO: Test the two approaches and choose one(for future commit). + val currentFileServers = obtainFileServers() + val newFileServers = currentFileServers?.minus(serverUrl) + newFileServers?.let { account.sendFileServersList(it) } + val serverName = if (name.isNotBlank()) name else URIReference.parse(serverUrl).host.value + _fileServers.update { + it.minus( + Nip96MediaServers.ServerName(serverName, serverUrl), + ) + } + isModified = true } - fun removeAll() { + fun removeAllServers() { _fileServers.update { emptyList() } isModified = true } + + private fun obtainFileServers(): List? = account.getFileServersList()?.servers() } From ed5d7335a051111be7b43069f700c3247511ea85 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 13 Jun 2024 11:08:44 +0100 Subject: [PATCH 03/21] Simplify addServer() and removeServer(). Introduce saveFileServers() for saving changes, and broadcasting them. --- .../mediaServers/MediaServersViewModel.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index d4b70759a..d3506fa0a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -21,11 +21,14 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip96MediaServers +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.czeal.rfc3986.URIReference class MediaServersViewModel : ViewModel() { @@ -55,7 +58,6 @@ class MediaServersViewModel : ViewModel() { } fun addServer(serverUrl: String) { - account.sendFileServersList(listOf(serverUrl)) val serverNameReference = URIReference.parse(serverUrl).host.value _fileServers.update { it.plus( @@ -69,13 +71,6 @@ class MediaServersViewModel : ViewModel() { name: String = "", serverUrl: String, ) { - // Here, there are two approaches: either use the internal fileServers flow value to update, - // Or re-read the account's saved file servers. They are the same most of the time, but I am - // not sure that assumption always holds. Prefer reading from account instead. - // TODO: Test the two approaches and choose one(for future commit). - val currentFileServers = obtainFileServers() - val newFileServers = currentFileServers?.minus(serverUrl) - newFileServers?.let { account.sendFileServersList(it) } val serverName = if (name.isNotBlank()) name else URIReference.parse(serverUrl).host.value _fileServers.update { it.minus( @@ -90,5 +85,16 @@ class MediaServersViewModel : ViewModel() { isModified = true } + fun saveFileServers() { + // TODO: Add setting the default file server here. + if (isModified) { + viewModelScope.launch(Dispatchers.IO) { + val serverList = _fileServers.value.map { it.baseUrl } + account.sendFileServersList(serverList) + refresh() + } + } + } + private fun obtainFileServers(): List? = account.getFileServersList()?.servers() } From bba7dba17cf4c02a30c23286c62ee139fab4c1d3 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 21 Jun 2024 14:52:08 +0100 Subject: [PATCH 04/21] Introduce MediaServersListView and add it to Drawer. --- .../mediaServers/MediaServersLIstView.kt | 237 ++++++++++++++++++ .../amethyst/ui/navigation/DrawerContent.kt | 15 ++ 2 files changed, 252 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt new file mode 100644 index 000000000..b8eb6c45f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.service.Nip96MediaServers +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.SaveButton +import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.grayText + +// TODO: Implement UI for Media servers view. +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MediaServersListView( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val mediaServersViewModel: MediaServersViewModel = viewModel() + val mediaServersState by mediaServersViewModel.fileServers.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + mediaServersViewModel.load(accountViewModel.account) + } + + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Media Servers", + style = MaterialTheme.typography.titleLarge, + ) + } + }, + navigationIcon = { + CloseButton( + onPress = { + mediaServersViewModel.refresh() + onClose() + }, + ) + }, + actions = { + SaveButton( + onPost = { + mediaServersViewModel.saveFileServers() + onClose() + }, + isActive = true, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { padding -> + LazyColumn( + modifier = + Modifier + .fillMaxSize() + .padding( + 16.dp, + padding.calculateTopPadding(), + 16.dp, + padding.calculateBottomPadding(), + ), + verticalArrangement = Arrangement.SpaceAround, + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = FeedPadding, + ) { + item { + Text( + "Set your preferred media upload servers.", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } + + if (mediaServersState.isEmpty()) { + item { + Box( + modifier = + Modifier + .fillMaxWidth() + .fillMaxWidth(0.2f), + contentAlignment = Alignment.Center, + ) { + Text(text = "You have no custom media servers set.") + } + } + } else { + itemsIndexed( + mediaServersState, + key = { index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, entry -> + MediaServerEntry(serverEntry = entry) { + mediaServersViewModel.removeServer(serverUrl = it) + } + } + } + + item { + SettingsCategoryWithButton( + title = "Built-in Media Servers", + action = { + OutlinedButton( + onClick = { }, + ) { + Text(text = "Set as Default") + } + }, + ) + } + Nip96MediaServers.DEFAULT.let { + itemsIndexed( + it, + key = { + index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, server -> + MediaServerEntry(serverEntry = server) { + } + } + } + } + } + } +} + +@Composable +fun MediaServerEntry( + modifier: Modifier = Modifier, + serverEntry: Nip96MediaServers.ServerName, + onDelete: (serverUrl: String) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = + Modifier + .weight(1f), + ) { + serverEntry.let { + Text( + text = it.name, + style = MaterialTheme.typography.headlineMedium, + ) + Spacer(modifier = StdVertSpacer) + Text( + text = it.baseUrl, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } + } + Column { + OutlinedButton( + onClick = { + onDelete(serverEntry.baseUrl) + }, + ) { + Text(text = "Delete") + } + OutlinedButton( + onClick = {}, + ) { + Text(text = "Set Default") + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 4202294fc..4800891b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus +import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji @@ -450,6 +451,7 @@ fun ListContent( val coroutineScope = rememberCoroutineScope() var wantsToEditRelays by remember { mutableStateOf(false) } + var editMediaServers by remember { mutableStateOf(false) } var backupDialogOpen by remember { mutableStateOf(false) } var checked by remember { mutableStateOf(accountViewModel.account.proxy != null) } @@ -494,6 +496,16 @@ fun ListContent( }, ) + IconRow( + title = "Media Servers", + icon = androidx.media3.ui.R.drawable.exo_icon_repeat_all, + tint = MaterialTheme.colorScheme.onBackground, + onClick = { + coroutineScope.launch { drawerState.close() } + editMediaServers = true + }, + ) + NavigationRow( title = stringRes(R.string.security_filters), icon = Route.BlockedUsers.icon, @@ -560,6 +572,9 @@ fun ListContent( if (wantsToEditRelays) { AllRelayListView({ wantsToEditRelays = false }, accountViewModel = accountViewModel, nav = nav) } + if (editMediaServers) { + MediaServersListView({ editMediaServers = false }, accountViewModel = accountViewModel, nav = nav) + } if (backupDialogOpen) { AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false }) } From cd9ba9befe55e2ca353e19ae962a1ec529f7d8e9 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 26 Jun 2024 16:09:56 +0100 Subject: [PATCH 05/21] Use divider to separate custom and hardcoded server options. Introduce new padding value. --- .../mediaServers/MediaServersLIstView.kt | 166 ++++++++++-------- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 3 + 2 files changed, 94 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt index b8eb6c45f..572df5205 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.actions.mediaServers +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -53,11 +54,13 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText // TODO: Implement UI for Media servers view. +@ExperimentalFoundationApi @OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaServersListView( @@ -115,75 +118,87 @@ fun MediaServersListView( ) }, ) { padding -> - LazyColumn( + Column( modifier = Modifier .fillMaxSize() .padding( - 16.dp, - padding.calculateTopPadding(), - 16.dp, - padding.calculateBottomPadding(), + start = 16.dp, + top = padding.calculateTopPadding(), + end = 16.dp, + bottom = padding.calculateBottomPadding(), ), - verticalArrangement = Arrangement.SpaceAround, + verticalArrangement = Arrangement.spacedBy(5.dp, alignment = Alignment.Top), horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = FeedPadding, ) { - item { - Text( - "Set your preferred media upload servers.", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.grayText, - ) - } + Text( + "Set your preferred media upload servers.", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.grayText, + ) - if (mediaServersState.isEmpty()) { - item { - Box( - modifier = - Modifier - .fillMaxWidth() - .fillMaxWidth(0.2f), - contentAlignment = Alignment.Center, - ) { - Text(text = "You have no custom media servers set.") - } - } - } else { - itemsIndexed( - mediaServersState, - key = { index: Int, server: Nip96MediaServers.ServerName -> - server.baseUrl - }, - ) { index, entry -> - MediaServerEntry(serverEntry = entry) { - mediaServersViewModel.removeServer(serverUrl = it) - } - } - } + HorizontalDivider( + thickness = DividerThickness, + color = MaterialTheme.colorScheme.onSurface, + ) - item { - SettingsCategoryWithButton( - title = "Built-in Media Servers", - action = { - OutlinedButton( - onClick = { }, - ) { - Text(text = "Set as Default") + LazyColumn( +// modifier = +// Modifier +// .padding(top = 10.dp), + verticalArrangement = Arrangement.SpaceAround, + horizontalAlignment = Alignment.CenterHorizontally, +// contentPadding = FeedPadding, + ) { + if (mediaServersState.isEmpty()) { + item { + Text( + text = "You have no custom media servers set.", + modifier = DoubleVertPadding, + ) + } + } else { + itemsIndexed( + mediaServersState, + key = { index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, entry -> + MediaServerEntry(serverEntry = entry) { + mediaServersViewModel.removeServer(serverUrl = it) + } + } + } + + item { + HorizontalDivider( + thickness = DividerThickness, + color = MaterialTheme.colorScheme.onSurface, + ) + + SettingsCategoryWithButton( + title = "Built-in Media Servers", + description = "These servers come by default with Amethyst.", + action = { + OutlinedButton( + onClick = { }, + ) { + Text(text = "Set as Default") + } + }, + ) + } + Nip96MediaServers.DEFAULT.let { + itemsIndexed( + it, + key = { + index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, server -> + MediaServerEntry(serverEntry = server) { } - }, - ) - } - Nip96MediaServers.DEFAULT.let { - itemsIndexed( - it, - key = { - index: Int, server: Nip96MediaServers.ServerName -> - server.baseUrl - }, - ) { index, server -> - MediaServerEntry(serverEntry = server) { } } } @@ -209,7 +224,7 @@ fun MediaServerEntry( serverEntry.let { Text( text = it.name, - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.bodyLarge, ) Spacer(modifier = StdVertSpacer) Text( @@ -219,19 +234,20 @@ fun MediaServerEntry( ) } } - Column { - OutlinedButton( - onClick = { - onDelete(serverEntry.baseUrl) - }, - ) { - Text(text = "Delete") - } - OutlinedButton( - onClick = {}, - ) { - Text(text = "Set Default") - } + OutlinedButton( + onClick = { + onDelete(serverEntry.baseUrl) + }, + ) { + Text(text = "Delete") } + OutlinedButton( + onClick = {}, + ) { + Text(text = "Set Default") + } +// Column { +// +// } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 2af199445..f8c89983f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -121,6 +121,9 @@ val HalfVertPadding = Modifier.padding(vertical = 5.dp) val HorzPadding = Modifier.padding(horizontal = 10.dp) val VertPadding = Modifier.padding(vertical = 10.dp) +val DoubleHorzPadding = Modifier.padding(horizontal = 20.dp) +val DoubleVertPadding = Modifier.padding(vertical = 20.dp) + val MaxWidthWithHorzPadding = Modifier.fillMaxWidth().padding(horizontal = 10.dp) val Size5Modifier = Modifier.size(5.dp) From 409b63b37470a3cf342910d38cb0f4cf5acd80f6 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 27 Jun 2024 16:23:27 +0100 Subject: [PATCH 06/21] Normalize server URL before adding it. --- .../ui/actions/mediaServers/MediaServersViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index d3506fa0a..4d4235253 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -58,10 +58,11 @@ class MediaServersViewModel : ViewModel() { } fun addServer(serverUrl: String) { - val serverNameReference = URIReference.parse(serverUrl).host.value + val normalizedUrl = URIReference.parse(serverUrl.trim()).normalize().toString() + val serverNameReference = URIReference.parse(normalizedUrl).host.value _fileServers.update { it.plus( - Nip96MediaServers.ServerName(serverNameReference, serverUrl), + Nip96MediaServers.ServerName(serverNameReference, normalizedUrl), ) } isModified = true From 0a6d9a7764d44dda8b0e4c811224fb6edaf32bb9 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 27 Jun 2024 17:17:49 +0100 Subject: [PATCH 07/21] Normalize server URL before adding it, Part 2. --- .../actions/mediaServers/MediaServersViewModel.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index 4d4235253..f6fd9a2f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -58,8 +58,18 @@ class MediaServersViewModel : ViewModel() { } fun addServer(serverUrl: String) { - val normalizedUrl = URIReference.parse(serverUrl.trim()).normalize().toString() - val serverNameReference = URIReference.parse(normalizedUrl).host.value + val normalizedUrl = + try { + URIReference.parse(serverUrl.trim()).normalize().toString() + } catch (e: Exception) { + serverUrl + } + val serverNameReference = + try { + URIReference.parse(normalizedUrl).host.value + } catch (e: Exception) { + normalizedUrl.replaceFirstChar { it.uppercase() } + } _fileServers.update { it.plus( Nip96MediaServers.ServerName(serverNameReference, normalizedUrl), From 963a42becf83e1092ad215193771645bf30da972 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 19:56:05 +0100 Subject: [PATCH 08/21] Make first character in derived name uppercase, for ...aesthetics. --- .../amethyst/ui/actions/mediaServers/MediaServersViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index f6fd9a2f6..84040cca7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -66,7 +66,7 @@ class MediaServersViewModel : ViewModel() { } val serverNameReference = try { - URIReference.parse(normalizedUrl).host.value + URIReference.parse(normalizedUrl).host.value.replaceFirstChar { it.uppercase() } } catch (e: Exception) { normalizedUrl.replaceFirstChar { it.uppercase() } } From aae7236106fd17fb5cf880c7f464a2ed01b5a69d Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 19:57:57 +0100 Subject: [PATCH 09/21] Introduce MediaServerEdit component. --- .../mediaServers/MediaServerEditField.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt new file mode 100644 index 000000000..1b8f0e284 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun MediaServerEditField( + modifier: Modifier = Modifier, + onAddServer: (String) -> Unit, +) { + var url by remember { mutableStateOf("") } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + Arrangement.spacedBy( + Size10dp, + ), + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.add_a_relay)) }, + modifier = Modifier.weight(1f), + value = url, + onValueChange = { url = it }, + placeholder = { + Text( + text = "server.com", + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + }, + singleLine = true, + ) + + Button( + onClick = { + if (url.isNotBlank() && url != "/") { + onAddServer(url) + url = "" + } + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = + if (url.isNotBlank()) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.placeholderText + }, + ), + ) { + Text(text = stringRes(id = R.string.add), color = Color.White) + } + } +} From 6c88029fe30184c9e53bef80aa16676c567d8c7c Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 20:01:41 +0100 Subject: [PATCH 10/21] Mve media server list rendering to separate function. Fix padding for rendered items. Rename onDelete to onAddOrDelete() for component reuse. --- .../mediaServers/MediaServersLIstView.kt | 148 ++++++++++-------- 1 file changed, 82 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt index 572df5205..cecf09b2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions.mediaServers -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,9 +28,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -54,13 +58,12 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding +import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText // TODO: Implement UI for Media servers view. -@ExperimentalFoundationApi @OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaServersListView( @@ -138,58 +141,31 @@ fun MediaServersListView( color = MaterialTheme.colorScheme.grayText, ) - HorizontalDivider( - thickness = DividerThickness, - color = MaterialTheme.colorScheme.onSurface, - ) - LazyColumn( -// modifier = -// Modifier -// .padding(top = 10.dp), verticalArrangement = Arrangement.SpaceAround, horizontalAlignment = Alignment.CenterHorizontally, -// contentPadding = FeedPadding, + contentPadding = FeedPadding, ) { - if (mediaServersState.isEmpty()) { + renderMediaServerList(mediaServersState, mediaServersViewModel) + + Nip96MediaServers.DEFAULT.let { item { - Text( - text = "You have no custom media servers set.", - modifier = DoubleVertPadding, + SettingsCategoryWithButton( + title = "Built-in Media Servers", + description = "Amethyst's default list. You can add them individually or add the list.", + action = { + OutlinedButton( + onClick = { + it.forEach { server -> + mediaServersViewModel.addServer(server.baseUrl) + } + }, + ) { + Text(text = "Use Default List") + } + }, ) } - } else { - itemsIndexed( - mediaServersState, - key = { index: Int, server: Nip96MediaServers.ServerName -> - server.baseUrl - }, - ) { index, entry -> - MediaServerEntry(serverEntry = entry) { - mediaServersViewModel.removeServer(serverUrl = it) - } - } - } - - item { - HorizontalDivider( - thickness = DividerThickness, - color = MaterialTheme.colorScheme.onSurface, - ) - - SettingsCategoryWithButton( - title = "Built-in Media Servers", - description = "These servers come by default with Amethyst.", - action = { - OutlinedButton( - onClick = { }, - ) { - Text(text = "Set as Default") - } - }, - ) - } - Nip96MediaServers.DEFAULT.let { itemsIndexed( it, key = { @@ -197,8 +173,13 @@ fun MediaServersListView( server.baseUrl }, ) { index, server -> - MediaServerEntry(serverEntry = server) { - } + MediaServerEntry( + serverEntry = server, + isAmethystDefault = true, + onAddOrDelete = { + mediaServersViewModel.addServer(it) + }, + ) } } } @@ -207,14 +188,49 @@ fun MediaServersListView( } } +fun LazyListScope.renderMediaServerList( + mediaServersState: List, + mediaServersViewModel: MediaServersViewModel, +) { + if (mediaServersState.isEmpty()) { + item { + Text( + text = "You have no custom media servers set. You can use Amethyst's list, or add one below ↓", + modifier = DoubleVertPadding, + ) + } + } else { + itemsIndexed( + mediaServersState, + key = { index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, entry -> + MediaServerEntry(serverEntry = entry) { + mediaServersViewModel.removeServer(serverUrl = it) + } + } + } + + item { + Spacer(modifier = StdVertSpacer) + MediaServerEditField { + mediaServersViewModel.addServer(it) + } + } +} + @Composable fun MediaServerEntry( modifier: Modifier = Modifier, serverEntry: Nip96MediaServers.ServerName, - onDelete: (serverUrl: String) -> Unit, + isAmethystDefault: Boolean = false, + onAddOrDelete: (serverUrl: String) -> Unit, ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, ) { Column( modifier = @@ -234,20 +250,20 @@ fun MediaServerEntry( ) } } - OutlinedButton( - onClick = { - onDelete(serverEntry.baseUrl) - }, + + Row( + horizontalArrangement = Arrangement.End, ) { - Text(text = "Delete") + IconButton( + onClick = { + onAddOrDelete(serverEntry.baseUrl) + }, + ) { + Icon( + imageVector = if (isAmethystDefault) Icons.Rounded.Add else Icons.Rounded.Delete, + contentDescription = if (isAmethystDefault) "Add media server" else "Delete media server", + ) + } } - OutlinedButton( - onClick = {}, - ) { - Text(text = "Set Default") - } -// Column { -// -// } } } From b5047f213e87a5ab51a099c2d6dda393daaeac7b Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 20:01:41 +0100 Subject: [PATCH 11/21] Move media server list rendering to separate function. Fix padding for rendered items. Rename onDelete to onAddOrDelete() for component reuse. --- .../mediaServers/MediaServersLIstView.kt | 148 ++++++++++-------- 1 file changed, 82 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt index 572df5205..cecf09b2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions.mediaServers -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,9 +28,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -54,13 +58,12 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding +import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText // TODO: Implement UI for Media servers view. -@ExperimentalFoundationApi @OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaServersListView( @@ -138,58 +141,31 @@ fun MediaServersListView( color = MaterialTheme.colorScheme.grayText, ) - HorizontalDivider( - thickness = DividerThickness, - color = MaterialTheme.colorScheme.onSurface, - ) - LazyColumn( -// modifier = -// Modifier -// .padding(top = 10.dp), verticalArrangement = Arrangement.SpaceAround, horizontalAlignment = Alignment.CenterHorizontally, -// contentPadding = FeedPadding, + contentPadding = FeedPadding, ) { - if (mediaServersState.isEmpty()) { + renderMediaServerList(mediaServersState, mediaServersViewModel) + + Nip96MediaServers.DEFAULT.let { item { - Text( - text = "You have no custom media servers set.", - modifier = DoubleVertPadding, + SettingsCategoryWithButton( + title = "Built-in Media Servers", + description = "Amethyst's default list. You can add them individually or add the list.", + action = { + OutlinedButton( + onClick = { + it.forEach { server -> + mediaServersViewModel.addServer(server.baseUrl) + } + }, + ) { + Text(text = "Use Default List") + } + }, ) } - } else { - itemsIndexed( - mediaServersState, - key = { index: Int, server: Nip96MediaServers.ServerName -> - server.baseUrl - }, - ) { index, entry -> - MediaServerEntry(serverEntry = entry) { - mediaServersViewModel.removeServer(serverUrl = it) - } - } - } - - item { - HorizontalDivider( - thickness = DividerThickness, - color = MaterialTheme.colorScheme.onSurface, - ) - - SettingsCategoryWithButton( - title = "Built-in Media Servers", - description = "These servers come by default with Amethyst.", - action = { - OutlinedButton( - onClick = { }, - ) { - Text(text = "Set as Default") - } - }, - ) - } - Nip96MediaServers.DEFAULT.let { itemsIndexed( it, key = { @@ -197,8 +173,13 @@ fun MediaServersListView( server.baseUrl }, ) { index, server -> - MediaServerEntry(serverEntry = server) { - } + MediaServerEntry( + serverEntry = server, + isAmethystDefault = true, + onAddOrDelete = { + mediaServersViewModel.addServer(it) + }, + ) } } } @@ -207,14 +188,49 @@ fun MediaServersListView( } } +fun LazyListScope.renderMediaServerList( + mediaServersState: List, + mediaServersViewModel: MediaServersViewModel, +) { + if (mediaServersState.isEmpty()) { + item { + Text( + text = "You have no custom media servers set. You can use Amethyst's list, or add one below ↓", + modifier = DoubleVertPadding, + ) + } + } else { + itemsIndexed( + mediaServersState, + key = { index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, entry -> + MediaServerEntry(serverEntry = entry) { + mediaServersViewModel.removeServer(serverUrl = it) + } + } + } + + item { + Spacer(modifier = StdVertSpacer) + MediaServerEditField { + mediaServersViewModel.addServer(it) + } + } +} + @Composable fun MediaServerEntry( modifier: Modifier = Modifier, serverEntry: Nip96MediaServers.ServerName, - onDelete: (serverUrl: String) -> Unit, + isAmethystDefault: Boolean = false, + onAddOrDelete: (serverUrl: String) -> Unit, ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, ) { Column( modifier = @@ -234,20 +250,20 @@ fun MediaServerEntry( ) } } - OutlinedButton( - onClick = { - onDelete(serverEntry.baseUrl) - }, + + Row( + horizontalArrangement = Arrangement.End, ) { - Text(text = "Delete") + IconButton( + onClick = { + onAddOrDelete(serverEntry.baseUrl) + }, + ) { + Icon( + imageVector = if (isAmethystDefault) Icons.Rounded.Add else Icons.Rounded.Delete, + contentDescription = if (isAmethystDefault) "Add media server" else "Delete media server", + ) + } } - OutlinedButton( - onClick = {}, - ) { - Text(text = "Set Default") - } -// Column { -// -// } } } From feee845d31c945689ee6c2848595647c536b161f Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 22:19:21 +0100 Subject: [PATCH 12/21] Add convenience function for adding default Amethyst list. --- .../mediaServers/MediaServersViewModel.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index 84040cca7..c708bd471 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -57,6 +57,12 @@ class MediaServersViewModel : ViewModel() { } } + fun addServerList(serverList: List) { + serverList.forEach { serverUrl -> + addServer(serverUrl) + } + } + fun addServer(serverUrl: String) { val normalizedUrl = try { @@ -66,9 +72,9 @@ class MediaServersViewModel : ViewModel() { } val serverNameReference = try { - URIReference.parse(normalizedUrl).host.value.replaceFirstChar { it.uppercase() } + URIReference.parse(normalizedUrl).host.value } catch (e: Exception) { - normalizedUrl.replaceFirstChar { it.uppercase() } + normalizedUrl } _fileServers.update { it.plus( @@ -82,13 +88,15 @@ class MediaServersViewModel : ViewModel() { name: String = "", serverUrl: String, ) { - val serverName = if (name.isNotBlank()) name else URIReference.parse(serverUrl).host.value - _fileServers.update { - it.minus( - Nip96MediaServers.ServerName(serverName, serverUrl), - ) + viewModelScope.launch { + val serverName = if (name.isNotBlank()) name else URIReference.parse(serverUrl).host.value + _fileServers.update { + it.minus( + Nip96MediaServers.ServerName(serverName, serverUrl), + ) + } + isModified = true } - isModified = true } fun removeAllServers() { From b26355097d3970632467be03f9a3833c7314e07e Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 22:26:24 +0100 Subject: [PATCH 13/21] Refactor renderMediaServerList to use callbacks. Use addServerList. --- .../mediaServers/MediaServersLIstView.kt | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt index cecf09b2b..6c00c17f9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -146,7 +146,15 @@ fun MediaServersListView( horizontalAlignment = Alignment.CenterHorizontally, contentPadding = FeedPadding, ) { - renderMediaServerList(mediaServersState, mediaServersViewModel) + renderMediaServerList( + mediaServersState = mediaServersState, + onAddServer = { server -> + mediaServersViewModel.addServer(server) + }, + onDeleteServer = { + mediaServersViewModel.removeServer(serverUrl = it) + }, + ) Nip96MediaServers.DEFAULT.let { item { @@ -156,9 +164,7 @@ fun MediaServersListView( action = { OutlinedButton( onClick = { - it.forEach { server -> - mediaServersViewModel.addServer(server.baseUrl) - } + mediaServersViewModel.addServerList(it.map { s -> s.baseUrl }) }, ) { Text(text = "Use Default List") @@ -168,8 +174,7 @@ fun MediaServersListView( } itemsIndexed( it, - key = { - index: Int, server: Nip96MediaServers.ServerName -> + key = { index: Int, server: Nip96MediaServers.ServerName -> server.baseUrl }, ) { index, server -> @@ -190,7 +195,8 @@ fun MediaServersListView( fun LazyListScope.renderMediaServerList( mediaServersState: List, - mediaServersViewModel: MediaServersViewModel, + onAddServer: (String) -> Unit, + onDeleteServer: (String) -> Unit, ) { if (mediaServersState.isEmpty()) { item { @@ -206,16 +212,19 @@ fun LazyListScope.renderMediaServerList( server.baseUrl }, ) { index, entry -> - MediaServerEntry(serverEntry = entry) { - mediaServersViewModel.removeServer(serverUrl = it) - } + MediaServerEntry( + serverEntry = entry, + onAddOrDelete = { + onDeleteServer(it) + }, + ) } } item { Spacer(modifier = StdVertSpacer) MediaServerEditField { - mediaServersViewModel.addServer(it) + onAddServer(it) } } } @@ -239,7 +248,7 @@ fun MediaServerEntry( ) { serverEntry.let { Text( - text = it.name, + text = it.name.replaceFirstChar(Char::titlecase), style = MaterialTheme.typography.bodyLarge, ) Spacer(modifier = StdVertSpacer) From dc40856bc2596dc85216c78de8d543c0b1049e0a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 23:04:25 +0100 Subject: [PATCH 14/21] Ugly solution to avoid duplicates. --- .../ui/actions/mediaServers/MediaServersViewModel.kt | 11 +++++++---- app/src/main/res/values/strings.xml | 10 ++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index c708bd471..cd57917fd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -76,10 +76,13 @@ class MediaServersViewModel : ViewModel() { } catch (e: Exception) { normalizedUrl } - _fileServers.update { - it.plus( - Nip96MediaServers.ServerName(serverNameReference, normalizedUrl), - ) + val serverRef = Nip96MediaServers.ServerName(serverNameReference, normalizedUrl) + if (_fileServers.value.contains(serverRef)) { + return + } else { + _fileServers.update { + it.plus(serverRef) + } } isModified = true } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63ed8c01e..3e80b8b4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -375,6 +375,16 @@ LnAddress or @User + Media Servers + Set your preferred media upload servers. + You have no custom media servers set. You can use Amethyst\'s list, or add one below ↓ + Built-in Media Servers + Amethyst\'s default list. You can add them individually or add the list. + Use Default List + Add media server + Delete media server + + Your relays (NIP-95) Files are hosted by your relays. New NIP: check if they support From 2423ad6e8779fbba1129f9bafb02a6d34d89404c Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 23:08:08 +0100 Subject: [PATCH 15/21] [Squash] Use string resources. Ugly solution to avoid duplicates. --- .../mediaServers/MediaServersLIstView.kt | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt index 6c00c17f9..08672cb3c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -53,11 +53,13 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -92,7 +94,7 @@ fun MediaServersListView( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "Media Servers", + text = stringRes(id = R.string.media_servers), style = MaterialTheme.typography.titleLarge, ) } @@ -135,7 +137,7 @@ fun MediaServersListView( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - "Set your preferred media upload servers.", + stringRes(id = R.string.set_preferred_media_servers), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.grayText, @@ -159,15 +161,15 @@ fun MediaServersListView( Nip96MediaServers.DEFAULT.let { item { SettingsCategoryWithButton( - title = "Built-in Media Servers", - description = "Amethyst's default list. You can add them individually or add the list.", + title = stringRes(id = R.string.built_in_media_servers_title), + description = stringRes(id = R.string.built_in_servers_description), action = { OutlinedButton( onClick = { mediaServersViewModel.addServerList(it.map { s -> s.baseUrl }) }, ) { - Text(text = "Use Default List") + Text(text = stringRes(id = R.string.use_default_servers)) } }, ) @@ -181,8 +183,8 @@ fun MediaServersListView( MediaServerEntry( serverEntry = server, isAmethystDefault = true, - onAddOrDelete = { - mediaServersViewModel.addServer(it) + onAddOrDelete = { serverUrl -> + mediaServersViewModel.addServer(serverUrl) }, ) } @@ -201,7 +203,7 @@ fun LazyListScope.renderMediaServerList( if (mediaServersState.isEmpty()) { item { Text( - text = "You have no custom media servers set. You can use Amethyst's list, or add one below ↓", + text = stringRes(id = R.string.no_media_server_message), modifier = DoubleVertPadding, ) } @@ -237,7 +239,10 @@ fun MediaServerEntry( onAddOrDelete: (serverUrl: String) -> Unit, ) { Row( - modifier = modifier.fillMaxWidth().padding(vertical = 10.dp), + modifier = + modifier + .fillMaxWidth() + .padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceAround, ) { @@ -270,7 +275,12 @@ fun MediaServerEntry( ) { Icon( imageVector = if (isAmethystDefault) Icons.Rounded.Add else Icons.Rounded.Delete, - contentDescription = if (isAmethystDefault) "Add media server" else "Delete media server", + contentDescription = + if (isAmethystDefault) { + stringRes(id = R.string.add_media_server) + } else { + stringRes(id = R.string.delete_media_server) + }, ) } } From 9a4d829f73753dc657eb30f64df61a78bbcf293d Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 28 Jun 2024 23:12:04 +0100 Subject: [PATCH 16/21] Remove TODO. Make isModified private. --- .../amethyst/ui/actions/mediaServers/MediaServersViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt index cd57917fd..9805e0d79 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt @@ -36,7 +36,7 @@ class MediaServersViewModel : ViewModel() { private val _fileServers = MutableStateFlow>(emptyList()) val fileServers = _fileServers.asStateFlow() - var isModified = false + private var isModified = false fun load(account: Account) { this.account = account @@ -108,7 +108,6 @@ class MediaServersViewModel : ViewModel() { } fun saveFileServers() { - // TODO: Add setting the default file server here. if (isModified) { viewModelScope.launch(Dispatchers.IO) { val serverList = _fileServers.value.map { it.baseUrl } From 9aa511b3984734806f243f2efeb1b46b79f358db Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sat, 29 Jun 2024 00:02:44 +0100 Subject: [PATCH 17/21] Refactor: location of files. --- .../amethyst/ui/actions/mediaServers/MediaServerEditField.kt | 0 .../amethyst/ui/actions/mediaServers/MediaServersLIstView.kt | 0 .../amethyst/ui/actions/mediaServers/MediaServersViewModel.kt | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {app => amethyst}/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt (100%) rename {app => amethyst}/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt (100%) rename {app => amethyst}/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt (100%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt similarity index 100% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerEditField.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt similarity index 100% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt similarity index 100% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersViewModel.kt From af3a52c75ef2c7b98972e320042b1ea4d6949ccb Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sat, 29 Jun 2024 00:22:06 +0100 Subject: [PATCH 18/21] Try fixing this again. --- .../mediaServers/MediaServerEditField.kt | 94 ++++++ .../mediaServers/MediaServersLIstView.kt | 288 ++++++++++++++++++ .../mediaServers/MediaServersViewModel.kt | 121 ++++++++ 3 files changed, 503 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt new file mode 100644 index 000000000..d504f824a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers.mediaServers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun MediaServerEditField( + modifier: Modifier = Modifier, + onAddServer: (String) -> Unit, +) { + var url by remember { mutableStateOf("") } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + Arrangement.spacedBy( + Size10dp, + ), + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.add_a_relay)) }, + modifier = Modifier.weight(1f), + value = url, + onValueChange = { url = it }, + placeholder = { + Text( + text = "server.com", + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + }, + singleLine = true, + ) + + Button( + onClick = { + if (url.isNotBlank() && url != "/") { + onAddServer(url) + url = "" + } + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = + if (url.isNotBlank()) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.placeholderText + }, + ), + ) { + Text(text = stringRes(id = R.string.add), color = Color.White) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt new file mode 100644 index 000000000..689ad5ce9 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt @@ -0,0 +1,288 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers.mediaServers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.Nip96MediaServers +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.SaveButton +import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.grayText + +// TODO: Implement UI for Media servers view. +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MediaServersListView( + onClose: () -> Unit, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val mediaServersViewModel: MediaServersViewModel = viewModel() + val mediaServersState by mediaServersViewModel.fileServers.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + mediaServersViewModel.load(accountViewModel.account) + } + + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.media_servers), + style = MaterialTheme.typography.titleLarge, + ) + } + }, + navigationIcon = { + CloseButton( + onPress = { + mediaServersViewModel.refresh() + onClose() + }, + ) + }, + actions = { + SaveButton( + onPost = { + mediaServersViewModel.saveFileServers() + onClose() + }, + isActive = true, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { padding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding( + start = 16.dp, + top = padding.calculateTopPadding(), + end = 16.dp, + bottom = padding.calculateBottomPadding(), + ), + verticalArrangement = Arrangement.spacedBy(5.dp, alignment = Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + stringRes(id = R.string.set_preferred_media_servers), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.grayText, + ) + + LazyColumn( + verticalArrangement = Arrangement.SpaceAround, + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = FeedPadding, + ) { + renderMediaServerList( + mediaServersState = mediaServersState, + onAddServer = { server -> + mediaServersViewModel.addServer(server) + }, + onDeleteServer = { + mediaServersViewModel.removeServer(serverUrl = it) + }, + ) + + Nip96MediaServers.DEFAULT.let { + item { + SettingsCategoryWithButton( + title = stringRes(id = R.string.built_in_media_servers_title), + description = stringRes(id = R.string.built_in_servers_description), + action = { + OutlinedButton( + onClick = { + mediaServersViewModel.addServerList(it.map { s -> s.baseUrl }) + }, + ) { + Text(text = stringRes(id = R.string.use_default_servers)) + } + }, + ) + } + itemsIndexed( + it, + key = { index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, server -> + MediaServerEntry( + serverEntry = server, + isAmethystDefault = true, + onAddOrDelete = { serverUrl -> + mediaServersViewModel.addServer(serverUrl) + }, + ) + } + } + } + } + } + } +} + +fun LazyListScope.renderMediaServerList( + mediaServersState: List, + onAddServer: (String) -> Unit, + onDeleteServer: (String) -> Unit, +) { + if (mediaServersState.isEmpty()) { + item { + Text( + text = stringRes(id = R.string.no_media_server_message), + modifier = DoubleVertPadding, + ) + } + } else { + itemsIndexed( + mediaServersState, + key = { index: Int, server: Nip96MediaServers.ServerName -> + server.baseUrl + }, + ) { index, entry -> + MediaServerEntry( + serverEntry = entry, + onAddOrDelete = { + onDeleteServer(it) + }, + ) + } + } + + item { + Spacer(modifier = StdVertSpacer) + MediaServerEditField { + onAddServer(it) + } + } +} + +@Composable +fun MediaServerEntry( + modifier: Modifier = Modifier, + serverEntry: Nip96MediaServers.ServerName, + isAmethystDefault: Boolean = false, + onAddOrDelete: (serverUrl: String) -> Unit, +) { + Row( + modifier = + modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, + ) { + Column( + modifier = + Modifier + .weight(1f), + ) { + serverEntry.let { + Text( + text = it.name.replaceFirstChar(Char::titlecase), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = StdVertSpacer) + Text( + text = it.baseUrl, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } + } + + Row( + horizontalArrangement = Arrangement.End, + ) { + IconButton( + onClick = { + onAddOrDelete(serverEntry.baseUrl) + }, + ) { + Icon( + imageVector = if (isAmethystDefault) Icons.Rounded.Add else Icons.Rounded.Delete, + contentDescription = + if (isAmethystDefault) { + stringRes(id = R.string.add_media_server) + } else { + stringRes(id = R.string.delete_media_server) + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt new file mode 100644 index 000000000..4de113b0b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers.mediaServers + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.Nip96MediaServers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.czeal.rfc3986.URIReference + +class MediaServersViewModel : ViewModel() { + lateinit var account: Account + + private val _fileServers = MutableStateFlow>(emptyList()) + val fileServers = _fileServers.asStateFlow() + private var isModified = false + + fun load(account: Account) { + this.account = account + refresh() + } + + fun refresh() { + isModified = false + _fileServers.update { + val obtainedFileServers = obtainFileServers() ?: emptyList() + obtainedFileServers.map { serverUrl -> + Nip96MediaServers + .ServerName( + URIReference.parse(serverUrl).host.value, + serverUrl, + ) + } + } + } + + fun addServerList(serverList: List) { + serverList.forEach { serverUrl -> + addServer(serverUrl) + } + } + + fun addServer(serverUrl: String) { + val normalizedUrl = + try { + URIReference.parse(serverUrl.trim()).normalize().toString() + } catch (e: Exception) { + serverUrl + } + val serverNameReference = + try { + URIReference.parse(normalizedUrl).host.value + } catch (e: Exception) { + normalizedUrl + } + val serverRef = Nip96MediaServers.ServerName(serverNameReference, normalizedUrl) + if (_fileServers.value.contains(serverRef)) { + return + } else { + _fileServers.update { + it.plus(serverRef) + } + } + isModified = true + } + + fun removeServer( + name: String = "", + serverUrl: String, + ) { + viewModelScope.launch { + val serverName = if (name.isNotBlank()) name else URIReference.parse(serverUrl).host.value + _fileServers.update { + it.minus( + Nip96MediaServers.ServerName(serverName, serverUrl), + ) + } + isModified = true + } + } + + fun removeAllServers() { + _fileServers.update { emptyList() } + isModified = true + } + + fun saveFileServers() { + if (isModified) { + viewModelScope.launch(Dispatchers.IO) { + val serverList = _fileServers.value.map { it.baseUrl } + account.sendFileServersList(serverList) + refresh() + } + } + } + + private fun obtainFileServers(): List? = account.getFileServersList()?.servers() +} From 593f817b6d3b3aa06494ad5349917ace1e3debb3 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sat, 29 Jun 2024 00:56:08 +0100 Subject: [PATCH 19/21] Try fix(again). --- .../com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index c0106ed89..3e0ea5087 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji From 7f3c7ea69c54a44adcae95772f39141e343283fc Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sat, 29 Jun 2024 01:13:16 +0100 Subject: [PATCH 20/21] Remove unnecessary files. --- .../mediaServers/MediaServerEditField.kt | 94 ------ .../mediaServers/MediaServersLIstView.kt | 288 ------------------ .../mediaServers/MediaServersViewModel.kt | 121 -------- 3 files changed, 503 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt deleted file mode 100644 index d504f824a..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServerEditField.kt +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.actions.mediaServers.mediaServers - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.placeholderText - -@Composable -fun MediaServerEditField( - modifier: Modifier = Modifier, - onAddServer: (String) -> Unit, -) { - var url by remember { mutableStateOf("") } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = - Arrangement.spacedBy( - Size10dp, - ), - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.add_a_relay)) }, - modifier = Modifier.weight(1f), - value = url, - onValueChange = { url = it }, - placeholder = { - Text( - text = "server.com", - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - }, - singleLine = true, - ) - - Button( - onClick = { - if (url.isNotBlank() && url != "/") { - onAddServer(url) - url = "" - } - }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = - if (url.isNotBlank()) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.placeholderText - }, - ), - ) { - Text(text = stringRes(id = R.string.add), color = Color.White) - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt deleted file mode 100644 index 689ad5ce9..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersLIstView.kt +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.actions.mediaServers.mediaServers - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.Nip96MediaServers -import com.vitorpamplona.amethyst.ui.actions.CloseButton -import com.vitorpamplona.amethyst.ui.actions.SaveButton -import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding -import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.amethyst.ui.theme.grayText - -// TODO: Implement UI for Media servers view. -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MediaServersListView( - onClose: () -> Unit, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val mediaServersViewModel: MediaServersViewModel = viewModel() - val mediaServersState by mediaServersViewModel.fileServers.collectAsStateWithLifecycle() - - LaunchedEffect(key1 = Unit) { - mediaServersViewModel.load(accountViewModel.account) - } - - Dialog( - onDismissRequest = onClose, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) { - Scaffold( - topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(id = R.string.media_servers), - style = MaterialTheme.typography.titleLarge, - ) - } - }, - navigationIcon = { - CloseButton( - onPress = { - mediaServersViewModel.refresh() - onClose() - }, - ) - }, - actions = { - SaveButton( - onPost = { - mediaServersViewModel.saveFileServers() - onClose() - }, - isActive = true, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - ) - }, - ) { padding -> - Column( - modifier = - Modifier - .fillMaxSize() - .padding( - start = 16.dp, - top = padding.calculateTopPadding(), - end = 16.dp, - bottom = padding.calculateBottomPadding(), - ), - verticalArrangement = Arrangement.spacedBy(5.dp, alignment = Alignment.Top), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - stringRes(id = R.string.set_preferred_media_servers), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.grayText, - ) - - LazyColumn( - verticalArrangement = Arrangement.SpaceAround, - horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = FeedPadding, - ) { - renderMediaServerList( - mediaServersState = mediaServersState, - onAddServer = { server -> - mediaServersViewModel.addServer(server) - }, - onDeleteServer = { - mediaServersViewModel.removeServer(serverUrl = it) - }, - ) - - Nip96MediaServers.DEFAULT.let { - item { - SettingsCategoryWithButton( - title = stringRes(id = R.string.built_in_media_servers_title), - description = stringRes(id = R.string.built_in_servers_description), - action = { - OutlinedButton( - onClick = { - mediaServersViewModel.addServerList(it.map { s -> s.baseUrl }) - }, - ) { - Text(text = stringRes(id = R.string.use_default_servers)) - } - }, - ) - } - itemsIndexed( - it, - key = { index: Int, server: Nip96MediaServers.ServerName -> - server.baseUrl - }, - ) { index, server -> - MediaServerEntry( - serverEntry = server, - isAmethystDefault = true, - onAddOrDelete = { serverUrl -> - mediaServersViewModel.addServer(serverUrl) - }, - ) - } - } - } - } - } - } -} - -fun LazyListScope.renderMediaServerList( - mediaServersState: List, - onAddServer: (String) -> Unit, - onDeleteServer: (String) -> Unit, -) { - if (mediaServersState.isEmpty()) { - item { - Text( - text = stringRes(id = R.string.no_media_server_message), - modifier = DoubleVertPadding, - ) - } - } else { - itemsIndexed( - mediaServersState, - key = { index: Int, server: Nip96MediaServers.ServerName -> - server.baseUrl - }, - ) { index, entry -> - MediaServerEntry( - serverEntry = entry, - onAddOrDelete = { - onDeleteServer(it) - }, - ) - } - } - - item { - Spacer(modifier = StdVertSpacer) - MediaServerEditField { - onAddServer(it) - } - } -} - -@Composable -fun MediaServerEntry( - modifier: Modifier = Modifier, - serverEntry: Nip96MediaServers.ServerName, - isAmethystDefault: Boolean = false, - onAddOrDelete: (serverUrl: String) -> Unit, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround, - ) { - Column( - modifier = - Modifier - .weight(1f), - ) { - serverEntry.let { - Text( - text = it.name.replaceFirstChar(Char::titlecase), - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(modifier = StdVertSpacer) - Text( - text = it.baseUrl, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.grayText, - ) - } - } - - Row( - horizontalArrangement = Arrangement.End, - ) { - IconButton( - onClick = { - onAddOrDelete(serverEntry.baseUrl) - }, - ) { - Icon( - imageVector = if (isAmethystDefault) Icons.Rounded.Add else Icons.Rounded.Delete, - contentDescription = - if (isAmethystDefault) { - stringRes(id = R.string.add_media_server) - } else { - stringRes(id = R.string.delete_media_server) - }, - ) - } - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt deleted file mode 100644 index 4de113b0b..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/mediaServers/MediaServersViewModel.kt +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.actions.mediaServers.mediaServers - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.Nip96MediaServers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.czeal.rfc3986.URIReference - -class MediaServersViewModel : ViewModel() { - lateinit var account: Account - - private val _fileServers = MutableStateFlow>(emptyList()) - val fileServers = _fileServers.asStateFlow() - private var isModified = false - - fun load(account: Account) { - this.account = account - refresh() - } - - fun refresh() { - isModified = false - _fileServers.update { - val obtainedFileServers = obtainFileServers() ?: emptyList() - obtainedFileServers.map { serverUrl -> - Nip96MediaServers - .ServerName( - URIReference.parse(serverUrl).host.value, - serverUrl, - ) - } - } - } - - fun addServerList(serverList: List) { - serverList.forEach { serverUrl -> - addServer(serverUrl) - } - } - - fun addServer(serverUrl: String) { - val normalizedUrl = - try { - URIReference.parse(serverUrl.trim()).normalize().toString() - } catch (e: Exception) { - serverUrl - } - val serverNameReference = - try { - URIReference.parse(normalizedUrl).host.value - } catch (e: Exception) { - normalizedUrl - } - val serverRef = Nip96MediaServers.ServerName(serverNameReference, normalizedUrl) - if (_fileServers.value.contains(serverRef)) { - return - } else { - _fileServers.update { - it.plus(serverRef) - } - } - isModified = true - } - - fun removeServer( - name: String = "", - serverUrl: String, - ) { - viewModelScope.launch { - val serverName = if (name.isNotBlank()) name else URIReference.parse(serverUrl).host.value - _fileServers.update { - it.minus( - Nip96MediaServers.ServerName(serverName, serverUrl), - ) - } - isModified = true - } - } - - fun removeAllServers() { - _fileServers.update { emptyList() } - isModified = true - } - - fun saveFileServers() { - if (isModified) { - viewModelScope.launch(Dispatchers.IO) { - val serverList = _fileServers.value.map { it.baseUrl } - account.sendFileServersList(serverList) - refresh() - } - } - } - - private fun obtainFileServers(): List? = account.getFileServersList()?.servers() -} From 2f276cb155380bd736118c6ca3dc0bea9a7ac6e3 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sat, 29 Jun 2024 21:13:17 +0100 Subject: [PATCH 21/21] Remove TODO, since a basic UI is already implemented. --- .../amethyst/ui/actions/mediaServers/MediaServersLIstView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt index 08672cb3c..0cc597e90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServersLIstView.kt @@ -65,7 +65,6 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText -// TODO: Implement UI for Media servers view. @OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaServersListView(