From dc79779a544d24977f02d6de2e9ff8d40201f226 Mon Sep 17 00:00:00 2001 From: Aleksey Ivanovsky Date: Thu, 14 Aug 2025 21:07:09 +0200 Subject: [PATCH] [ANDROID] Add Export and Share buttons to group menu --- .../android/di/AndroidAppModule.kt | 4 + .../domain/usecase/CreateExportUrlUseCase.kt | 15 ++++ .../domain/usecase/CreateGroupUrlUseCase.kt | 15 ++++ .../android/presentation/MainActivity.kt | 32 ++++++- .../core/compose/AppDropdownField.kt | 4 +- .../presentation/core/compose/AppTextField.kt | 10 +-- .../presentation/core/compose/TopBar.kt | 8 +- .../compose/cells/ui/BottomSheetHeaderCell.kt | 4 +- .../core/compose/cells/ui/MenuCell.kt | 4 +- .../core/compose/navigation/Navigator.kt | 11 ++- .../core/compose/navigation/Router.kt | 9 ++ .../core/compose/theme/AppIcon.kt | 57 ++++++++++++ .../core/compose/theme/AppIcons.kt | 48 ---------- .../android/presentation/core/event/Event.kt | 48 ++++++++++ .../ExpenseDetailsDialogCellFactory.kt | 8 +- .../ExpenseDetailsDialogScreen.kt | 6 +- .../dialogs/menuDialog/MenuDialogViewModel.kt | 3 +- .../dialogs/menuDialog/model/MenuItem.kt | 4 +- .../groupDetails/GroupDetailsInteractor.kt | 13 ++- .../groupDetails/GroupDetailsViewModel.kt | 51 ++++++++--- .../groupDetails/model/GroupDetailsIntent.kt | 2 + .../model/GroupDetailsNavEvent.kt | 6 ++ .../screens/groupEditor/GroupEditorScreen.kt | 6 +- .../screens/groups/GroupsInteractor.kt | 12 ++- .../screens/groups/GroupsScreen.kt | 4 +- .../screens/groups/GroupsViewModel.kt | 89 ++++++++++++------- .../screens/groups/model/GroupsIntent.kt | 2 + .../screens/root/RootViewModel.kt | 10 ++- .../screens/root/model/RootIntent.kt | 4 + .../screens/root/model/StartActivityEvent.kt | 6 ++ .../simplesplit/android/utils/IntentUtils.kt | 21 +++++ android/app/src/main/res/values/strings.xml | 2 + 32 files changed, 390 insertions(+), 128 deletions(-) create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt delete mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcons.kt create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/event/Event.kt create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsNavEvent.kt create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/StartActivityEvent.kt create mode 100644 android/app/src/main/java/com/github/ai/simplesplit/android/utils/IntentUtils.kt diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt index 7bf8165..5d8319f 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt @@ -8,6 +8,8 @@ import com.github.ai.simplesplit.android.data.repository.ExpenseRepository import com.github.ai.simplesplit.android.data.repository.GroupCredentialsRepository import com.github.ai.simplesplit.android.data.repository.GroupRepository import com.github.ai.simplesplit.android.data.repository.MemberRepository +import com.github.ai.simplesplit.android.domain.usecase.CreateExportUrlUseCase +import com.github.ai.simplesplit.android.domain.usecase.CreateGroupUrlUseCase import com.github.ai.simplesplit.android.domain.usecase.ParseGroupUrlUseCase import com.github.ai.simplesplit.android.presentation.core.ResourceProvider import com.github.ai.simplesplit.android.presentation.core.ResourceProviderImpl @@ -66,6 +68,8 @@ object AndroidAppModule { // UseCases singleOf(::ParseGroupUrlUseCase) + singleOf(::CreateExportUrlUseCase) + singleOf(::CreateGroupUrlUseCase) // Interactors singleOf(::GroupsInteractor) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt new file mode 100644 index 0000000..b23942c --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt @@ -0,0 +1,15 @@ +package com.github.ai.simplesplit.android.domain.usecase + +import com.github.ai.simplesplit.android.data.api.ApiClient +import com.github.ai.simplesplit.android.model.db.GroupCredentials + +class CreateExportUrlUseCase { + + fun createUrl(credentials: GroupCredentials): String { + return "%s/export/%s.csv?password=%s".format( + ApiClient.SERVER_URL, + credentials.groupUid, + credentials.password + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt new file mode 100644 index 0000000..9675e60 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt @@ -0,0 +1,15 @@ +package com.github.ai.simplesplit.android.domain.usecase + +import com.github.ai.simplesplit.android.data.api.ApiClient +import com.github.ai.simplesplit.android.model.db.GroupCredentials + +class CreateGroupUrlUseCase { + + fun createUrl(credentials: GroupCredentials): String { + return "%s/group?ids=%s&passwords=%s".format( + ApiClient.SERVER_URL, + credentials.groupUid, + credentials.password + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/MainActivity.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/MainActivity.kt index 06255b6..2420664 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/MainActivity.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/MainActivity.kt @@ -1,21 +1,30 @@ package com.github.ai.simplesplit.android.presentation +import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.arkivanov.decompose.defaultComponentContext import com.github.ai.simplesplit.android.di.GlobalInjector.inject import com.github.ai.simplesplit.android.presentation.core.compose.navigation.NavigatorImpl import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme import com.github.ai.simplesplit.android.presentation.core.compose.theme.ThemeProvider +import com.github.ai.simplesplit.android.presentation.core.event.collectWithLifecycle import com.github.ai.simplesplit.android.presentation.screens.root.RootScreen import com.github.ai.simplesplit.android.presentation.screens.root.RootScreenComponent +import com.github.ai.simplesplit.android.presentation.screens.root.RootViewModel +import com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent +import com.github.ai.simplesplit.android.utils.IntentUtils.newOpenUrlIntent +import com.github.ai.simplesplit.android.utils.IntentUtils.newShareUrlIntent +import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { private val themeProvider: ThemeProvider by inject() private val router: Router by inject() + private val viewModel: RootViewModel by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -24,9 +33,30 @@ class MainActivity : AppCompatActivity() { componentContext = defaultComponentContext() ) - router.bindNavigator(NavigatorImpl(component, this)) + val navigator = NavigatorImpl( + rootComponent = component, + activity = this, + viewModel = viewModel + ) + router.bindNavigator(navigator) themeProvider.onThemedContextCreated(this) + lifecycleScope.launch { + viewModel.events.collectWithLifecycle(lifecycle) { event -> + when (event) { + is StartActivityEvent.OpenUrl -> { + val intent = Intent.createChooser(newOpenUrlIntent(event.url), null) + startActivity(intent) + } + + is StartActivityEvent.ShareUrl -> { + val intent = Intent.createChooser(newShareUrlIntent(event.url), null) + startActivity(intent) + } + } + } + } + setContent { AppTheme(theme = themeProvider.theme) { RootScreen( diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt index b790d99..2198038 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.LightTheme @Composable @@ -45,7 +45,7 @@ fun AppDropdownField( readOnly = true, trailingIcon = { Icon( - imageVector = AppIcons.ExpandMore, + imageVector = AppIcon.EXPAND_MORE.vector, contentDescription = null, modifier = Modifier.clickable { expanded = true } ) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppTextField.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppTextField.kt index 2f8d5b0..a5f76e4 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppTextField.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppTextField.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.LightTheme import kotlinx.coroutines.flow.MutableStateFlow @@ -76,20 +76,20 @@ fun AppTextField( when { isError -> { Icon( - imageVector = AppIcons.ErrorCircle, + imageVector = AppIcon.ERROR_CIRCLE.vector, contentDescription = null ) } isPasswordToggleEnabled -> { val icon = if (isPasswordVisible) { - AppIcons.VisibilityOff + AppIcon.VISIBILITY_OFF } else { - AppIcons.VisibilityOn + AppIcon.VISIBILITY_ON } Icon( - imageVector = icon, + imageVector = icon.vector, contentDescription = null, modifier = Modifier .clickable( diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt index 3585239..edf08db 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt @@ -8,7 +8,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme enum class TopBarMenuItem { @@ -43,7 +43,7 @@ fun TopBar( ) { Icon( tint = AppTheme.theme.colors.primaryIcon, - imageVector = AppIcons.ArrowBack, + imageVector = AppIcon.ARROW_BACK.vector, contentDescription = null ) } @@ -73,7 +73,7 @@ fun TopBar( private fun TopBarMenuItem.getImageVector(): ImageVector { return when (this) { - TopBarMenuItem.DONE -> AppIcons.Check - TopBarMenuItem.MENU -> AppIcons.Menu + TopBarMenuItem.DONE -> AppIcon.CHECK.vector + TopBarMenuItem.MENU -> AppIcon.MENU.vector } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/BottomSheetHeaderCell.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/BottomSheetHeaderCell.kt index b290bee..68b5457 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/BottomSheetHeaderCell.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/BottomSheetHeaderCell.kt @@ -22,7 +22,7 @@ import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewMod import com.github.ai.simplesplit.android.presentation.core.compose.preview.PreviewEventProvider import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview import com.github.ai.simplesplit.android.presentation.core.compose.rememberOnClickedCallback -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme import com.github.ai.simplesplit.android.presentation.core.compose.theme.DoubleGroupMargin import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin @@ -96,7 +96,7 @@ fun BottomSheetHeaderCellPreview() { } fun newBottomSheetHeaderCell( - icon: ImageVector = AppIcons.Close, + icon: ImageVector = AppIcon.CLOSE.vector, title: String = "Beer", description: String = "15.42$", titleTextSize: TextSize = TextSize.TITLE_MEDIUM, diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/MenuCell.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/MenuCell.kt index eb0bc80..6678fbc 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/MenuCell.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/MenuCell.kt @@ -20,7 +20,7 @@ import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewMod import com.github.ai.simplesplit.android.presentation.core.compose.preview.PreviewEventProvider import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview import com.github.ai.simplesplit.android.presentation.core.compose.rememberOnClickedCallback -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin import com.github.ai.simplesplit.android.presentation.core.compose.theme.HalfMargin @@ -75,7 +75,7 @@ fun MenuCellPreview() { } fun newMenuCell( - icon: ImageVector = AppIcons.Settings, + icon: ImageVector = AppIcon.SETTINGS.vector, title: String = "Settings" ) = MenuCellViewModel( model = MenuCellModel( diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Navigator.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Navigator.kt index 10c40b8..395256e 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Navigator.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Navigator.kt @@ -8,17 +8,22 @@ import com.arkivanov.decompose.router.stack.StackNavigator import com.arkivanov.decompose.value.Value import com.github.ai.simplesplit.android.presentation.screens.Screen import com.github.ai.simplesplit.android.presentation.screens.root.RootScreenComponent +import com.github.ai.simplesplit.android.presentation.screens.root.RootViewModel +import com.github.ai.simplesplit.android.presentation.screens.root.model.RootIntent +import com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent interface Navigator { fun getStackNavigation(): StackNavigator fun getStack(): Value> fun getFragmentManager(): FragmentManager fun exitNavigation() + fun startActivity(event: StartActivityEvent) } class NavigatorImpl( private val rootComponent: RootScreenComponent, - private val activity: AppCompatActivity + private val activity: AppCompatActivity, + private val viewModel: RootViewModel ) : Navigator { override fun getStackNavigation(): StackNavigator = rootComponent.navigation @@ -28,4 +33,8 @@ class NavigatorImpl( override fun exitNavigation() = activity.finish() override fun getFragmentManager(): FragmentManager = activity.supportFragmentManager + + override fun startActivity(event: StartActivityEvent) { + viewModel.sendIntent(RootIntent.StartActivity(event)) + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Router.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Router.kt index d4e4d1f..d059ce5 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Router.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/navigation/Router.kt @@ -9,6 +9,7 @@ import com.arkivanov.decompose.router.stack.replaceCurrent import com.github.ai.simplesplit.android.presentation.dialogs.Dialog import com.github.ai.simplesplit.android.presentation.dialogs.root.BottomSheetRootDialog import com.github.ai.simplesplit.android.presentation.screens.Screen +import com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent import com.github.ai.simplesplit.android.utils.StringUtils import com.github.ai.simplesplit.android.utils.mutableStateFlow import java.util.concurrent.ConcurrentHashMap @@ -35,6 +36,8 @@ interface Router { screenType: KClass, result: Any ) + + fun startActivity(event: StartActivityEvent) } fun interface ResultListener { @@ -142,6 +145,12 @@ class RouterImpl : Router { resultListeners.remove(key)?.onResult(result) } + override fun startActivity(event: StartActivityEvent) { + scope.launch { + getNavigatorOrThrow().startActivity(event) + } + } + private fun isDialogActive(): Boolean { return currentDialog != null } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt new file mode 100644 index 0000000..4e6e7d1 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt @@ -0,0 +1,57 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.theme + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.AddLink +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.FileUpload +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.ui.graphics.vector.ImageVector + +enum class AppIcon { + ARROW_BACK, + ADD, + LINK, + CHECK, + MENU, + SETTINGS, + VISIBILITY_OFF, + VISIBILITY_ON, + ERROR_CIRCLE, + CLOSE, + EXPAND_MORE, + EDIT, + REMOVE, + EXPORT, + SHARE; + + val vector: ImageVector + get() = + when (this) { + ARROW_BACK -> Icons.AutoMirrored.Filled.ArrowBackIos + ADD -> Icons.Filled.Add + LINK -> Icons.Filled.AddLink + CHECK -> Icons.Outlined.Check + MENU -> Icons.Filled.MoreVert + SETTINGS -> Icons.Filled.Settings + VISIBILITY_OFF -> Icons.Outlined.VisibilityOff + VISIBILITY_ON -> Icons.Outlined.Visibility + ERROR_CIRCLE -> Icons.Outlined.ErrorOutline + CLOSE -> Icons.Filled.Close + EXPAND_MORE -> Icons.Filled.ExpandMore + EDIT -> Icons.Filled.Edit + REMOVE -> Icons.Filled.Delete + EXPORT -> Icons.Filled.FileUpload + SHARE -> Icons.Filled.Share + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcons.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcons.kt deleted file mode 100644 index 6d779bc..0000000 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcons.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.ai.simplesplit.android.presentation.core.compose.theme - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBackIos -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.AddLink -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.outlined.Check -import androidx.compose.material.icons.outlined.ErrorOutline -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff -import androidx.compose.ui.graphics.vector.ImageVector - -object AppIcons { - val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos - val Add = Icons.Filled.Add - val Link = Icons.Filled.AddLink - val Check = Icons.Outlined.Check - val Menu = Icons.Filled.MoreVert - val Settings = Icons.Filled.Settings - val VisibilityOff = Icons.Outlined.VisibilityOff - val VisibilityOn = Icons.Outlined.Visibility - val ErrorCircle = Icons.Outlined.ErrorOutline - val Close = Icons.Filled.Close - val ExpandMore = Icons.Filled.ExpandMore - val Edit = Icons.Filled.Edit - val Remove = Icons.Filled.Delete -} - -enum class Icon { - EDIT, - REMOVE, - ADD, - LINK -} - -fun Icon.toImageVector(): ImageVector = - when (this) { - Icon.EDIT -> AppIcons.Edit - Icon.REMOVE -> AppIcons.Remove - Icon.ADD -> AppIcons.Add - Icon.LINK -> AppIcons.Link - } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/event/Event.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/event/Event.kt new file mode 100644 index 0000000..b412362 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/event/Event.kt @@ -0,0 +1,48 @@ +package com.github.ai.simplesplit.android.presentation.core.event + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector + +suspend fun Flow>.collectWithLifecycle( + lifecycle: Lifecycle, + collector: FlowCollector +) { + val events = this + + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + events.collect { event -> + val content = event.getContentIfNotHandled() + if (content != null) { + collector.emit(content) + } + } + } +} + +class Event( + private val content: T +) { + + private var isHandled = false + + fun getContentIfNotHandled(): T? { + return if (isHandled) { + null + } else { + isHandled = true + content + } + } + + companion object { + + @Suppress("UNCHECKED_CAST") + fun empty(): Event { + return Event(Unit).apply { + isHandled = true + } as Event + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt index 89b818e..06cd530 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogCellFactory.kt @@ -17,7 +17,7 @@ import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewMod import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.MenuCellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SpaceCellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.TextCellViewModel -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin import com.github.ai.simplesplit.android.presentation.core.compose.theme.GroupMargin import com.github.ai.simplesplit.android.presentation.core.compose.theme.HalfMargin @@ -68,7 +68,7 @@ class ExpenseDetailsDialogCellFactory( description = expense.amount.toString(), titleTextSize = TextSize.TITLE_MEDIUM, descriptionTextSize = TextSize.TITLE_LARGE, - icon = AppIcons.Close + icon = AppIcon.CLOSE.vector ), eventProvider = eventProvider ) @@ -213,7 +213,7 @@ class ExpenseDetailsDialogCellFactory( MenuCellViewModel( MenuCellModel( id = CellId.EDIT_MENU.name, - icon = AppIcons.Edit, + icon = AppIcon.EDIT.vector, title = resourceProvider.getString(R.string.edit) ), eventProvider @@ -221,7 +221,7 @@ class ExpenseDetailsDialogCellFactory( MenuCellViewModel( MenuCellModel( id = CellId.REMOVE_MENU.name, - icon = AppIcons.Remove, + icon = AppIcon.REMOVE.vector, title = resourceProvider.getString(R.string.remove) ), eventProvider diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogScreen.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogScreen.kt index 21f85f9..a540c01 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogScreen.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/expenseDetails/ExpenseDetailsDialogScreen.kt @@ -31,7 +31,7 @@ import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewMod import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SpaceCellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.TextCellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme import com.github.ai.simplesplit.android.presentation.core.compose.theme.DialogCardCornerSize import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin @@ -132,11 +132,11 @@ private fun ExpenseDetailsScreenPreview() { height = HalfMargin ), newMenuCell( - icon = AppIcons.Edit, + icon = AppIcon.EDIT.vector, title = "Edit" ), newMenuCell( - icon = AppIcons.Remove, + icon = AppIcon.REMOVE.vector, title = "Remove" ) ) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/MenuDialogViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/MenuDialogViewModel.kt index 6e05159..6eb6eac 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/MenuDialogViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/MenuDialogViewModel.kt @@ -7,7 +7,6 @@ import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.M import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.MenuCellModel import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.MenuCellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router -import com.github.ai.simplesplit.android.presentation.core.compose.theme.toImageVector import com.github.ai.simplesplit.android.presentation.core.mvi.CellsMviViewModel import com.github.ai.simplesplit.android.presentation.dialogs.Dialog import com.github.ai.simplesplit.android.presentation.dialogs.menuDialog.model.MenuDialogArgs @@ -63,7 +62,7 @@ class MenuDialogViewModel( return items.mapIndexed { index, item -> val model = MenuCellModel( id = CellId("menu_item", IntPayload(item.actionId)).format(), - icon = item.icon.toImageVector(), + icon = item.icon.vector, title = item.text ) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/model/MenuItem.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/model/MenuItem.kt index c2139a0..08402db 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/model/MenuItem.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/dialogs/menuDialog/model/MenuItem.kt @@ -1,11 +1,11 @@ package com.github.ai.simplesplit.android.presentation.dialogs.menuDialog.model -import com.github.ai.simplesplit.android.presentation.core.compose.theme.Icon +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import kotlinx.serialization.Serializable @Serializable data class MenuItem( - val icon: Icon, + val icon: AppIcon, val text: String, val actionId: Int ) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsInteractor.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsInteractor.kt index 677dbcf..fe50ad1 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsInteractor.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsInteractor.kt @@ -5,13 +5,18 @@ import arrow.core.raise.either import com.github.ai.simplesplit.android.data.repository.ExpenseRepository import com.github.ai.simplesplit.android.data.repository.GroupCredentialsRepository import com.github.ai.simplesplit.android.data.repository.GroupRepository +import com.github.ai.simplesplit.android.domain.usecase.CreateExportUrlUseCase +import com.github.ai.simplesplit.android.domain.usecase.CreateGroupUrlUseCase +import com.github.ai.simplesplit.android.model.db.GroupCredentials import com.github.ai.simplesplit.android.model.exception.AppException import com.github.ai.split.api.GroupDto class GroupDetailsInteractor( private val groupRepository: GroupRepository, private val expenseRepository: ExpenseRepository, - private val credentialsRepository: GroupCredentialsRepository + private val credentialsRepository: GroupCredentialsRepository, + private val exportUrlUseCase: CreateExportUrlUseCase, + private val groupUrlUseCase: CreateGroupUrlUseCase ) { suspend fun getGroup( @@ -38,4 +43,10 @@ class GroupDetailsInteractor( either { credentialsRepository.removeByGroupUid(groupUid) } + + fun createExportToCsvUrl(credentials: GroupCredentials): String = + exportUrlUseCase.createUrl(credentials) + + fun createShareUrl(credentials: GroupCredentials): String = + groupUrlUseCase.createUrl(credentials) } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt index 3c21d02..a19eec9 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/GroupDetailsViewModel.kt @@ -6,7 +6,7 @@ import com.github.ai.simplesplit.android.model.db.GroupCredentials import com.github.ai.simplesplit.android.presentation.core.ResourceProvider import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEvent import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router -import com.github.ai.simplesplit.android.presentation.core.compose.theme.Icon +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.mvi.CellsMviViewModel import com.github.ai.simplesplit.android.presentation.core.mvi.nonStateAction import com.github.ai.simplesplit.android.presentation.dialogs.Dialog @@ -25,6 +25,7 @@ import com.github.ai.simplesplit.android.presentation.screens.groupDetails.model import com.github.ai.simplesplit.android.presentation.screens.groupDetails.model.GroupDetailsState import com.github.ai.simplesplit.android.presentation.screens.groupEditor.model.GroupEditorArgs import com.github.ai.simplesplit.android.presentation.screens.groupEditor.model.GroupEditorMode +import com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent import com.github.ai.simplesplit.android.utils.getErrorMessage import com.github.ai.simplesplit.android.utils.getStringOrNull import com.github.ai.simplesplit.android.utils.mutableStateFlow @@ -92,6 +93,12 @@ class GroupDetailsViewModel( GroupDetailsIntent.OnRemoveGroupClick -> nonStateAction { showRemoveGroupConfirmationDialog() } + + is GroupDetailsIntent.OpenUrl -> + nonStateAction { router.startActivity(StartActivityEvent.OpenUrl(intent.url)) } + + is GroupDetailsIntent.ShareGroupUrl -> + nonStateAction { router.startActivity(StartActivityEvent.ShareUrl(intent.url)) } } } @@ -252,11 +259,8 @@ class GroupDetailsViewModel( router.showDialog(Dialog.MenuDialog(MenuDialogArgs(items))) router.setResultListener(Dialog.MenuDialog::class) { item -> if (item is MenuItem) { - onGroupMenuItemClicked( - action = GroupMenuAction.entries.first { action -> - action.ordinal == item.actionId - } - ) + val action = GroupMenuAction.entries.first { it.ordinal == item.actionId } + onGroupMenuItemClicked(action = action) } } } @@ -325,6 +329,27 @@ class GroupDetailsViewModel( when (action) { GroupMenuAction.EDIT_GROUP -> sendIntent(GroupDetailsIntent.OnEditGroupClick) GroupMenuAction.REMOVE_GROUP -> sendIntent(GroupDetailsIntent.OnRemoveGroupClick) + GroupMenuAction.EXPORT_TO_CSV -> { + val url = interactor.createExportToCsvUrl( + GroupCredentials( + groupUid = args.group.uid, + password = args.password + ) + ) + + sendIntent(GroupDetailsIntent.OpenUrl(url)) + } + + GroupMenuAction.SHARE_LINK -> { + val url = interactor.createShareUrl( + GroupCredentials( + groupUid = args.group.uid, + password = args.password + ) + ) + + sendIntent(GroupDetailsIntent.ShareGroupUrl(url)) + } } } @@ -359,18 +384,20 @@ class GroupDetailsViewModel( } enum class GroupMenuAction( - val icon: Icon, + val icon: AppIcon, @StringRes val resourceId: Int ) { - EDIT_GROUP(Icon.EDIT, R.string.edit), - REMOVE_GROUP(Icon.REMOVE, R.string.remove) + EDIT_GROUP(AppIcon.EDIT, R.string.edit), + REMOVE_GROUP(AppIcon.REMOVE, R.string.remove), + EXPORT_TO_CSV(AppIcon.EXPORT, R.string.export_as_csv_file), + SHARE_LINK(AppIcon.SHARE, R.string.share) } enum class ExpenseMenuAction( - val icon: Icon, + val icon: AppIcon, @StringRes val resourceId: Int ) { - EDIT_EXPENSE(Icon.EDIT, R.string.edit), - REMOVE_EXPENSE(Icon.REMOVE, R.string.remove) + EDIT_EXPENSE(AppIcon.EDIT, R.string.edit), + REMOVE_EXPENSE(AppIcon.REMOVE, R.string.remove) } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsIntent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsIntent.kt index 6c78d12..f3668e6 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsIntent.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsIntent.kt @@ -19,4 +19,6 @@ sealed class GroupDetailsIntent( data class OnEditExpenseClick(val expenseUid: String) : GroupDetailsIntent() data class OnRemoveExpenseClick(val expenseUid: String) : GroupDetailsIntent() data class OnRemoveExpenseConfirmed(val expenseUid: String) : GroupDetailsIntent() + data class OpenUrl(val url: String) : GroupDetailsIntent() + data class ShareGroupUrl(val url: String) : GroupDetailsIntent() } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsNavEvent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsNavEvent.kt new file mode 100644 index 0000000..5b4d48a --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupDetails/model/GroupDetailsNavEvent.kt @@ -0,0 +1,6 @@ +package com.github.ai.simplesplit.android.presentation.screens.groupDetails.model + +sealed interface GroupDetailsNavEvent { + data class OpenUrl(val url: String) : GroupDetailsNavEvent + data class ShareUrl(val url: String) : GroupDetailsNavEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorScreen.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorScreen.kt index c8faec4..2a5de4f 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorScreen.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groupEditor/GroupEditorScreen.kt @@ -32,7 +32,7 @@ import com.github.ai.simplesplit.android.presentation.core.compose.TopBar import com.github.ai.simplesplit.android.presentation.core.compose.TopBarMenuItem import com.github.ai.simplesplit.android.presentation.core.compose.rememberCallback import com.github.ai.simplesplit.android.presentation.core.compose.rememberOnClickedCallback -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin import com.github.ai.simplesplit.android.presentation.core.compose.theme.HalfMargin @@ -191,7 +191,7 @@ private fun RenderDataContent( } ) { Icon( - imageVector = AppIcons.Add, + imageVector = AppIcon.ADD.vector, contentDescription = null ) } @@ -225,7 +225,7 @@ private fun RenderDataContent( onClick = { onIntent.invoke(GroupEditorIntent.OnRemoveMemberClick(index)) } ) { Icon( - imageVector = AppIcons.Close, + imageVector = AppIcon.CLOSE.vector, contentDescription = null ) } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsInteractor.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsInteractor.kt index 909eb8c..7984829 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsInteractor.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsInteractor.kt @@ -4,6 +4,8 @@ import arrow.core.Either import arrow.core.raise.either import com.github.ai.simplesplit.android.data.repository.GroupCredentialsRepository import com.github.ai.simplesplit.android.data.repository.GroupRepository +import com.github.ai.simplesplit.android.domain.usecase.CreateExportUrlUseCase +import com.github.ai.simplesplit.android.domain.usecase.CreateGroupUrlUseCase import com.github.ai.simplesplit.android.model.db.GroupCredentials import com.github.ai.simplesplit.android.model.exception.AppException import com.github.ai.simplesplit.android.presentation.screens.groups.model.GroupsData @@ -11,7 +13,9 @@ import kotlinx.coroutines.flow.Flow class GroupsInteractor( private val groupRepository: GroupRepository, - private val credentialsRepository: GroupCredentialsRepository + private val credentialsRepository: GroupCredentialsRepository, + private val exportUrlUseCase: CreateExportUrlUseCase, + private val groupUrlUseCase: CreateGroupUrlUseCase ) { suspend fun loadData(): Either = @@ -45,4 +49,10 @@ class GroupsInteractor( } fun getGroupCredentialsFlow(): Flow> = credentialsRepository.getAllFlow() + + fun createExportToCsvUrl(credentials: GroupCredentials): String = + exportUrlUseCase.createUrl(credentials) + + fun createShareUrl(credentials: GroupCredentials): String = + groupUrlUseCase.createUrl(credentials) } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt index 6f721e4..e4e93be 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt @@ -22,7 +22,7 @@ import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellVie import com.github.ai.simplesplit.android.presentation.core.compose.cells.ui.SpaceCell import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SpaceCellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.rememberOnClickedCallback -import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcons +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme import com.github.ai.simplesplit.android.presentation.screens.groups.cells.ui.GroupCell import com.github.ai.simplesplit.android.presentation.screens.groups.cells.viewModel.GroupCellViewModel @@ -61,7 +61,7 @@ private fun GroupsScreen( onClick = onFabClick ) { Icon( - imageVector = AppIcons.Add, + imageVector = AppIcon.ADD.vector, contentDescription = null ) } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt index 7f4fd51..61293e7 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt @@ -1,11 +1,13 @@ package com.github.ai.simplesplit.android.presentation.screens.groups +import androidx.annotation.StringRes import androidx.lifecycle.viewModelScope import com.github.ai.simplesplit.android.R +import com.github.ai.simplesplit.android.model.db.GroupCredentials import com.github.ai.simplesplit.android.presentation.core.ResourceProvider import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEvent import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router -import com.github.ai.simplesplit.android.presentation.core.compose.theme.Icon +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.mvi.CellsMviViewModel import com.github.ai.simplesplit.android.presentation.core.mvi.nonStateAction import com.github.ai.simplesplit.android.presentation.dialogs.Dialog @@ -22,6 +24,7 @@ import com.github.ai.simplesplit.android.presentation.screens.groups.cells.model import com.github.ai.simplesplit.android.presentation.screens.groups.model.GroupsData import com.github.ai.simplesplit.android.presentation.screens.groups.model.GroupsIntent import com.github.ai.simplesplit.android.presentation.screens.groups.model.GroupsState +import com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent import com.github.ai.simplesplit.android.utils.StringUtils import com.github.ai.simplesplit.android.utils.getErrorMessage import com.github.ai.simplesplit.android.utils.getStringOrNull @@ -90,6 +93,12 @@ class GroupsViewModel( is GroupsIntent.OnAddGroupByUrlClick -> nonStateAction { navigateToCheckoutGroupScreen() } + + is GroupsIntent.OpenUrl -> + nonStateAction { router.startActivity(StartActivityEvent.OpenUrl(intent.url)) } + + is GroupsIntent.ShareUrl -> + nonStateAction { router.startActivity(StartActivityEvent.ShareUrl(intent.url)) } } } @@ -163,9 +172,7 @@ class GroupsViewModel( } private fun navigateToGroupEditor(groupUid: String) { - val creds = screenData?.credentials - ?.firstOrNull { creds -> creds.groupUid == groupUid } - ?: return + val creds = getGroupCredentials(groupUid) ?: return router.navigateTo( Screen.GroupEditor( @@ -202,12 +209,12 @@ class GroupsViewModel( MenuDialogArgs( items = listOf( MenuItem( - icon = Icon.ADD, + icon = AppIcon.ADD, text = resourceProvider.getString(R.string.create_new_group), actionId = MenuActions.CREATE_GROUP ), MenuItem( - icon = Icon.LINK, + icon = AppIcon.LINK, text = resourceProvider.getString(R.string.add_by_url), actionId = MenuActions.ADD_GROUP_BY_URL ) @@ -223,28 +230,23 @@ class GroupsViewModel( } private fun showGroupMenuDialog(groupUid: String) { - router.showDialog( - Dialog.MenuDialog( - MenuDialogArgs( - items = listOf( - MenuItem( - icon = Icon.EDIT, - text = resourceProvider.getString(R.string.edit), - actionId = MenuActions.EDIT_GROUP - ), - MenuItem( - icon = Icon.REMOVE, - text = resourceProvider.getString(R.string.remove), - actionId = MenuActions.REMOVE_GROUP - ) - ) - ) + val items = GroupMenuAction.entries.map { entry -> + MenuItem( + icon = entry.icon, + text = resourceProvider.getString(entry.resourceId), + actionId = entry.ordinal ) - ) + } + + router.showDialog(Dialog.MenuDialog(MenuDialogArgs(items))) router.setResultListener(Dialog.MenuDialog::class) { item -> if (item is MenuItem) { + val action = GroupMenuAction.entries.first { action -> + action.ordinal == item.actionId + } + onGroupMenuItemClicked( - actionId = item.actionId, + action = action, groupUid = groupUid ) } @@ -270,13 +272,25 @@ class GroupsViewModel( } private fun onGroupMenuItemClicked( - actionId: Int, + action: GroupMenuAction, groupUid: String ) { - when (actionId) { - MenuActions.EDIT_GROUP -> sendIntent(GroupsIntent.OnEditGroupClick(groupUid)) - MenuActions.REMOVE_GROUP -> sendIntent(GroupsIntent.OnRemoveGroupClick(groupUid)) - else -> throw IllegalArgumentException("Illegal actionId: $actionId") + when (action) { + GroupMenuAction.EDIT_GROUP -> sendIntent(GroupsIntent.OnEditGroupClick(groupUid)) + + GroupMenuAction.REMOVE_GROUP -> sendIntent(GroupsIntent.OnRemoveGroupClick(groupUid)) + + GroupMenuAction.EXPORT_TO_CSV -> { + val creds = getGroupCredentials(groupUid) ?: return + val url = interactor.createExportToCsvUrl(creds) + sendIntent(GroupsIntent.OpenUrl(url)) + } + + GroupMenuAction.SHARE_LINK -> { + val creds = getGroupCredentials(groupUid) ?: return + val url = interactor.createShareUrl(creds) + sendIntent(GroupsIntent.ShareUrl(url)) + } } } @@ -292,9 +306,22 @@ class GroupsViewModel( return cellId.parseCellId()?.payload?.getStringOrNull() } + private fun getGroupCredentials(groupUid: String): GroupCredentials? { + return screenData?.credentials + ?.firstOrNull { creds -> creds.groupUid == groupUid } + } + + enum class GroupMenuAction( + val icon: AppIcon, + @StringRes val resourceId: Int + ) { + EDIT_GROUP(AppIcon.EDIT, R.string.edit), + REMOVE_GROUP(AppIcon.REMOVE, R.string.remove), + EXPORT_TO_CSV(AppIcon.EXPORT, R.string.export_as_csv_file), + SHARE_LINK(AppIcon.SHARE, R.string.share) + } + object MenuActions { - const val EDIT_GROUP = 100 - const val REMOVE_GROUP = 101 const val CREATE_GROUP = 102 const val ADD_GROUP_BY_URL = 103 } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt index 1dd3daa..3c78c64 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt @@ -16,4 +16,6 @@ sealed class GroupsIntent( data class OnEditGroupClick(val groupUid: String) : GroupsIntent() data class OnRemoveGroupClick(val groupUid: String) : GroupsIntent() data class OnRemoveGroupConfirmed(val groupUid: String) : GroupsIntent() + data class OpenUrl(val url: String) : GroupsIntent() + data class ShareUrl(val url: String) : GroupsIntent() } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootViewModel.kt index bb13d72..0eaba90 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootViewModel.kt @@ -2,13 +2,18 @@ package com.github.ai.simplesplit.android.presentation.screens.root import androidx.lifecycle.ViewModel import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router +import com.github.ai.simplesplit.android.presentation.core.event.Event import com.github.ai.simplesplit.android.presentation.screens.Screen import com.github.ai.simplesplit.android.presentation.screens.root.model.RootIntent +import com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent +import kotlinx.coroutines.flow.MutableStateFlow class RootViewModel( private val router: Router ) : ViewModel() { + val events = MutableStateFlow>(Event.empty()) + fun getStartScreens(): List { return listOf(Screen.Groups) } @@ -19,8 +24,9 @@ class RootViewModel( private fun handleIntent(intent: RootIntent) { when (intent) { - RootIntent.OnBackClick -> { - router.exit() + RootIntent.OnBackClick -> router.exit() + is RootIntent.StartActivity -> { + events.value = Event(intent.event) } } } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/RootIntent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/RootIntent.kt index cdb1f7f..3395216 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/RootIntent.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/RootIntent.kt @@ -2,4 +2,8 @@ package com.github.ai.simplesplit.android.presentation.screens.root.model sealed interface RootIntent { data object OnBackClick : RootIntent + data class StartActivity( + val event: + com.github.ai.simplesplit.android.presentation.screens.root.model.StartActivityEvent + ) : RootIntent } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/StartActivityEvent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/StartActivityEvent.kt new file mode 100644 index 0000000..d8e33f6 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/model/StartActivityEvent.kt @@ -0,0 +1,6 @@ +package com.github.ai.simplesplit.android.presentation.screens.root.model + +sealed interface StartActivityEvent { + data class OpenUrl(val url: String) : StartActivityEvent + data class ShareUrl(val url: String) : StartActivityEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/utils/IntentUtils.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/utils/IntentUtils.kt new file mode 100644 index 0000000..aec6db3 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/utils/IntentUtils.kt @@ -0,0 +1,21 @@ +package com.github.ai.simplesplit.android.utils + +import android.content.Intent +import android.net.Uri + +object IntentUtils { + + fun newOpenUrlIntent(url: String): Intent { + return Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(url) + } + } + + fun newShareUrlIntent(url: String): Intent { + return Intent(Intent.ACTION_SEND) + .apply { + putExtra(Intent.EXTRA_TEXT, url) + type = "text/plain" + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index dc7d657..9ed750f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Edit Remove + Share This field is required An error occurred. Please try again. @@ -31,6 +32,7 @@ No expenses yet in this group. Add the first expense to get started! Expenses No debts yet + Export as CSV file Add Expense