diff --git a/common-core/src/main/kotlin/com/blacksquircle/ui/core/extensions/JsonExtensions.kt b/common-core/src/main/kotlin/com/blacksquircle/ui/core/extensions/JsonExtensions.kt index 65183c512..ad86cb647 100644 --- a/common-core/src/main/kotlin/com/blacksquircle/ui/core/extensions/JsonExtensions.kt +++ b/common-core/src/main/kotlin/com/blacksquircle/ui/core/extensions/JsonExtensions.kt @@ -16,11 +16,15 @@ package com.blacksquircle.ui.core.extensions +import android.net.Uri import com.google.gson.Gson -import java.net.URLDecoder -import java.net.URLEncoder -fun String.encodeUrl(encoding: String = "UTF-8"): String = URLEncoder.encode(this, encoding) -fun String.decodeUrl(encoding: String = "UTF-8"): String = URLDecoder.decode(this, encoding) +fun String.encodeUri(): String = Uri.encode(this) +fun String.decodeUri(): String = Uri.decode(this) -fun Gson.toJsonEncoded(any: Any): String = toJson(any).encodeUrl() \ No newline at end of file +fun Any.toJsonEncoded(): String { + return Gson().toJson(this).encodeUri() +} +inline fun String.fromJsonEncoded(): T { + return Gson().fromJson(this.decodeUri(), T::class.java) +} \ No newline at end of file diff --git a/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/CloseModifiedDialog.kt b/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/CloseModifiedDialog.kt index d894bd253..33ea24d70 100644 --- a/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/CloseModifiedDialog.kt +++ b/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/CloseModifiedDialog.kt @@ -22,6 +22,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import com.blacksquircle.ui.core.extensions.decodeUri import com.blacksquircle.ui.feature.editor.R import com.blacksquircle.ui.feature.editor.ui.mvi.EditorIntent import com.blacksquircle.ui.feature.editor.ui.viewmodel.EditorViewModel @@ -35,7 +36,7 @@ class CloseModifiedDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialog.Builder(requireContext()) - .setTitle(navArgs.fileName) + .setTitle(navArgs.fileName.decodeUri()) .setMessage(R.string.dialog_message_close_tab) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.action_close) { _, _ -> diff --git a/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/ForceSyntaxDialog.kt b/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/ForceSyntaxDialog.kt index 8bccea852..7d27d6a7b 100644 --- a/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/ForceSyntaxDialog.kt +++ b/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/dialog/ForceSyntaxDialog.kt @@ -22,6 +22,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import com.blacksquircle.ui.core.extensions.decodeUri import com.blacksquircle.ui.core.extensions.showToast import com.blacksquircle.ui.feature.editor.R import com.blacksquircle.ui.feature.editor.ui.mvi.EditorIntent @@ -39,7 +40,10 @@ class ForceSyntaxDialog : DialogFragment() { val langEntries = resources.getStringArray(R.array.language_name) return AlertDialog.Builder(requireContext()) .setTitle(R.string.dialog_title_force_syntax) - .setSingleChoiceItems(langNames, langEntries.indexOf(navArgs.languageName)) { _, which -> + .setSingleChoiceItems( + langNames, + langEntries.indexOf(navArgs.languageName.decodeUri()) + ) { _, which -> val intent = EditorIntent.ForceSyntaxHighlighting(langEntries[which]) viewModel.obtainEvent(intent) requireContext().showToast(text = langNames[which]) diff --git a/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/navigation/EditorScreen.kt b/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/navigation/EditorScreen.kt index 0ec1c0e23..de4321690 100644 --- a/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/navigation/EditorScreen.kt +++ b/feature-editor/impl/src/main/kotlin/com/blacksquircle/ui/feature/editor/ui/navigation/EditorScreen.kt @@ -16,16 +16,16 @@ package com.blacksquircle.ui.feature.editor.ui.navigation -import com.blacksquircle.ui.core.extensions.encodeUrl +import com.blacksquircle.ui.core.extensions.encodeUri import com.blacksquircle.ui.core.navigation.Screen sealed class EditorScreen(route: String) : Screen(route) { class ForceSyntaxDialog(languageName: String) : EditorScreen( - route = "blacksquircle://editor/syntax?languageName=${languageName.encodeUrl()}", + route = "blacksquircle://editor/syntax?languageName=${languageName.encodeUri()}", ) class CloseModifiedDialog(position: Int, fileName: String) : EditorScreen( - route = "blacksquircle://editor/close?position=$position&fileName=${fileName.encodeUrl()}", + route = "blacksquircle://editor/close?position=$position&fileName=${fileName.encodeUri()}", ) data object GotoLine : EditorScreen("blacksquircle://editor/goto") diff --git a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/DeleteDialog.kt b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/DeleteDialog.kt index 5e682f91c..6e7fb9596 100644 --- a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/DeleteDialog.kt +++ b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/DeleteDialog.kt @@ -23,6 +23,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.blacksquircle.ui.core.extensions.decodeUri import com.blacksquircle.ui.feature.explorer.R import com.blacksquircle.ui.feature.explorer.ui.mvi.ExplorerIntent import com.blacksquircle.ui.feature.explorer.ui.viewmodel.ExplorerViewModel @@ -42,7 +43,7 @@ class DeleteDialog : DialogFragment() { val dialogTitle = if (isMultiDelete) { getString(R.string.dialog_title_multi_delete) } else { - navArgs.fileName + navArgs.fileName.decodeUri() } val dialogMessage = if (isMultiDelete) { diff --git a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/PropertiesDialog.kt b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/PropertiesDialog.kt index cc7b53974..dcb27426c 100644 --- a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/PropertiesDialog.kt +++ b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/PropertiesDialog.kt @@ -21,6 +21,7 @@ import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.navigation.fragment.navArgs +import com.blacksquircle.ui.core.extensions.fromJsonEncoded import com.blacksquircle.ui.feature.explorer.R import com.blacksquircle.ui.feature.explorer.data.utils.toReadableDate import com.blacksquircle.ui.feature.explorer.data.utils.toReadableSize @@ -37,8 +38,7 @@ class PropertiesDialog : DialogFragment() { private val navArgs by navArgs() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val fileModel = Gson().fromJson(navArgs.data, FileModel::class.java) // FIXME - + val fileModel = navArgs.data.fromJsonEncoded() val readableSize = fileModel.size.toReadableSize() val readableDate = fileModel.lastModified .toReadableDate(getString(R.string.properties_date_format)) diff --git a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/RenameDialog.kt b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/RenameDialog.kt index c5f8ff206..8a81a9389 100644 --- a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/RenameDialog.kt +++ b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/dialog/RenameDialog.kt @@ -23,6 +23,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.blacksquircle.ui.core.extensions.decodeUri import com.blacksquircle.ui.feature.explorer.R import com.blacksquircle.ui.feature.explorer.databinding.DialogRenameBinding import com.blacksquircle.ui.feature.explorer.ui.mvi.ExplorerIntent @@ -39,7 +40,7 @@ class RenameDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val binding = DialogRenameBinding.inflate(layoutInflater) - binding.input.setText(navArgs.fileName) + binding.input.setText(navArgs.fileName.decodeUri()) return AlertDialog.Builder(requireContext()) .setTitle(R.string.dialog_title_rename) diff --git a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/navigation/ExplorerScreen.kt b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/navigation/ExplorerScreen.kt index 36ec5e493..eb653ea8c 100644 --- a/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/navigation/ExplorerScreen.kt +++ b/feature-explorer/impl/src/main/kotlin/com/blacksquircle/ui/feature/explorer/ui/navigation/ExplorerScreen.kt @@ -16,7 +16,7 @@ package com.blacksquircle.ui.feature.explorer.ui.navigation -import com.blacksquircle.ui.core.extensions.encodeUrl +import com.blacksquircle.ui.core.extensions.encodeUri import com.blacksquircle.ui.core.extensions.toJsonEncoded import com.blacksquircle.ui.core.navigation.Screen import com.blacksquircle.ui.feature.explorer.data.utils.Operation @@ -27,16 +27,16 @@ import com.google.gson.Gson sealed class ExplorerScreen(route: String) : Screen(route) { class DeleteDialog(fileName: String, fileCount: Int) : ExplorerScreen( - route = "blacksquircle://explorer/delete?fileName=${fileName.encodeUrl()}&fileCount=$fileCount", + route = "blacksquircle://explorer/delete?fileName=${fileName.encodeUri()}&fileCount=$fileCount", ) class RenameDialog(fileName: String) : ExplorerScreen( - route = "blacksquircle://explorer/rename?fileName=${fileName.encodeUrl()}", + route = "blacksquircle://explorer/rename?fileName=${fileName.encodeUri()}", ) class ProgressDialog(totalCount: Int, operation: Operation) : ExplorerScreen( route = "blacksquircle://explorer/progress?totalCount=$totalCount&operation=${operation.value}", ) class PropertiesDialog(fileModel: FileModel) : ExplorerScreen( - route = "blacksquircle://explorer/properties?data=${Gson().toJsonEncoded(fileModel)}", + route = "blacksquircle://explorer/properties?data=${fileModel.toJsonEncoded()}", ) class AuthDialog(authMethod: AuthMethod) : ExplorerScreen( route = "blacksquircle://explorer/authenticate?authMethod=${authMethod.value}" diff --git a/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/dialog/ServerDialog.kt b/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/dialog/ServerDialog.kt index 47394f6e0..10d816b7f 100644 --- a/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/dialog/ServerDialog.kt +++ b/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/dialog/ServerDialog.kt @@ -58,7 +58,7 @@ internal class ServerDialog : DialogFragment() { when (result) { is ContractResult.Success -> { val filePath = context?.extractFilePath(result.uri) - viewModel.onKeyFileChosen(filePath.orEmpty()) + viewModel.onKeyFileSelected(filePath.orEmpty()) } is ContractResult.Canceled -> Unit } diff --git a/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/navigation/ServersScreen.kt b/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/navigation/ServersScreen.kt index e894f4925..3f7ab710c 100644 --- a/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/navigation/ServersScreen.kt +++ b/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/navigation/ServersScreen.kt @@ -24,6 +24,6 @@ import com.google.gson.Gson internal sealed class ServersScreen(route: String) : Screen(route) { class EditServer(serverConfig: ServerConfig) : ServersScreen( - route = "blacksquircle://settings/cloud/edit?data=${Gson().toJsonEncoded(serverConfig)}", + route = "blacksquircle://settings/cloud/edit?data=${serverConfig.toJsonEncoded()}", ) } \ No newline at end of file diff --git a/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/viewmodel/ServerViewModel.kt b/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/viewmodel/ServerViewModel.kt index a746a4513..9e6de9b29 100644 --- a/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/viewmodel/ServerViewModel.kt +++ b/feature-servers/impl/src/main/kotlin/com/blacksquircle/ui/feature/servers/ui/viewmodel/ServerViewModel.kt @@ -18,6 +18,7 @@ package com.blacksquircle.ui.feature.servers.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.blacksquircle.ui.core.extensions.fromJsonEncoded import com.blacksquircle.ui.core.mvi.ViewEvent import com.blacksquircle.ui.feature.servers.ui.dialog.ServerViewState import com.blacksquircle.ui.feature.servers.ui.dialog.internal.PassphraseAction @@ -111,7 +112,7 @@ internal class ServerViewModel @AssistedInject constructor( } } - fun onKeyFileChosen(filePath: String) { + fun onKeyFileSelected(filePath: String) { _viewState.update { it.copy(privateKey = filePath) } @@ -179,7 +180,7 @@ internal class ServerViewModel @AssistedInject constructor( private fun initialViewState(): ServerViewState { return if (isEditMode) { - val serverConfig = Gson().fromJson(serverData, ServerConfig::class.java) + val serverConfig = serverData!!.fromJsonEncoded() ServerViewState( isEditMode = true, uuid = serverConfig.uuid, diff --git a/feature-shortcuts/impl/src/main/kotlin/com/blacksquircle/ui/feature/shortcuts/ui/fragment/ShortcutsScreen.kt b/feature-shortcuts/impl/src/main/kotlin/com/blacksquircle/ui/feature/shortcuts/ui/fragment/ShortcutsScreen.kt index 34e7a275c..75de4a335 100644 --- a/feature-shortcuts/impl/src/main/kotlin/com/blacksquircle/ui/feature/shortcuts/ui/fragment/ShortcutsScreen.kt +++ b/feature-shortcuts/impl/src/main/kotlin/com/blacksquircle/ui/feature/shortcuts/ui/fragment/ShortcutsScreen.kt @@ -156,8 +156,10 @@ private fun ShortcutsScreen( onClick = { onKeyClicked(keybinding) }, ) } - item { - HorizontalDivider() + if (keyGroup != KeyGroup.TOOLS) { + item { + HorizontalDivider() + } } } } diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/NewThemeFragment.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/EditThemeFragment.kt similarity index 94% rename from feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/NewThemeFragment.kt rename to feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/EditThemeFragment.kt index 9d59aea97..e63cca52d 100644 --- a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/NewThemeFragment.kt +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/EditThemeFragment.kt @@ -35,13 +35,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @AndroidEntryPoint -internal class NewThemeFragment : Fragment() { +internal class EditThemeFragment : Fragment() { private val viewModel by hiltNavGraphViewModels(R.id.themes_graph) - // private val binding by viewBinding(FragmentNewThemeBinding::bind) + // private val binding by viewBinding(FragmentEditThemeBinding::bind) private val navController by lazy { findNavController() } - private val navArgs by navArgs() + private val navArgs by navArgs() private val openFileContract = OpenFileContract(this) { result -> /*when (result) { is ContractResult.Success -> viewModel.obtainEvent(ThemeIntent.ImportTheme(result.uri)) @@ -65,7 +65,7 @@ internal class NewThemeFragment : Fragment() { viewModel.newThemeState.flowWithLifecycle(viewLifecycleOwner.lifecycle) .onEach { state -> when (state) { - is NewThemeViewState.MetaData -> { + is EditThemeViewState.MetaData -> { /*val name = binding.textInputThemeName.text.toString() val author = binding.textInputThemeAuthor.text.toString() val description = binding.textInputThemeDescription.text.toString() diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/NewThemeViewState.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/EditThemeViewState.kt similarity index 92% rename from feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/NewThemeViewState.kt rename to feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/EditThemeViewState.kt index 333f95f18..4ff869c5e 100644 --- a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/NewThemeViewState.kt +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/EditThemeViewState.kt @@ -20,10 +20,10 @@ import com.blacksquircle.ui.core.mvi.ViewState import com.blacksquircle.ui.feature.themes.domain.model.Meta import com.blacksquircle.ui.feature.themes.domain.model.PropertyItem -sealed class NewThemeViewState : ViewState() { +sealed class EditThemeViewState : ViewState() { data class MetaData( val meta: Meta, val properties: List, - ) : NewThemeViewState() + ) : EditThemeViewState() } \ No newline at end of file diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesFragment.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesFragment.kt index d70fb51af..41100a5ad 100644 --- a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesFragment.kt +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesFragment.kt @@ -33,6 +33,7 @@ import com.blacksquircle.ui.core.extensions.navigateTo import com.blacksquircle.ui.core.extensions.showToast import com.blacksquircle.ui.core.mvi.ViewEvent import com.blacksquircle.ui.ds.SquircleTheme +import com.blacksquircle.ui.feature.themes.ui.navigation.ThemesViewEvent import com.blacksquircle.ui.feature.themes.ui.viewmodel.ThemesViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn @@ -45,10 +46,7 @@ internal class ThemesFragment : Fragment() { private val navController by lazy { findNavController() } private val exportThemeContract = CreateFileContract(this) { result -> when (result) { - is ContractResult.Success -> { - // viewModel.obtainEvent(ThemeIntent.ExportTheme(themeModel, result.uri)) - } - + is ContractResult.Success -> viewModel.onExportFileSelected(result.uri) is ContractResult.Canceled -> Unit } } @@ -80,6 +78,12 @@ internal class ThemesFragment : Fragment() { is ViewEvent.Toast -> context?.showToast(text = event.message) is ViewEvent.Navigation -> navController.navigateTo(event.screen) is ViewEvent.PopBackStack -> navController.popBackStack() + is ThemesViewEvent.ChooseExportFile -> { + exportThemeContract.launch( + event.themeName, + CreateFileContract.JSON + ) + } } } .launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesScreen.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesScreen.kt index 32c991d59..32e4097b1 100644 --- a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesScreen.kt +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/fragment/ThemesScreen.kt @@ -81,9 +81,11 @@ internal fun ThemesScreen(viewModel: ThemesViewModel) { onCodePreviewChanged = viewModel::onCodePreviewChanged, onQueryChanged = viewModel::onQueryChanged, onClearQueryClicked = viewModel::onClearQueryClicked, - onCreateClicked = {}, - onSelectClicked = {}, - onRemoveClicked = {}, + onCreateClicked = viewModel::onCreateClicked, + onSelectClicked = viewModel::onSelectClicked, + onExportClicked = viewModel::onExportClicked, + onEditClicked = viewModel::onEditClicked, + onRemoveClicked = viewModel::onRemoveClicked, ) } @@ -96,6 +98,8 @@ private fun ThemesScreen( onClearQueryClicked: () -> Unit, onCreateClicked: () -> Unit, onSelectClicked: (ThemeModel) -> Unit, + onExportClicked: (ThemeModel) -> Unit, + onEditClicked: (ThemeModel) -> Unit, onRemoveClicked: (ThemeModel) -> Unit, ) { val scrollState = rememberLazyGridState() @@ -209,8 +213,8 @@ private fun ThemesScreen( fontPath = viewState.fontPath, codeSample = viewState.preview.codeSample, onSelectClicked = { onSelectClicked(theme) }, - onExportClicked = {}, - onEditClicked = {}, + onExportClicked = { onExportClicked(theme) }, + onEditClicked = { onEditClicked(theme) }, onRemoveClicked = { onRemoveClicked(theme) }, modifier = Modifier.animateItem(), ) @@ -244,6 +248,8 @@ private fun ThemesScreenPreview() { onClearQueryClicked = {}, onCreateClicked = {}, onSelectClicked = {}, + onExportClicked = {}, + onEditClicked = {}, onRemoveClicked = {}, ) } diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesScreen.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesScreen.kt index 4c8c2674e..e21476688 100644 --- a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesScreen.kt +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesScreen.kt @@ -16,7 +16,7 @@ package com.blacksquircle.ui.feature.themes.ui.navigation -import com.blacksquircle.ui.core.extensions.encodeUrl +import com.blacksquircle.ui.core.extensions.encodeUri import com.blacksquircle.ui.core.navigation.Screen internal sealed class ThemesScreen(route: String) : Screen(route) { @@ -26,6 +26,6 @@ internal sealed class ThemesScreen(route: String) : Screen(route) { class Update(uuid: String?) : ThemesScreen("blacksquircle://themes/update?uuid=$uuid") class ChooseColor(key: String, value: String) : ThemesScreen( - route = "blacksquircle://themes/choosecolor?key=$key&value=${value.encodeUrl()}" + route = "blacksquircle://themes/choosecolor?key=$key&value=${value.encodeUri()}" ) } \ No newline at end of file diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesViewEvent.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesViewEvent.kt new file mode 100644 index 000000000..f7b6e2fb1 --- /dev/null +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/navigation/ThemesViewEvent.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Squircle CE contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blacksquircle.ui.feature.themes.ui.navigation + +import com.blacksquircle.ui.core.mvi.ViewEvent + +internal sealed class ThemesViewEvent : ViewEvent() { + data class ChooseExportFile(val themeName: String) : ThemesViewEvent() +} \ No newline at end of file diff --git a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/viewmodel/ThemesViewModel.kt b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/viewmodel/ThemesViewModel.kt index a0a5e9f8c..5c2e8714f 100644 --- a/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/viewmodel/ThemesViewModel.kt +++ b/feature-themes/impl/src/main/kotlin/com/blacksquircle/ui/feature/themes/ui/viewmodel/ThemesViewModel.kt @@ -31,9 +31,10 @@ import com.blacksquircle.ui.feature.themes.domain.model.Property import com.blacksquircle.ui.feature.themes.domain.model.PropertyItem import com.blacksquircle.ui.feature.themes.domain.model.ThemeModel import com.blacksquircle.ui.feature.themes.domain.repository.ThemesRepository -import com.blacksquircle.ui.feature.themes.ui.fragment.NewThemeViewState +import com.blacksquircle.ui.feature.themes.ui.fragment.EditThemeViewState import com.blacksquircle.ui.feature.themes.ui.fragment.ThemesViewState import com.blacksquircle.ui.feature.themes.ui.navigation.ThemesScreen +import com.blacksquircle.ui.feature.themes.ui.navigation.ThemesViewEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -56,12 +57,13 @@ internal class ThemesViewModel @Inject constructor( val viewState: StateFlow = _viewState.asStateFlow() // TODO move to other viewmodel - private val _newThemeState = MutableStateFlow(NewThemeViewState.MetaData(Meta(), emptyList())) - val newThemeState: StateFlow = _newThemeState.asStateFlow() + private val _newThemeState = MutableStateFlow(EditThemeViewState.MetaData(Meta(), emptyList())) + val newThemeState: StateFlow = _newThemeState.asStateFlow() private val _viewEvent = Channel(Channel.BUFFERED) val viewEvent: Flow = _viewEvent.receiveAsFlow() + private var pendingExport: ThemeModel? = null private var currentJob: Job? = null init { @@ -97,56 +99,53 @@ internal class ThemesViewModel @Inject constructor( } } - private fun loadThemes(query: String = "") { - currentJob?.cancel() - currentJob = viewModelScope.launch { - try { - _viewState.update { - it.copy(isLoading = true) - } - val themes = themesRepository.loadThemes(query) - delay(300L) // too fast, avoid blinking - _viewState.update { - it.copy( - themes = themes, - fontPath = settingsManager.fontType, - isLoading = false, - ) - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Timber.e(e, e.message) - _viewState.update { - it.copy(isLoading = false) - } - _viewEvent.send( - ViewEvent.Toast(stringProvider.getString(UiR.string.common_error_occurred)), - ) - } + fun onCreateClicked() { + viewModelScope.launch { + val screen = ThemesScreen.Create + _viewEvent.send(ViewEvent.Navigation(screen)) } } - private fun importTheme(fileUri: Uri) { + fun onSelectClicked(themeModel: ThemeModel) { viewModelScope.launch { try { - val themeModel = themesRepository.importTheme(fileUri) - loadProperties(themeModel) + themesRepository.selectTheme(themeModel) + _viewEvent.send( + ViewEvent.Toast( + stringProvider.getString( + R.string.message_selected, + themeModel.name, + ), + ), + ) } catch (e: Exception) { Timber.e(e, e.message) _viewEvent.send( ViewEvent.Toast( - stringProvider.getString(R.string.message_theme_syntax_exception), + stringProvider.getString(UiR.string.common_error_occurred), ), ) } } } - private fun exportTheme(themeModel: ThemeModel, fileUri: Uri) { + fun onExportClicked(themeModel: ThemeModel) { + viewModelScope.launch { + pendingExport = themeModel + val themeName = themeModel.name + ".json" + _viewEvent.send(ThemesViewEvent.ChooseExportFile(themeName)) + } + } + + fun onExportFileSelected(fileUri: Uri) { viewModelScope.launch { try { - themesRepository.exportTheme(themeModel, fileUri) + if (pendingExport == null) { + throw IllegalStateException("Theme is not selected") + } + themesRepository.exportTheme(pendingExport!!, fileUri) + pendingExport = null + _viewEvent.send( ViewEvent.Toast(stringProvider.getString(R.string.message_saved)), ) @@ -161,18 +160,23 @@ internal class ThemesViewModel @Inject constructor( } } - private fun selectTheme(themeModel: ThemeModel) { + fun onEditClicked(themeModel: ThemeModel) { + + } + + fun onRemoveClicked(themeModel: ThemeModel) { viewModelScope.launch { try { - themesRepository.selectTheme(themeModel) + themesRepository.removeTheme(themeModel) _viewEvent.send( ViewEvent.Toast( stringProvider.getString( - R.string.message_selected, + R.string.message_theme_removed, themeModel.name, ), ), ) + loadThemes() } catch (e: Exception) { Timber.e(e, e.message) _viewEvent.send( @@ -184,24 +188,46 @@ internal class ThemesViewModel @Inject constructor( } } - private fun removeTheme(themeModel: ThemeModel) { - viewModelScope.launch { + private fun loadThemes(query: String = "") { + currentJob?.cancel() + currentJob = viewModelScope.launch { try { - themesRepository.removeTheme(themeModel) + _viewState.update { + it.copy(isLoading = true) + } + val themes = themesRepository.loadThemes(query) + delay(300L) // too fast, avoid blinking + _viewState.update { + it.copy( + themes = themes, + fontPath = settingsManager.fontType, + isLoading = false, + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.e(e, e.message) + _viewState.update { + it.copy(isLoading = false) + } _viewEvent.send( - ViewEvent.Toast( - stringProvider.getString( - R.string.message_theme_removed, - themeModel.name, - ), - ), + ViewEvent.Toast(stringProvider.getString(UiR.string.common_error_occurred)), ) - loadThemes() + } + } + } + + private fun importTheme(fileUri: Uri) { + viewModelScope.launch { + try { + val themeModel = themesRepository.importTheme(fileUri) + loadProperties(themeModel) } catch (e: Exception) { Timber.e(e, e.message) _viewEvent.send( ViewEvent.Toast( - stringProvider.getString(UiR.string.common_error_occurred), + stringProvider.getString(R.string.message_theme_syntax_exception), ), ) } @@ -258,7 +284,7 @@ internal class ThemesViewModel @Inject constructor( } private fun onThemeNameChanged(name: String) { - val state = newThemeState.value as? NewThemeViewState.MetaData + val state = newThemeState.value as? EditThemeViewState.MetaData if (state != null) { _newThemeState.value = state.copy( meta = state.meta.copy(name = name), @@ -267,7 +293,7 @@ internal class ThemesViewModel @Inject constructor( } private fun onThemeAuthorChanged(author: String) { - val state = newThemeState.value as? NewThemeViewState.MetaData + val state = newThemeState.value as? EditThemeViewState.MetaData if (state != null) { _newThemeState.value = state.copy( meta = state.meta.copy(author = author), @@ -276,7 +302,7 @@ internal class ThemesViewModel @Inject constructor( } private fun onThemeDescriptionChanged(description: String) { - val state = newThemeState.value as? NewThemeViewState.MetaData + val state = newThemeState.value as? EditThemeViewState.MetaData if (state != null) { _newThemeState.value = state.copy( meta = state.meta.copy(description = description), @@ -285,7 +311,7 @@ internal class ThemesViewModel @Inject constructor( } private fun onThemeColorChanged(property: PropertyItem) { - val state = newThemeState.value as? NewThemeViewState.MetaData + val state = newThemeState.value as? EditThemeViewState.MetaData if (state != null) { _newThemeState.value = state.copy( properties = state.properties.map { propertyItem -> @@ -300,7 +326,7 @@ internal class ThemesViewModel @Inject constructor( } private fun loadProperties(themeModel: ThemeModel) { - _newThemeState.value = NewThemeViewState.MetaData( + _newThemeState.value = EditThemeViewState.MetaData( meta = Meta( uuid = themeModel.uuid, name = themeModel.name, diff --git a/feature-themes/impl/src/main/res/navigation/themes_graph.xml b/feature-themes/impl/src/main/res/navigation/themes_graph.xml index bbfd2a91b..bce713d0e 100644 --- a/feature-themes/impl/src/main/res/navigation/themes_graph.xml +++ b/feature-themes/impl/src/main/res/navigation/themes_graph.xml @@ -26,13 +26,13 @@ android:label="@string/label_themes"> + android:id="@+id/to_editThemeFragment" + app:destination="@id/editThemeFragment" />