refactor: navigation
This commit is contained in:
parent
ab858ab59e
commit
85319ba874
|
@ -74,7 +74,9 @@ class WireGuardAutoTunnel : Application() {
|
|||
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
|
||||
}
|
||||
appStateRepository.getLocale()?.let {
|
||||
LocaleUtil.changeLocale(it)
|
||||
withContext(mainDispatcher) {
|
||||
LocaleUtil.changeLocale(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -222,24 +222,16 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
composable<Route.Config> {
|
||||
val args = it.toRoute<Route.Config>()
|
||||
ConfigScreen(
|
||||
appUiState,
|
||||
tunnelId = args.id,
|
||||
appViewModel = viewModel,
|
||||
)
|
||||
val config = appUiState.tunnels.firstOrNull { it.id == args.id }
|
||||
ConfigScreen(config, viewModel)
|
||||
}
|
||||
composable<Route.TunnelOptions> {
|
||||
val args = it.toRoute<Route.TunnelOptions>()
|
||||
OptionsScreen(
|
||||
tunnelId = args.id,
|
||||
appUiState = appUiState,
|
||||
appViewModel = viewModel,
|
||||
)
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
OptionsScreen(config, viewModel)
|
||||
}
|
||||
composable<Route.Lock> {
|
||||
PinLockScreen(
|
||||
appViewModel = viewModel,
|
||||
)
|
||||
PinLockScreen(viewModel)
|
||||
}
|
||||
composable<Route.Scanner> {
|
||||
ScannerScreen()
|
||||
|
@ -249,11 +241,13 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
composable<Route.SplitTunnel> {
|
||||
val args = it.toRoute<Route.SplitTunnel>()
|
||||
SplitTunnelScreen(appUiState, args.id, viewModel)
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
SplitTunnelScreen(config,viewModel)
|
||||
}
|
||||
composable<Route.TunnelAutoTunnel> {
|
||||
val args = it.toRoute<Route.SplitTunnel>()
|
||||
TunnelAutoTunnelScreen(appUiState, args.id)
|
||||
val args = it.toRoute<Route.TunnelOptions>()
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
TunnelAutoTunnelScreen(config, appUiState.settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||
|
@ -39,18 +39,17 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
|||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||
|
||||
@Composable
|
||||
fun OptionsScreen(appViewModel: AppViewModel, appUiState: AppUiState, tunnelId: Int) {
|
||||
fun OptionsScreen(tunnelConfig: TunnelConfig, appViewModel: AppViewModel) {
|
||||
val navController = LocalNavController.current
|
||||
val config = appUiState.tunnels.first { it.id == tunnelId }
|
||||
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(config.tunnelNetworks) {
|
||||
LaunchedEffect(tunnelConfig.tunnelNetworks) {
|
||||
currentText = ""
|
||||
}
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopNavBar(config.name)
|
||||
TopNavBar(tunnelConfig.name)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
|
@ -83,11 +82,11 @@ fun OptionsScreen(appViewModel: AppViewModel, appUiState: AppUiState, tunnelId:
|
|||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
config.isPrimaryTunnel,
|
||||
onClick = { appViewModel.onTogglePrimaryTunnel(config) },
|
||||
tunnelConfig.isPrimaryTunnel,
|
||||
onClick = { appViewModel.onTogglePrimaryTunnel(tunnelConfig) },
|
||||
)
|
||||
},
|
||||
onClick = { appViewModel.onTogglePrimaryTunnel(config) },
|
||||
onClick = { appViewModel.onTogglePrimaryTunnel(tunnelConfig) },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.Bolt,
|
||||
|
@ -104,10 +103,10 @@ fun OptionsScreen(appViewModel: AppViewModel, appUiState: AppUiState, tunnelId:
|
|||
)
|
||||
},
|
||||
onClick = {
|
||||
navController.navigate(Route.TunnelAutoTunnel(id = tunnelId))
|
||||
navController.navigate(Route.TunnelAutoTunnel(id = tunnelConfig.id))
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelId)) }
|
||||
ForwardButton { navController.navigate(Route.TunnelAutoTunnel(id = tunnelConfig.id)) }
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
|
@ -119,10 +118,10 @@ fun OptionsScreen(appViewModel: AppViewModel, appUiState: AppUiState, tunnelId:
|
|||
)
|
||||
},
|
||||
onClick = {
|
||||
navController.navigate(Route.Config(id = tunnelId))
|
||||
navController.navigate(Route.Config(id = tunnelConfig.id))
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.Config(id = tunnelId)) }
|
||||
ForwardButton { navController.navigate(Route.Config(id = tunnelConfig.id)) }
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
|
@ -134,10 +133,10 @@ fun OptionsScreen(appViewModel: AppViewModel, appUiState: AppUiState, tunnelId:
|
|||
)
|
||||
},
|
||||
onClick = {
|
||||
navController.navigate(Route.SplitTunnel(id = tunnelId))
|
||||
navController.navigate(Route.SplitTunnel(id = tunnelConfig.id))
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelId)) }
|
||||
ForwardButton { navController.navigate(Route.SplitTunnel(id = tunnelConfig.id)) }
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -58,6 +58,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
|
@ -77,7 +78,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|||
import org.amnezia.awg.crypto.KeyPair
|
||||
|
||||
@Composable
|
||||
fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: Int) {
|
||||
fun ConfigScreen(tunnelConfig: TunnelConfig?, appViewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
val snackbar = SnackbarController.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
|
@ -90,8 +91,6 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
|
|||
|
||||
val popBackStack by appViewModel.popBackStack.collectAsStateWithLifecycle(false)
|
||||
|
||||
val tunnelConfig = appUiState.tunnels.firstOrNull { it.id == tunnelId }
|
||||
|
||||
val configPair = Pair(tunnelConfig?.name ?: "", tunnelConfig?.toAmConfig())
|
||||
|
||||
var tunnelName by remember {
|
||||
|
@ -261,32 +260,6 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
|
|||
}
|
||||
}
|
||||
}
|
||||
// ConfigurationToggle(
|
||||
// stringResource(id = R.string.show_amnezia_properties),
|
||||
// checked = showAmneziaValues,
|
||||
// onCheckChanged = {
|
||||
// if (appUiState.settings.isKernelEnabled) {
|
||||
// snackbar.showMessage(context.getString(R.string.amnezia_kernel_message))
|
||||
// } else {
|
||||
// showAmneziaValues = it
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(id = R.string.show_scripts),
|
||||
// checked = showScripts,
|
||||
// onCheckChanged = { checked ->
|
||||
// if (appUiState.settings.isKernelEnabled) {
|
||||
// showScripts = checked
|
||||
// } else {
|
||||
// scope.launch {
|
||||
// appViewModel.requestRoot().onSuccess {
|
||||
// showScripts = checked
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
ConfigurationTextBox(
|
||||
value = tunnelName,
|
||||
onValueChange = { tunnelName = it },
|
||||
|
@ -297,7 +270,7 @@ fun ConfigScreen(appUiState: AppUiState, appViewModel: AppViewModel, tunnelId: I
|
|||
Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
val privateKeyEnabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated
|
||||
val privateKeyEnabled = (tunnelConfig == null) || isAuthenticated
|
||||
OutlinedTextField(
|
||||
textStyle = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
|
|
|
@ -48,7 +48,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SelectionItemButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
|
@ -62,7 +62,7 @@ import java.text.Collator
|
|||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewModel) {
|
||||
fun SplitTunnelScreen(tunnelConfig: TunnelConfig, viewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
|
@ -76,8 +76,6 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
|
|||
if (popBackStack) navController.popBackStack()
|
||||
}
|
||||
|
||||
val config = appUiState.tunnels.first { it.id == tunnelId }
|
||||
|
||||
val splitTunnelApps by viewModel.splitTunnelApps.collectAsStateWithLifecycle()
|
||||
|
||||
var proxyInterface by remember { mutableStateOf(InterfaceProxy()) }
|
||||
|
@ -87,7 +85,7 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
|
|||
val selectedPackages = remember { mutableStateListOf<String>() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
proxyInterface = InterfaceProxy.from(config.toAmConfig().`interface`)
|
||||
proxyInterface = InterfaceProxy.from(tunnelConfig.toAmConfig().`interface`)
|
||||
val pair = when {
|
||||
proxyInterface.excludedApplications.isNotEmpty() -> Pair(SplitOptions.EXCLUDE, proxyInterface.excludedApplications)
|
||||
proxyInterface.includedApplications.isNotEmpty() -> Pair(SplitOptions.INCLUDE, proxyInterface.includedApplications)
|
||||
|
@ -107,7 +105,7 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
|
|||
|
||||
LaunchedEffect(Unit) {
|
||||
// clean up any split tunnel packages for apps that were uninstalled
|
||||
viewModel.cleanUpUninstalledApps(config, splitTunnelApps.map { it.`package` })
|
||||
viewModel.cleanUpUninstalledApps(tunnelConfig, splitTunnelApps.map { it.`package` })
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
@ -127,7 +125,7 @@ fun SplitTunnelScreen(appUiState: AppUiState, tunnelId: Int, viewModel: AppViewM
|
|||
}
|
||||
SplitOptions.ALL -> Unit
|
||||
}
|
||||
viewModel.updateExistingTunnelConfig(config, `interface` = proxyInterface)
|
||||
viewModel.updateExistingTunnelConfig(tunnelConfig, `interface` = proxyInterface)
|
||||
}) {
|
||||
val icon = Icons.Outlined.Save
|
||||
Icon(
|
||||
|
|
|
@ -33,7 +33,8 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
|
@ -48,17 +49,16 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
|||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||
|
||||
@Composable
|
||||
fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunnelViewModel: TunnelAutoTunnelViewModel = hiltViewModel()) {
|
||||
val config = appUiState.tunnels.first { it.id == tunnelId }
|
||||
fun TunnelAutoTunnelScreen(tunnelConfig: TunnelConfig, settings: Settings, tunnelAutoTunnelViewModel: TunnelAutoTunnelViewModel = hiltViewModel()) {
|
||||
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(config.tunnelNetworks) {
|
||||
LaunchedEffect(tunnelConfig.tunnelNetworks) {
|
||||
currentText = ""
|
||||
}
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopNavBar(config.name)
|
||||
TopNavBar(tunnelConfig.name)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
|
@ -92,11 +92,11 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
config.isMobileDataTunnel,
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(config) },
|
||||
tunnelConfig.isMobileDataTunnel,
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConfig) },
|
||||
)
|
||||
},
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(config) },
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsMobileDataTunnel(tunnelConfig) },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.SettingsEthernet,
|
||||
|
@ -114,11 +114,11 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
config.isEthernetTunnel,
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(config) },
|
||||
tunnelConfig.isEthernetTunnel,
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
|
||||
)
|
||||
},
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(config) },
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.NetworkPing,
|
||||
|
@ -130,27 +130,27 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = config.isPingEnabled,
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(config) },
|
||||
checked = tunnelConfig.isPingEnabled,
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(tunnelConfig) },
|
||||
)
|
||||
},
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(config) },
|
||||
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(tunnelConfig) },
|
||||
),
|
||||
),
|
||||
)
|
||||
if (config.isPingEnabled || appUiState.settings.isPingEnabled) {
|
||||
if (tunnelConfig.isPingEnabled || settings.isPingEnabled) {
|
||||
add(
|
||||
SelectionItem(
|
||||
title = {},
|
||||
description = {
|
||||
SubmitConfigurationTextBox(
|
||||
config.pingIp,
|
||||
tunnelConfig.pingIp,
|
||||
stringResource(R.string.set_custom_ping_ip),
|
||||
stringResource(R.string.default_ping_ip),
|
||||
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
|
||||
onSubmit = {
|
||||
tunnelAutoTunnelViewModel.saveTunnelChanges(
|
||||
config.copy(pingIp = it.ifBlank { null }),
|
||||
tunnelConfig.copy(pingIp = it.ifBlank { null }),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -158,7 +158,7 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
|
||||
}
|
||||
SubmitConfigurationTextBox(
|
||||
config.pingInterval?.let { (it / 1000).toString() },
|
||||
tunnelConfig.pingInterval?.let { (it / 1000).toString() },
|
||||
stringResource(R.string.set_custom_ping_internal),
|
||||
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
@ -168,12 +168,12 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
isErrorValue = ::isSecondsError,
|
||||
onSubmit = {
|
||||
tunnelAutoTunnelViewModel.saveTunnelChanges(
|
||||
config.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
|
||||
tunnelConfig.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
|
||||
)
|
||||
},
|
||||
)
|
||||
SubmitConfigurationTextBox(
|
||||
config.pingCooldown?.let { (it / 1000).toString() },
|
||||
tunnelConfig.pingCooldown?.let { (it / 1000).toString() },
|
||||
stringResource(R.string.set_custom_ping_cooldown),
|
||||
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
@ -182,7 +182,7 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
isErrorValue = ::isSecondsError,
|
||||
onSubmit = {
|
||||
tunnelAutoTunnelViewModel.saveTunnelChanges(
|
||||
config.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
|
||||
tunnelConfig.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -229,13 +229,13 @@ fun TunnelAutoTunnelScreen(appUiState: AppUiState, tunnelId: Int, tunnelAutoTunn
|
|||
},
|
||||
description = {
|
||||
TrustedNetworkTextBox(
|
||||
config.tunnelNetworks,
|
||||
onDelete = { tunnelAutoTunnelViewModel.onDeleteRunSSID(it, config) },
|
||||
tunnelConfig.tunnelNetworks,
|
||||
onDelete = { tunnelAutoTunnelViewModel.onDeleteRunSSID(it, tunnelConfig) },
|
||||
currentText = currentText,
|
||||
onSave = { tunnelAutoTunnelViewModel.onSaveRunSSID(it, config) },
|
||||
onSave = { tunnelAutoTunnelViewModel.onSaveRunSSID(it, tunnelConfig) },
|
||||
onValueChange = { currentText = it },
|
||||
supporting = {
|
||||
if (appUiState.settings.isWildcardsEnabled) {
|
||||
if (settings.isWildcardsEnabled) {
|
||||
WildcardsLabel()
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@ coreKtx = "1.15.0"
|
|||
datastorePreferences = "1.1.1"
|
||||
desugar_jdk_libs = "2.1.4"
|
||||
espressoCore = "3.6.1"
|
||||
hiltAndroid = "2.53"
|
||||
hiltAndroid = "2.54"
|
||||
hiltNavigationCompose = "1.2.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.7.3"
|
||||
|
|
Loading…
Reference in New Issue