From 3085e428db60946769d1ffe2c699937b36c3c1a9 Mon Sep 17 00:00:00 2001 From: Matthias Emde Date: Sat, 7 Sep 2024 21:01:44 +0200 Subject: [PATCH] refactor: organize string resources (#109) Replace strings with string resources in active session, goals, library, metronome, permissions, recorder, sessions, settings and statistics feature. Add a new htmlResource composable for displaying html annotated strings in compose. Fix some of minor bugs around the refactoring. Remove legacy strings. --- .../library/presentation/LibraryScreenTest.kt | 71 ++--- .../FakeUserPreferencesRepository.kt | 10 +- app/src/main/AndroidManifest.xml | 2 +- .../presentation/ActiveSessionScreen.kt | 147 +++++---- .../presentation/ActiveSessionUiState.kt | 3 +- .../presentation/ActiveSessionViewModel.kt | 14 +- .../presentation/SessionService.kt | 28 +- .../main/java/app/musikus/core/data/Enums.kt | 24 ++ .../java/app/musikus/core/domain/Sorting.kt | 173 +---------- .../core/presentation/MainViewModel.kt | 45 +-- .../app/musikus/core/presentation/Screen.kt | 20 +- .../core/presentation/components/ActionBar.kt | 18 +- .../DeleteConfirmationBottomSheet.kt | 18 ++ .../presentation/components/DialogActions.kt | 12 +- .../presentation/components/DurationInput.kt | 1 - .../presentation/components/FadingEdge.kt | 5 +- .../core/presentation/components/MainMenu.kt | 6 +- .../components/SelectionSpinner.kt | 13 +- .../core/presentation/components/Snackbar.kt | 43 +++ .../core/presentation/components/SortMenu.kt | 16 +- .../components/SwipeToDeleteContainer.kt | 2 +- .../presentation/components/ToggleButton.kt | 9 +- .../core/presentation/components/TopBar.kt | 10 +- .../core/presentation/components/TwoLiner.kt | 9 +- .../musikus/core/presentation/utils/UiText.kt | 51 ++- .../app/musikus/goals/data/GoalSorting.kt | 88 ++++++ .../goals/data/daos/GoalDescriptionDao.kt | 8 +- .../goals/data/entities/GoalDescription.kt | 22 +- .../goals/domain/usecase/SortGoalsUseCase.kt | 6 +- .../musikus/goals/presentation/GoalCard.kt | 15 +- .../musikus/goals/presentation/GoalDialog.kt | 43 ++- .../musikus/goals/presentation/GoalsScreen.kt | 56 ++-- .../goals/presentation/GoalsUiEvent.kt | 2 +- .../goals/presentation/GoalsUiState.kt | 5 +- .../goals/presentation/GoalsViewModel.kt | 10 +- .../musikus/library/data/LibrarySorting.kt | 112 +++++++ .../usecase/GetSortedLibraryFoldersUseCase.kt | 6 +- .../usecase/GetSortedLibraryItemsUseCase.kt | 6 +- .../library/presentation/LibraryDialogs.kt | 53 ++-- .../library/presentation/LibraryScreen.kt | 87 ++++-- .../library/presentation/LibraryUiEvent.kt | 12 +- .../library/presentation/LibraryUiState.kt | 3 +- .../library/presentation/LibraryViewModel.kt | 10 +- .../presentation/MetronomeService.kt | 5 +- .../metronome/presentation/MetronomeUi.kt | 26 +- .../presentation/MetronomeViewModel.kt | 38 +-- .../presentation/PermissionDialog.kt | 11 +- .../musikus/recorder/presentation/Recorder.kt | 55 +++- .../recorder/presentation/RecorderService.kt | 8 +- .../recorder/presentation/RecorderUi.kt | 28 +- .../recorder/presentation/RecorderUiState.kt | 16 +- .../presentation/RecorderViewModel.kt | 20 +- .../sessions/presentation/EditSession.kt | 6 +- .../sessions/presentation/SessionCard.kt | 8 +- .../sessions/presentation/SessionsScreen.kt | 35 ++- .../sessions/presentation/SessionsUiState.kt | 3 +- .../presentation/SessionsViewModel.kt | 4 +- .../data/UserPreferencesRepository.kt | 8 +- .../java/app/musikus/settings/domain/Types.kt | 38 +-- .../usecase/SelectFolderSortModeUseCase.kt | 4 +- .../usecase/SelectGoalsSortModeUseCase.kt | 4 +- .../usecase/SelectItemSortModeUseCase.kt | 4 +- .../settings/presentation/SettingsScreen.kt | 12 +- .../presentation/about/AboutScreen.kt | 28 +- .../{LicenseScreen.kt => LicensesScreen.kt} | 7 +- .../appearance/AppearanceScreen.kt | 42 +-- .../presentation/backup/BackupScreen.kt | 19 +- .../presentation/donate/DonateScreen.kt | 14 +- .../presentation/export/ExportScreen.kt | 13 +- .../settings/presentation/help/HelpScreen.kt | 18 +- .../presentation/StatisticsScreen.kt | 106 ++++--- .../goalstatistics/GoalStatisticsScreen.kt | 19 +- .../SessionStatisticsBarChart.kt | 4 +- .../SessionStatisticsPieChart.kt | 4 +- .../SessionStatisticsScreen.kt | 43 ++- .../SessionStatisticsViewModel.kt | 55 +++- .../main/res/values/activesession_strings.xml | 57 ++++ app/src/main/res/values/goals_strings.xml | 76 +++++ app/src/main/res/values/library_strings.xml | 79 +++++ app/src/main/res/values/metronome_strings.xml | 42 +++ .../main/res/values/permissions_strings.xml | 14 + app/src/main/res/values/quotes.xml | 210 ++++++------- app/src/main/res/values/recorder_strings.xml | 68 ++++ app/src/main/res/values/sessions_strings.xml | 30 ++ app/src/main/res/values/settings_strings.xml | 113 +++++++ .../main/res/values/statistics_strings.xml | 60 ++++ app/src/main/res/values/strings.xml | 293 +++--------------- .../domain/usecase/SortGoalsUseCaseTest.kt | 2 +- .../GetSortedLibraryFoldersUseCaseTest.kt | 4 +- .../GetSortedLibraryItemsUseCaseTest.kt | 18 +- .../data/FakeUserPreferencesRepository.kt | 8 +- .../SelectFolderSortModeUseCaseTest.kt | 2 +- .../usecase/SelectGoalsSortModeUseCaseTest.kt | 4 +- .../usecase/SelectItemSortModeUseCaseTest.kt | 6 +- 94 files changed, 1881 insertions(+), 1204 deletions(-) create mode 100644 app/src/main/java/app/musikus/core/data/Enums.kt create mode 100644 app/src/main/java/app/musikus/core/presentation/components/Snackbar.kt create mode 100644 app/src/main/java/app/musikus/goals/data/GoalSorting.kt create mode 100644 app/src/main/java/app/musikus/library/data/LibrarySorting.kt rename app/src/main/java/app/musikus/settings/presentation/about/{LicenseScreen.kt => LicensesScreen.kt} (90%) create mode 100644 app/src/main/res/values/activesession_strings.xml create mode 100644 app/src/main/res/values/goals_strings.xml create mode 100644 app/src/main/res/values/library_strings.xml create mode 100644 app/src/main/res/values/metronome_strings.xml create mode 100644 app/src/main/res/values/permissions_strings.xml create mode 100644 app/src/main/res/values/recorder_strings.xml create mode 100644 app/src/main/res/values/sessions_strings.xml create mode 100644 app/src/main/res/values/settings_strings.xml create mode 100644 app/src/main/res/values/statistics_strings.xml diff --git a/app/src/androidTest/java/app/musikus/library/presentation/LibraryScreenTest.kt b/app/src/androidTest/java/app/musikus/library/presentation/LibraryScreenTest.kt index c4a0c23dd..3475d42d4 100644 --- a/app/src/androidTest/java/app/musikus/library/presentation/LibraryScreenTest.kt +++ b/app/src/androidTest/java/app/musikus/library/presentation/LibraryScreenTest.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.library.presentation @@ -33,9 +33,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.test.core.app.ApplicationProvider import app.musikus.R -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode -import app.musikus.core.domain.SortMode +import app.musikus.core.domain.FakeTimeProvider import app.musikus.core.presentation.HomeViewModel import app.musikus.core.presentation.MainActivity import app.musikus.core.presentation.MainViewModel @@ -44,7 +42,6 @@ import app.musikus.core.presentation.theme.MusikusTheme import app.musikus.core.presentation.utils.TestTags import app.musikus.settings.domain.ColorSchemeSelections import app.musikus.settings.domain.ThemeSelections -import app.musikus.core.domain.FakeTimeProvider import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before @@ -97,13 +94,13 @@ class LibraryScreenTest { @Test fun clickFab_multiFabMenuIsShown() { - composeRule.onNodeWithContentDescription("Folder").assertDoesNotExist() - composeRule.onNodeWithContentDescription("Item").assertDoesNotExist() + composeRule.onNodeWithContentDescription("Add folder").assertDoesNotExist() + composeRule.onNodeWithContentDescription("Add item").assertDoesNotExist() - composeRule.onNodeWithContentDescription("Add").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() - composeRule.onNodeWithContentDescription("Folder").assertIsDisplayed() - composeRule.onNodeWithContentDescription("Item").assertIsDisplayed() + composeRule.onNodeWithContentDescription("Add folder").assertIsDisplayed() + composeRule.onNodeWithContentDescription("Add item").assertIsDisplayed() } @Test @@ -111,16 +108,16 @@ class LibraryScreenTest { val context = ApplicationProvider.getApplicationContext() // Check if hint is displayed initially - composeRule.onNodeWithText(context.getString(R.string.libraryHint)).assertIsDisplayed() + composeRule.onNodeWithText(context.getString(R.string.library_screen_hint)).assertIsDisplayed() // Add a folder - composeRule.onNodeWithContentDescription("Add").performClick() - composeRule.onNodeWithContentDescription("Folder").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() + composeRule.onNodeWithContentDescription("Add folder").performClick() composeRule.onNodeWithTag(TestTags.FOLDER_DIALOG_NAME_INPUT).performTextInput("Test") composeRule.onNodeWithContentDescription("Create").performClick() // Check if hint is not displayed anymore - composeRule.onNodeWithText(context.getString(R.string.libraryHint)).assertDoesNotExist() + composeRule.onNodeWithText(context.getString(R.string.library_screen_hint)).assertDoesNotExist() // Remove the folder composeRule.onNodeWithText("Test").performTouchInput { longClick() } @@ -128,16 +125,16 @@ class LibraryScreenTest { composeRule.onNodeWithContentDescription("Delete forever (1)").performClick() // Check if hint is displayed again - composeRule.onNodeWithText(context.getString(R.string.libraryHint)).assertIsDisplayed() + composeRule.onNodeWithText(context.getString(R.string.library_screen_hint)).assertIsDisplayed() // Add an item - composeRule.onNodeWithContentDescription("Add").performClick() - composeRule.onNodeWithContentDescription("Item").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() + composeRule.onNodeWithContentDescription("Add item").performClick() composeRule.onNodeWithTag(TestTags.ITEM_DIALOG_NAME_INPUT).performTextInput("Test") composeRule.onNodeWithContentDescription("Create").performClick() // Check if hint is not displayed anymore - composeRule.onNodeWithText(context.getString(R.string.libraryHint)).assertDoesNotExist() + composeRule.onNodeWithText(context.getString(R.string.library_screen_hint)).assertDoesNotExist() // Remove the item composeRule.onNodeWithText("Test").performTouchInput { longClick() } @@ -145,21 +142,21 @@ class LibraryScreenTest { composeRule.onNodeWithContentDescription("Delete forever (1)").performClick() // Check if hint is displayed again - composeRule.onNodeWithText(context.getString(R.string.libraryHint)).assertIsDisplayed() + composeRule.onNodeWithText(context.getString(R.string.library_screen_hint)).assertIsDisplayed() } @Test fun addItemToFolderFromInsideAndOutside() { // Add a folder - composeRule.onNodeWithContentDescription("Add").performClick() - composeRule.onNodeWithContentDescription("Folder").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() + composeRule.onNodeWithContentDescription("Add folder").performClick() composeRule.onNodeWithTag(TestTags.FOLDER_DIALOG_NAME_INPUT).performTextInput("TestFolder") composeRule.onNodeWithContentDescription("Create").performClick() // Add an item from outside the folder - composeRule.onNodeWithContentDescription("Add").performClick() - composeRule.onNodeWithContentDescription("Item").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() + composeRule.onNodeWithContentDescription("Add item").performClick() composeRule.onNodeWithTag(TestTags.ITEM_DIALOG_NAME_INPUT).performTextInput("TestItem1") composeRule.onNodeWithContentDescription("Select folder").performClick() @@ -185,18 +182,16 @@ class LibraryScreenTest { composeRule.onNodeWithText("TestItem2").assertIsDisplayed() } - private fun clickSortMode(sortMode: SortMode<*>) { - val sortModeType = when(sortMode) { - is LibraryItemSortMode -> "items" - is LibraryFolderSortMode -> "folders" - else -> throw Exception("Unknown sort mode type") - } + private fun clickSortMode( + sortModeType: String, + sortMode: String + ) { composeRule.onNodeWithContentDescription("Select sort mode and direction for $sortModeType").performClick() // Select name as sorting mode composeRule.onNode( matcher = hasAnyAncestor(hasContentDescription("List of sort modes for $sortModeType")) and - hasText(sortMode.label) + hasText(sortMode) ).performClick() } @@ -210,8 +205,8 @@ class LibraryScreenTest { ) namesAndColors.forEach { (name, color) -> - composeRule.onNodeWithContentDescription("Add").performClick() - composeRule.onNodeWithContentDescription("Item").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() + composeRule.onNodeWithContentDescription("Add item").performClick() composeRule.onNodeWithTag(TestTags.ITEM_DIALOG_NAME_INPUT).performTextInput(name) composeRule.onNodeWithContentDescription("Color $color").performClick() composeRule.onNodeWithContentDescription("Create").performClick() @@ -229,7 +224,7 @@ class LibraryScreenTest { } // Change sorting mode to name descending - clickSortMode(LibraryItemSortMode.NAME) + clickSortMode("items", "Name") // Check if items are displayed in correct order itemNodes = composeRule.onAllNodes(hasText("TestItem", substring = true)) @@ -241,7 +236,7 @@ class LibraryScreenTest { } // Change sorting mode to name ascending - clickSortMode(LibraryItemSortMode.NAME) + clickSortMode("items", "Name") // Check if items are displayed in correct order itemNodes = composeRule.onAllNodes(hasText("TestItem", substring = true)) @@ -262,8 +257,8 @@ class LibraryScreenTest { ) names.forEach { name -> - composeRule.onNodeWithContentDescription("Add").performClick() - composeRule.onNodeWithContentDescription("Folder").performClick() + composeRule.onNodeWithContentDescription("Add folder or item").performClick() + composeRule.onNodeWithContentDescription("Add folder").performClick() composeRule.onNodeWithTag(TestTags.FOLDER_DIALOG_NAME_INPUT).performTextInput(name) composeRule.onNodeWithContentDescription("Create").performClick() @@ -280,7 +275,7 @@ class LibraryScreenTest { } // Change sorting mode to name descending - clickSortMode(LibraryFolderSortMode.NAME) + clickSortMode("folders", "Name") // Check if items are displayed in correct order itemNodes = composeRule.onAllNodes(hasText("TestFolder", substring = true)) @@ -292,7 +287,7 @@ class LibraryScreenTest { } // Change sorting mode to name ascending - clickSortMode(LibraryFolderSortMode.NAME) + clickSortMode("folders", "Name") // Check if items are displayed in correct order itemNodes = composeRule.onAllNodes(hasText("TestFolder", substring = true)) diff --git a/app/src/androidTest/java/app/musikus/repository/FakeUserPreferencesRepository.kt b/app/src/androidTest/java/app/musikus/repository/FakeUserPreferencesRepository.kt index ba7a2b8c2..93d916e26 100644 --- a/app/src/androidTest/java/app/musikus/repository/FakeUserPreferencesRepository.kt +++ b/app/src/androidTest/java/app/musikus/repository/FakeUserPreferencesRepository.kt @@ -3,17 +3,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.repository -import app.musikus.core.domain.GoalSortInfo -import app.musikus.core.domain.GoalsSortMode -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.goals.data.GoalSortInfo +import app.musikus.goals.data.GoalsSortMode +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem import app.musikus.metronome.presentation.MetronomeSettings diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0856a96c4..8aaf98143 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,7 +33,7 @@ android:name="app.musikus.core.presentation.Musikus" android:allowBackup="true" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" + android:label="@string/core_app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Musikus" diff --git a/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionScreen.kt b/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionScreen.kt index 5be310b98..db16e43b3 100644 --- a/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionScreen.kt +++ b/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionScreen.kt @@ -4,7 +4,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * Copyright (c) 2024 Michael Prommersberger, Matthias Emde - * */ @file:OptIn(ExperimentalFoundationApi::class) @@ -91,10 +90,8 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue -import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Tab @@ -134,6 +131,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -152,9 +150,7 @@ import androidx.lifecycle.repeatOnLifecycle import app.musikus.R import app.musikus.core.data.LibraryFolderWithItems import app.musikus.core.data.UUIDConverter -import app.musikus.library.data.daos.LibraryFolder -import app.musikus.library.data.daos.LibraryItem -import app.musikus.settings.domain.ColorSchemeSelections +import app.musikus.core.domain.TimeProvider import app.musikus.core.presentation.Screen import app.musikus.core.presentation.components.DeleteConfirmationBottomSheet import app.musikus.core.presentation.components.DialogActions @@ -163,7 +159,7 @@ import app.musikus.core.presentation.components.ExceptionHandler import app.musikus.core.presentation.components.SwipeToDeleteContainer import app.musikus.core.presentation.components.conditional import app.musikus.core.presentation.components.fadingEdge -import app.musikus.sessions.presentation.RatingBar +import app.musikus.core.presentation.components.showSnackbar import app.musikus.core.presentation.theme.MusikusColorSchemeProvider import app.musikus.core.presentation.theme.MusikusPreviewElement1 import app.musikus.core.presentation.theme.MusikusPreviewElement2 @@ -176,19 +172,23 @@ import app.musikus.core.presentation.theme.MusikusThemedPreview import app.musikus.core.presentation.theme.dimensions import app.musikus.core.presentation.theme.libraryItemColors import app.musikus.core.presentation.theme.spacing -import app.musikus.library.presentation.LibraryUiItem -import app.musikus.metronome.presentation.MetronomeUi -import app.musikus.recorder.presentation.RecorderUi import app.musikus.core.presentation.utils.DurationFormat -import app.musikus.core.domain.TimeProvider import app.musikus.core.presentation.utils.UiIcon import app.musikus.core.presentation.utils.UiText import app.musikus.core.presentation.utils.getDurationString +import app.musikus.library.data.daos.LibraryFolder +import app.musikus.library.data.daos.LibraryItem +import app.musikus.library.presentation.LibraryUiItem +import app.musikus.metronome.presentation.MetronomeUi +import app.musikus.recorder.presentation.RecorderUi +import app.musikus.sessions.presentation.RatingBar +import app.musikus.settings.domain.ColorSchemeSelections import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -245,13 +245,13 @@ fun ActiveSession( val tabs = persistentListOf( ToolsTab( type = ActiveSessionTab.METRONOME, - title = "Metronome", + title = stringResource(id = R.string.active_session_toolbar_metronome), icon = UiIcon.IconResource(R.drawable.ic_metronome), content = { MetronomeUi() } ), ToolsTab( type = ActiveSessionTab.RECORDER, - title = "Recorder", + title = stringResource(id = R.string.active_session_toolbar_recorder), icon = UiIcon.DynamicIcon(Icons.Default.Mic), content = { RecorderUi(snackbarHostState = snackbarHostState) } ) @@ -453,8 +453,8 @@ private fun ActiveSessionScreen( /** Discard Session Dialog */ if (dialogUiState.value.discardDialogVisible) { DeleteConfirmationBottomSheet( - confirmationIcon = UiIcon.DynamicIcon(Icons.Default.Delete), - confirmationText = UiText.DynamicString("Discard session?"), + confirmationIcon = Icons.Default.Delete, + confirmationText = stringResource(id = R.string.active_session_discard_session_dialog_confirm), onDismiss = { eventHandler(ActiveSessionUiEvent.ToggleDiscardDialog) }, onConfirm = { eventHandler(ActiveSessionUiEvent.DiscardSessionDialogConfirmed) @@ -587,7 +587,7 @@ private fun ToolsBottomSheetScaffold( } else { 0.dp }, - label = "" + label = "animatedBottomPadding" ) val paddingValues = remember { derivedStateOf { @@ -690,8 +690,9 @@ private fun ActiveSessionMainContent( // Past Items val pastItemsState = uiState.value.pastSectionsUiState.collectAsState() if (pastItemsState.value != null) { - SectionsList( + SectionList( uiState = pastItemsState, + scope = rememberCoroutineScope(), nestedScrollConnection = nestedScrollConnection, // for hiding the FAB listState = sectionsListState, snackbarHostState = snackbarHostState, @@ -813,7 +814,7 @@ private fun ActiveSessionTopBar( TextButton( onClick = onSave ) { - Text(text = "Finish") + Text(text = stringResource(id = R.string.active_session_top_bar_save)) } } } @@ -891,7 +892,8 @@ private fun ActiveSessionBottomTabs( } } } - }) + } + ) } } } @@ -979,14 +981,14 @@ private fun PracticeTimer( ) { Icon(imageVector = Icons.Outlined.PlayCircle, contentDescription = null) Spacer(Modifier.width(MaterialTheme.spacing.small)) - Text(text = uiState.value.subHeadingText) + Text(text = uiState.value.subHeadingText.asString()) } } else -> { Text( style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant), - text = uiState.value.subHeadingText, + text = uiState.value.subHeadingText.asString(), ) } @@ -1065,8 +1067,9 @@ private fun CurrentPracticingItem( @OptIn(ExperimentalFoundationApi::class) @Composable -private fun SectionsList( +private fun SectionList( uiState: State, + scope :CoroutineScope, onSectionDeleted: (CompletedSectionUiState) -> Unit, nestedScrollConnection: NestedScrollConnection, listState: LazyListState, @@ -1082,7 +1085,7 @@ private fun SectionsList( .fillMaxWidth() .padding(horizontal = MaterialTheme.spacing.large), textAlign = TextAlign.Start, - text = "Already practiced", + text = stringResource(id = R.string.active_session_section_list_title), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1105,6 +1108,7 @@ private fun SectionsList( ) { item -> SectionListElement( modifier = Modifier.animateItemPlacement(), + scope = scope, item = item, snackbarHostState = snackbarHostState, onSectionDeleted = onSectionDeleted, @@ -1123,49 +1127,40 @@ private fun SectionsList( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SectionListElement( modifier: Modifier = Modifier, + scope: CoroutineScope, item: CompletedSectionUiState, - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + snackbarHostState: SnackbarHostState, onSectionDeleted: (CompletedSectionUiState) -> Unit = {}, ) { - val scope = rememberCoroutineScope() + val context = LocalContext.current var deleted by remember { mutableStateOf(false) } val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { targetValue -> deleted = targetValue == SwipeToDismissBoxValue.EndToStart - true// don't set to deleted or item will not be dismissable again after restore + true // don't set to deleted or item will not be dismissible again after restore }, positionalThreshold = with(LocalDensity.current) { { 100.dp.toPx() } // TODO remove hardcode? } ) + SwipeToDeleteContainer( state = dismissState, deleted = deleted, onDeleted = { - // TODO: re-use maineventhandler::ShowSnackbar - scope.launch { - // TODO handle deletion when user leaves screen before timeout - val result = snackbarHostState.showSnackbar( - message = "Section deleted", - actionLabel = "Undo", - withDismissAction = true, - duration = SnackbarDuration.Short, - ) - when (result) { - SnackbarResult.ActionPerformed -> { - deleted = false - dismissState.reset() - } - - SnackbarResult.Dismissed -> { - onSectionDeleted(item) - } + onSectionDeleted(item) + showSnackbar( + context = context, + scope = scope, + hostState = snackbarHostState, + message = context.getString(R.string.active_session_sections_list_element_deleted), + onUndo = { + TODO("Fix this using soft delete of sections in repository") } - } + ) } ) { Surface( @@ -1206,6 +1201,7 @@ private fun SectionListElement( } } +// FAB for new Item @Composable private fun AddSectionFAB( sessionState: State, @@ -1219,23 +1215,17 @@ private fun AddSectionFAB( enter = slideInVertically(initialOffsetY = { it * 2 }), exit = slideOutVertically(targetOffsetY = { it * 2 }), ) { - // FAB for new Item + val message = stringResource( + id = + if (sessionState.value == ActiveSessionState.NOT_STARTED) + R.string.active_session_add_section_fab_before_session + else + R.string.active_session_add_section_fab_during_session + ) ExtendedFloatingActionButton( onClick = onClick, - icon = { - Icon( - imageVector = Icons.Filled.Add, contentDescription = "New Library Item" - ) - }, - text = { - Text( - if (sessionState.value == ActiveSessionState.NOT_STARTED) { - "Start practicing" - } else { - "Next Item" - } - ) - }, + icon = { Icon(imageVector = Icons.Filled.Add, contentDescription = message) }, + text = { Text(text = message) }, expanded = true, ) } @@ -1267,7 +1257,7 @@ private fun NewItemSelector( ) { Spacer(modifier = Modifier.width(MaterialTheme.spacing.medium)) Text( - text = "Select a library item", + text = stringResource(id = R.string.active_session_new_item_selector_title), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1289,11 +1279,11 @@ private fun NewItemSelector( DropdownMenuItem( onClick = onNewItem, - text = { Text(text = "Create Item") } + text = { Text(text = stringResource(id = R.string.active_session_new_item_selector_create_item) } ) DropdownMenuItem( onClick = onNewFolder, - text = { Text(text = "Create Folder") } + text = { Text(text = stringResource(id = R.string.active_session_new_item_selector_create_folder) } ) } } @@ -1318,7 +1308,8 @@ private fun NewItemSelector( { folderId -> selectedFolder = folderId } - }) + } + ) } // use own divider to avoid padding of default one from TabRow @@ -1423,7 +1414,7 @@ private fun LibraryFolderElement( modifier = Modifier .padding(horizontal = MaterialTheme.spacing.small) .basicMarquee(), - text = folder?.name ?: "no folder", + text = folder?.name ?: stringResource(id = R.string.active_session_library_folder_element_default), style = MaterialTheme.typography.labelMedium, color = textColor, textAlign = TextAlign.Center, @@ -1445,7 +1436,7 @@ private fun LibraryItemList( modifier = modifier.fillMaxWidth(), contentPadding = WindowInsets( top = MaterialTheme.spacing.small, - ).add(WindowInsets.navigationBars).asPaddingValues() // don't get covered by navbars + ).add(WindowInsets.navigationBars).asPaddingValues() // don't get covered by navbars ) { items(items) { LibraryUiItem( @@ -1478,11 +1469,11 @@ fun EndSessionDialog( color = MaterialTheme.colorScheme.surfaceContainerHigh ) { Column { - DialogHeader(title = "Finish session") + DialogHeader(title = stringResource(id = R.string.active_session_end_session_dialog_title)) Column(Modifier.padding(horizontal = MaterialTheme.spacing.medium)) { - Text(text = "Rate your session: ") + Text(text = stringResource(id = R.string.active_session_end_session_dialog_rating)) Spacer(Modifier.height(MaterialTheme.spacing.small)) Row( @@ -1500,13 +1491,13 @@ fun EndSessionDialog( Spacer(Modifier.height(MaterialTheme.spacing.large)) OutlinedTextField( value = comment, - placeholder = { Text("Comment (optional)") }, + placeholder = { Text(text = stringResource(id = R.string.active_session_end_session_dialog_comment)) }, onValueChange = onCommentChanged ) } DialogActions( - dismissButtonText = "Keep Practicing", - confirmButtonText = "Save", + dismissButtonText = stringResource(id = R.string.active_session_end_session_dialog_dismiss), + confirmButtonText = stringResource(id = R.string.active_session_end_session_dialog_confirm), onDismissHandler = onDismiss, onConfirmHandler = onConfirm ) @@ -1534,7 +1525,7 @@ private fun PreviewActiveSessionScreen( timerText = getDurationString( (42 * 60 + 24).seconds, DurationFormat.MS_DIGITAL ).toString(), - subHeadingText = "Practice Time", + subHeadingText = UiText.StringResource(R.string.active_session_timer_subheading), ) ), currentItemUiState = MutableStateFlow(dummyRunningItem), @@ -1566,12 +1557,12 @@ private fun PreviewActiveSessionScreen( tabs = listOf( ToolsTab( type = ActiveSessionTab.METRONOME, - title = "Metronome", + title = stringResource(id = R.string.active_session_toolbar_metronome), icon = UiIcon.IconResource(R.drawable.ic_metronome), content = { }), ToolsTab( type = ActiveSessionTab.RECORDER, - title = "Recorder", + title = stringResource(id = R.string.active_session_toolbar_recorder), icon = UiIcon.DynamicIcon(Icons.Default.Mic), content = { }) ).toImmutableList(), @@ -1604,7 +1595,11 @@ private fun PreviewSectionItem( @PreviewParameter(MusikusColorSchemeProvider::class) theme: ColorSchemeSelections, ) { MusikusThemedPreview(theme) { - SectionListElement(item = dummySections.first()) + SectionListElement( + item = dummySections.first(), + snackbarHostState = remember { SnackbarHostState() }, + scope = rememberCoroutineScope() + ) } } diff --git a/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionUiState.kt b/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionUiState.kt index 6d7de4e28..2f57fa7c0 100644 --- a/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionUiState.kt +++ b/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionUiState.kt @@ -11,6 +11,7 @@ package app.musikus.activesession.presentation import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import app.musikus.core.data.LibraryFolderWithItems +import app.musikus.core.presentation.utils.UiText import app.musikus.library.data.daos.LibraryItem import kotlinx.coroutines.flow.StateFlow import java.time.ZonedDateTime @@ -50,7 +51,7 @@ data class ActiveSessionContentUiState( @Stable data class ActiveSessionTimerUiState( val timerText: String, - val subHeadingText: String, + val subHeadingText: UiText, ) @Stable diff --git a/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionViewModel.kt b/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionViewModel.kt index 8d817ac04..fc36e92ad 100644 --- a/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionViewModel.kt +++ b/app/src/main/java/app/musikus/activesession/presentation/ActiveSessionViewModel.kt @@ -13,6 +13,7 @@ import android.app.Application import android.os.Build import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import app.musikus.R import app.musikus.core.data.Nullable import app.musikus.library.data.daos.LibraryItem import app.musikus.sessions.data.entities.SectionCreationAttributes @@ -25,6 +26,7 @@ import app.musikus.library.domain.usecase.LibraryUseCases import app.musikus.permissions.domain.usecase.PermissionsUseCases import app.musikus.sessions.domain.usecase.SessionsUseCases import app.musikus.core.presentation.utils.DurationFormat +import app.musikus.core.presentation.utils.UiText import app.musikus.permissions.domain.PermissionChecker import app.musikus.core.presentation.utils.getDurationString import dagger.hilt.android.lifecycle.HiltViewModel @@ -150,14 +152,16 @@ class ActiveSessionViewModel @Inject constructor( ) ActiveSessionTimerUiState( timerText = getFormattedTimerText(practiceDuration), - subHeadingText = if (pause) "Paused $pauseDurStr" else "Practice Time", + subHeadingText = + if (pause) UiText.StringResource(R.string.active_session_timer_subheading_paused, pauseDurStr) + else UiText.StringResource(R.string.active_session_timer_subheading), ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = ActiveSessionTimerUiState( timerText = getFormattedTimerText(Duration.ZERO), - subHeadingText = "Practice Time" + subHeadingText = UiText.StringResource(R.string.active_session_timer_subheading) ) ) @@ -171,7 +175,7 @@ class ActiveSessionViewModel @Inject constructor( runningLibraryItem, _clock // should update with clock ) { sessionState, item, _ -> - if (sessionState == ActiveSessionState.NOT_STARTED) return@combine null + if (sessionState == ActiveSessionState.NOT_STARTED || item == null) return@combine null val currentItemDuration = try { activeSessionUseCases.getRunningItemDuration() @@ -179,12 +183,12 @@ class ActiveSessionViewModel @Inject constructor( Duration.ZERO // Session not yet started } ActiveSessionCurrentItemUiState( - name = item?.name ?: "Not started", + name = item.name, durationText = getDurationString( currentItemDuration, DurationFormat.MS_DIGITAL ).toString(), - color = libraryItemColors[item?.colorIndex ?: 0] + color = libraryItemColors[item.colorIndex] ) }.stateIn( scope = viewModelScope, diff --git a/app/src/main/java/app/musikus/activesession/presentation/SessionService.kt b/app/src/main/java/app/musikus/activesession/presentation/SessionService.kt index d8c1e34c0..dba4fa609 100644 --- a/app/src/main/java/app/musikus/activesession/presentation/SessionService.kt +++ b/app/src/main/java/app/musikus/activesession/presentation/SessionService.kt @@ -28,11 +28,11 @@ import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import app.musikus.R -import app.musikus.core.presentation.SESSION_NOTIFICATION_CHANNEL_ID -import app.musikus.core.di.ApplicationScope import app.musikus.activesession.domain.usecase.ActiveSessionUseCases -import app.musikus.core.presentation.utils.DurationFormat +import app.musikus.core.di.ApplicationScope import app.musikus.core.domain.TimeProvider +import app.musikus.core.presentation.SESSION_NOTIFICATION_CHANNEL_ID +import app.musikus.core.presentation.utils.DurationFormat import app.musikus.core.presentation.utils.getDurationString import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -207,13 +207,18 @@ class SessionService : Service() { try { val totalPracticeDurationStr = getDurationString(useCases.getPracticeDuration(), DurationFormat.HMS_DIGITAL) - val currentSectionName = useCases.getRunningItem().first()?.name ?: "No section selected" + + val currentSectionName = useCases.getRunningItem().first()!!.name if (useCases.getPausedState()) { - title = "Practicing Paused" - description = "$currentSectionName - Total: $totalPracticeDurationStr" + title = getString(R.string.active_session_service_notification_title_paused) + description = getString( + R.string.active_session_service_notification_description_paused, + currentSectionName, + totalPracticeDurationStr + ) } else { - title = "Practicing for $totalPracticeDurationStr" + title = getString(R.string.active_session_service_notification_title, totalPracticeDurationStr) description = currentSectionName } } catch (e: IllegalStateException) { @@ -253,7 +258,8 @@ class SessionService : Service() { title: String, description: String, actionButton1: NotificationActionButtonConfig?, - actionButton2: NotificationActionButtonConfig?): Notification { + actionButton2: NotificationActionButtonConfig? + ): Notification { val icon = R.drawable.ic_launcher_foreground @@ -307,19 +313,19 @@ class SessionService : Service() { pauseActionButton = NotificationActionButtonConfig( icon = R.drawable.ic_pause, - text = "Pause", + text = getString(R.string.active_session_service_notification_action_pause), tapIntent = pendingIntentActionPause ) resumeActionButton = NotificationActionButtonConfig( icon = R.drawable.ic_play, - text = "Resume", + text = getString(R.string.active_session_service_notification_action_resume), tapIntent = pendingIntentActionPause ) finishActionButton = NotificationActionButtonConfig( icon = R.drawable.ic_stop, - text = "Finish", + text = getString(R.string.active_session_service_notification_action_finish), tapIntent = pendingIntentActionFinish ) diff --git a/app/src/main/java/app/musikus/core/data/Enums.kt b/app/src/main/java/app/musikus/core/data/Enums.kt new file mode 100644 index 000000000..9ca3524be --- /dev/null +++ b/app/src/main/java/app/musikus/core/data/Enums.kt @@ -0,0 +1,24 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2024 Matthias Emde + */ + +package app.musikus.core.data + +import app.musikus.core.presentation.utils.UiIcon +import app.musikus.core.presentation.utils.UiText + +interface EnumWithLabel { + val label: UiText +} + +interface EnumWithIcon { + val icon: UiIcon +} + +interface EnumWithDescription { + val description: UiText +} \ No newline at end of file diff --git a/app/src/main/java/app/musikus/core/domain/Sorting.kt b/app/src/main/java/app/musikus/core/domain/Sorting.kt index 02fd00138..303a091e4 100644 --- a/app/src/main/java/app/musikus/core/domain/Sorting.kt +++ b/app/src/main/java/app/musikus/core/domain/Sorting.kt @@ -3,18 +3,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.core.domain -import app.musikus.core.data.GoalDescriptionWithInstancesAndLibraryItems -import app.musikus.core.data.GoalInstanceWithDescriptionWithLibraryItems -import app.musikus.core.data.LibraryFolderWithItems -import app.musikus.goals.data.daos.GoalDescription -import app.musikus.goals.data.daos.GoalInstance -import app.musikus.library.data.daos.LibraryFolder -import app.musikus.library.data.daos.LibraryItem +import app.musikus.core.presentation.utils.UiText data class SortInfo( val mode: SortMode, @@ -42,171 +36,10 @@ enum class SortDirection { } interface SortMode { - val label: String + val label: UiText val comparator: Comparator val name: String val isDefault: Boolean } -typealias GoalSortInfo = SortInfo> -enum class GoalsSortMode : SortMode> { - DATE_ADDED { - override val label = "Date added" - override val comparator = compareBy> { (description, _) -> - description.createdAt - } - }, - TARGET { - override val label = "Target" - override val comparator = compareBy> { (_, instance) -> - instance.target - } - }, - PERIOD { - override val label = "Period" - override val comparator = compareBy> { (description, _) -> - description.periodUnit - }.thenBy { (description, _) -> - description.periodInPeriodUnits - } - }, -// CUSTOM { -// override val label = "Custom" -// override val comparator = compareBy> { TODO } -// } - ; - - override val isDefault: Boolean - get() = this == DEFAULT - - companion object { - val DEFAULT = DATE_ADDED - - fun valueOrDefault(string: String?) = try { - valueOf(string ?: "") - } catch (e: Exception) { - DEFAULT - } - } -} - -@JvmName("sortedGoalInstanceWithDescriptionWithLibraryItems") -fun List.sorted( - mode: GoalsSortMode, - direction: SortDirection -) : List = this.sortedWith( - when(direction) { - SortDirection.ASCENDING -> - compareBy (mode.comparator) { it.description.description to it.instance} - SortDirection.DESCENDING -> - compareByDescending(mode.comparator) { it.description.description to it.instance } - } -) - -@JvmName("sortedGoalDescriptionWithInstancesAndLibraryItems") -fun List.sorted( - mode: GoalsSortMode, - direction: SortDirection -) : List = this.sortedWith( - when(direction) { - SortDirection.ASCENDING -> - compareBy (mode.comparator) { it.description to it.latestInstance } - SortDirection.DESCENDING -> - compareByDescending(mode.comparator) { it.description to it.latestInstance } - } -) - -enum class LibraryItemSortMode : SortMode { - DATE_ADDED { - override val label = "Date added" - override val comparator = compareBy { it.createdAt } - }, - LAST_MODIFIED { - override val label = "Last modified" - override val comparator = compareBy { it.modifiedAt } - }, - NAME { - override val label = "Name" - override val comparator = compareBy { it.name } - }, - COLOR { - override val label = "Color" - override val comparator = compareBy { it.colorIndex } - }, -// CUSTOM { -// override val label = "Custom" -// override val comparator = compareBy { TODO } -// } - ; - - override val isDefault: Boolean - get() = this == DEFAULT - - companion object { - val DEFAULT = DATE_ADDED - - fun valueOrDefault(string: String?) = try { - valueOf(string ?: "") - } catch (e: Exception) { - DEFAULT - } - } -} - -fun List.sorted( - mode: LibraryItemSortMode, - direction: SortDirection -) = this.sortedWith ( - when(direction) { - SortDirection.ASCENDING -> - compareBy (mode.comparator) { it } - SortDirection.DESCENDING -> - compareByDescending(mode.comparator) { it } - } -) - -enum class LibraryFolderSortMode : SortMode { - DATE_ADDED { - override val label = "Date added" - override val comparator = compareBy { it.createdAt } - }, - LAST_MODIFIED { - override val label = "Last modified" - override val comparator = compareBy { it.modifiedAt } - }, - NAME { - override val label = "Name" - override val comparator = compareBy { it.name } - }, -// CUSTOM { -// override val label = "Custom" -// override val comparator = compareBy { TODO } -// } - ; - - override val isDefault: Boolean - get() = this == DEFAULT - - companion object { - val DEFAULT = DATE_ADDED - - fun valueOrDefault(string: String?) = try { - valueOf(string ?: "") - } catch (e: Exception) { - DEFAULT - } - } -} - -fun List.sorted( - mode: LibraryFolderSortMode, - direction: SortDirection, -) = this.sortedWith( - when (direction) { - SortDirection.ASCENDING -> - compareBy(mode.comparator) { it.folder } - SortDirection.DESCENDING -> - compareByDescending(mode.comparator) { it.folder } - } -) \ No newline at end of file diff --git a/app/src/main/java/app/musikus/core/presentation/MainViewModel.kt b/app/src/main/java/app/musikus/core/presentation/MainViewModel.kt index bae281c5e..8457acbd6 100644 --- a/app/src/main/java/app/musikus/core/presentation/MainViewModel.kt +++ b/app/src/main/java/app/musikus/core/presentation/MainViewModel.kt @@ -8,22 +8,20 @@ package app.musikus.core.presentation -import androidx.compose.material3.SnackbarDuration +import android.app.Application import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.lifecycle.ViewModel +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import app.musikus.activesession.domain.usecase.ActiveSessionUseCases +import app.musikus.core.presentation.components.showSnackbar import app.musikus.settings.domain.ColorSchemeSelections import app.musikus.settings.domain.ThemeSelections -import app.musikus.activesession.domain.usecase.ActiveSessionUseCases import app.musikus.settings.domain.usecase.UserPreferencesUseCases import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import javax.inject.Inject @@ -40,12 +38,12 @@ data class MainUiState( var isSessionRunning: Boolean ) -@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class MainViewModel @Inject constructor( + private val application: Application, userPreferencesUseCases: UserPreferencesUseCases, activeSessionUseCases: ActiveSessionUseCases -) : ViewModel() { +) : AndroidViewModel(application) { /** @@ -105,30 +103,13 @@ class MainViewModel @Inject constructor( fun onUiEvent(event: MainUiEvent) { when (event) { is MainUiEvent.ShowSnackbar -> { - showSnackbar(event.message, event.onUndo) - } - } - } - - /** - * Private state mutators - */ - - private fun showSnackbar(message: String, onUndo: (() -> Unit)? = null) { - viewModelScope.launch { - val result = _snackbarHost.value.showSnackbar( - message, - actionLabel = if (onUndo != null) "Undo" else null, - duration = SnackbarDuration.Long - ) - when (result) { - SnackbarResult.ActionPerformed -> { - onUndo?.invoke() - } - - SnackbarResult.Dismissed -> { - // do nothing - } + showSnackbar( + context = application, + scope = viewModelScope, + hostState = _snackbarHost.value, + message = event.message, + onUndo = event.onUndo + ) } } } diff --git a/app/src/main/java/app/musikus/core/presentation/Screen.kt b/app/src/main/java/app/musikus/core/presentation/Screen.kt index 80d39b714..3d687a443 100644 --- a/app/src/main/java/app/musikus/core/presentation/Screen.kt +++ b/app/src/main/java/app/musikus/core/presentation/Screen.kt @@ -34,7 +34,7 @@ sealed class Screen( data object Sessions : HomeTab( subRoute = "sessions", displayData = DisplayData( - title = UiText.StringResource(R.string.navigationSessionsTitle), + title = UiText.StringResource(R.string.components_bottom_bar_items_sessions), icon = UiIcon.IconResource(R.drawable.ic_sessions), animatedIcon = R.drawable.avd_sessions, ) @@ -43,7 +43,7 @@ sealed class Screen( data object Goals : HomeTab( subRoute = "goals", displayData = DisplayData( - title = UiText.StringResource(R.string.navigationGoalsTitle), + title = UiText.StringResource(R.string.components_bottom_bar_items_goals), icon = UiIcon.IconResource(R.drawable.ic_goals), animatedIcon = R.drawable.avd_goals ) @@ -52,7 +52,7 @@ sealed class Screen( data object Statistics : HomeTab( subRoute = "statistics", displayData = DisplayData( - title = UiText.StringResource(R.string.navigationStatisticsTitle), + title = UiText.StringResource(R.string.components_bottom_bar_items_statistics), icon = UiIcon.IconResource(R.drawable.ic_bar_chart), animatedIcon = R.drawable.avd_bar_chart ) @@ -61,7 +61,7 @@ sealed class Screen( data object Library : HomeTab( subRoute = "library", displayData = DisplayData( - title = UiText.StringResource(R.string.navigationLibraryTitle), + title = UiText.StringResource(R.string.components_bottom_bar_items_library), icon = UiIcon.IconResource(R.drawable.ic_library), animatedIcon = R.drawable.avd_library ) @@ -103,7 +103,7 @@ sealed class Screen( data object About : SettingsOption( subRoute = "about", displayData = DisplayData( - title = UiText.StringResource(R.string.about_app_title), + title = UiText.StringResource(R.string.settings_items_about), icon = UiIcon.DynamicIcon(Icons.Outlined.Info), ) ) @@ -111,7 +111,7 @@ sealed class Screen( data object Help : SettingsOption( subRoute = "help", displayData = DisplayData( - title = UiText.StringResource(R.string.help_title), + title = UiText.StringResource(R.string.settings_items_help), icon = UiIcon.DynamicIcon(Icons.AutoMirrored.Outlined.Help), ) ) @@ -119,7 +119,7 @@ sealed class Screen( data object Backup : SettingsOption( subRoute = "backup", displayData = DisplayData( - title = UiText.StringResource(R.string.backup_title), + title = UiText.StringResource(R.string.settings_items_backup), icon = UiIcon.DynamicIcon(Icons.Outlined.CloudUpload), ) ) @@ -127,7 +127,7 @@ sealed class Screen( data object Export : SettingsOption( subRoute = "export", displayData = DisplayData( - title = UiText.DynamicString("Export session data"), + title = UiText.StringResource(R.string.settings_items_export), icon = UiIcon.IconResource(R.drawable.ic_export), ) ) @@ -135,7 +135,7 @@ sealed class Screen( data object Donate : SettingsOption( subRoute = "donate", displayData = DisplayData( - title = UiText.StringResource(R.string.donations_title), + title = UiText.StringResource(R.string.settings_items_donate), icon = UiIcon.DynamicIcon(Icons.Outlined.Favorite), ) ) @@ -143,7 +143,7 @@ sealed class Screen( data object Appearance : SettingsOption( subRoute = "appearance", displayData = DisplayData( - title = UiText.StringResource(R.string.appearance_title), + title = UiText.StringResource(R.string.settings_items_appearance), icon = UiIcon.IconResource(R.drawable.ic_appearance), ) ) diff --git a/app/src/main/java/app/musikus/core/presentation/components/ActionBar.kt b/app/src/main/java/app/musikus/core/presentation/components/ActionBar.kt index a1851310b..f7c87d17d 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/ActionBar.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/ActionBar.kt @@ -12,8 +12,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.musikus.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -26,7 +34,7 @@ fun ActionBar( onDeleteHandler: () -> Unit ) { TopAppBar( - title = { Text(text = "$numSelectedItems selected") }, + title = { Text(text = stringResource(id = R.string.components_action_bar_title, numSelectedItems)) }, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, @@ -37,7 +45,7 @@ fun ActionBar( IconButton(onClick = onDismissHandler) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Back", + contentDescription = stringResource(id = R.string.components_action_bar_back_button_description) ) } }, @@ -47,14 +55,14 @@ fun ActionBar( IconButton(onClick = onEditHandler) { Icon( imageVector = Icons.Rounded.Edit, - contentDescription = "Edit", + contentDescription = stringResource(id = R.string.components_action_bar_edit_button_description) ) } } IconButton(onClick = onDeleteHandler) { Icon( imageVector = Icons.Rounded.Delete, - contentDescription = "Delete", + contentDescription = stringResource(id = R.string.components_action_bar_delete_button_description) ) } } diff --git a/app/src/main/java/app/musikus/core/presentation/components/DeleteConfirmationBottomSheet.kt b/app/src/main/java/app/musikus/core/presentation/components/DeleteConfirmationBottomSheet.kt index f778ff87d..b4346dc55 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/DeleteConfirmationBottomSheet.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/DeleteConfirmationBottomSheet.kt @@ -30,12 +30,30 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import app.musikus.core.presentation.theme.spacing import app.musikus.core.presentation.utils.UiIcon import app.musikus.core.presentation.utils.UiText +@Composable +fun DeleteConfirmationBottomSheet( + explanation: UiText? = null, + confirmationIcon: ImageVector, + confirmationText: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + DeleteConfirmationBottomSheet( + explanation = explanation, + confirmationIcon = UiIcon.DynamicIcon(confirmationIcon), + confirmationText = UiText.DynamicString(confirmationText), + onDismiss = onDismiss, + onConfirm = onConfirm + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun DeleteConfirmationBottomSheet( diff --git a/app/src/main/java/app/musikus/core/presentation/components/DialogActions.kt b/app/src/main/java/app/musikus/core/presentation/components/DialogActions.kt index e86dd1300..01acbc55a 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/DialogActions.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/DialogActions.kt @@ -1,3 +1,11 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2024 Matthias Emde + */ + package app.musikus.core.presentation.components import androidx.compose.foundation.layout.Arrangement @@ -28,8 +36,8 @@ import app.musikus.core.presentation.theme.spacing fun DialogActions( onDismissHandler: () -> Unit, onConfirmHandler: () -> Unit, - confirmButtonText: String = stringResource(id = R.string.dialogConfirm), - dismissButtonText: String = stringResource(id = R.string.dialogDismiss), + confirmButtonText: String = stringResource(id = android.R.string.ok), + dismissButtonText: String = stringResource(id = android.R.string.cancel), confirmButtonEnabled: Boolean = true, ) { Row( diff --git a/app/src/main/java/app/musikus/core/presentation/components/DurationInput.kt b/app/src/main/java/app/musikus/core/presentation/components/DurationInput.kt index cb08f32b9..18b06784f 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/DurationInput.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/DurationInput.kt @@ -4,7 +4,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * Copyright (c) 2024 Matthias Emde - * */ package app.musikus.core.presentation.components diff --git a/app/src/main/java/app/musikus/core/presentation/components/FadingEdge.kt b/app/src/main/java/app/musikus/core/presentation/components/FadingEdge.kt index c396b5ae4..04193f669 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/FadingEdge.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/FadingEdge.kt @@ -1,14 +1,13 @@ -package app.musikus.core.presentation.components - /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * Copyright (c) 2024 Michael Prommersberger - * */ +package app.musikus.core.presentation.components + import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent diff --git a/app/src/main/java/app/musikus/core/presentation/components/MainMenu.kt b/app/src/main/java/app/musikus/core/presentation/components/MainMenu.kt index c71f35b19..bf77aa686 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/MainMenu.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/MainMenu.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.core.presentation.components @@ -12,8 +12,10 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import app.musikus.R enum class CommonMenuSelections { SETTINGS, @@ -24,7 +26,7 @@ fun CommonMenuItems( onSelection: (CommonMenuSelections) -> Unit ) { DropdownMenuItem( - text = { Text(text = "Settings") }, + text = { Text(text = stringResource(R.string.components_main_menu)) }, onClick = { onSelection(CommonMenuSelections.SETTINGS) } ) } diff --git a/app/src/main/java/app/musikus/core/presentation/components/SelectionSpinner.kt b/app/src/main/java/app/musikus/core/presentation/components/SelectionSpinner.kt index 82fb730d6..284eeb1fe 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/SelectionSpinner.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/SelectionSpinner.kt @@ -21,11 +21,12 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import app.musikus.core.presentation.utils.UiText import java.util.* -sealed class SelectionSpinnerOption(val name: String) -class UUIDSelectionSpinnerOption(val id: UUID?, name: String) : SelectionSpinnerOption(name) -class IntSelectionSpinnerOption(val id: Int?, name: String) : SelectionSpinnerOption(name) +sealed class SelectionSpinnerOption(val name: UiText) +class UUIDSelectionSpinnerOption(val id: UUID?, name: UiText) : SelectionSpinnerOption(name) +class IntSelectionSpinnerOption(val id: Int?, name: UiText) : SelectionSpinnerOption(name) @OptIn(ExperimentalMaterial3Api::class) @@ -60,7 +61,7 @@ fun SelectionSpinner( readOnly = true, modifier = Modifier .menuAnchor(), - value = selected?.name ?: "", // if no option is selected, show nothing + value = selected?.name?.asString() ?: "", // if no option is selected, show nothing onValueChange = {}, label = label, placeholder = placeholder, @@ -98,7 +99,7 @@ fun SelectionSpinner( modifier = Modifier .conditional(scrollBarShowing) { padding(end = 12.dp) } .height(singleDropdownItemHeight - 2.dp), - text = { Text(text = it.name) }, + text = { Text(text = it.name.asString()) }, onClick = { onSelectedChange(it) } ) HorizontalDivider( @@ -113,7 +114,7 @@ fun SelectionSpinner( .conditional(scrollBarShowing) { padding(end = 12.dp) } .height(singleDropdownItemHeight), onClick = { onSelectedChange(option) }, - text = { Text(text = option.name) } + text = { Text(text = option.name.asString()) } ) } } diff --git a/app/src/main/java/app/musikus/core/presentation/components/Snackbar.kt b/app/src/main/java/app/musikus/core/presentation/components/Snackbar.kt new file mode 100644 index 000000000..781a87399 --- /dev/null +++ b/app/src/main/java/app/musikus/core/presentation/components/Snackbar.kt @@ -0,0 +1,43 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2024 Matthias Emde + * + */ + +package app.musikus.core.presentation.components + +import android.content.Context +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import app.musikus.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +fun showSnackbar( + context: Context, + scope: CoroutineScope, + hostState: SnackbarHostState, + message: String, + onUndo: (() -> Unit)? = null +) { + scope.launch { + val result = hostState.showSnackbar( + message, + actionLabel = if (onUndo != null) context.getString(R.string.components_snackbar_undo) else null, + duration = SnackbarDuration.Long + ) + when (result) { + SnackbarResult.ActionPerformed -> { + onUndo?.invoke() + } + + SnackbarResult.Dismissed -> { + // do nothing + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/musikus/core/presentation/components/SortMenu.kt b/app/src/main/java/app/musikus/core/presentation/components/SortMenu.kt index e9c0cda85..f28b90fbd 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/SortMenu.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/SortMenu.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.core.presentation.components @@ -22,10 +22,12 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import app.musikus.R import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortMode @@ -40,16 +42,18 @@ fun > SortMenu( onShowMenuChanged: (Boolean) -> Unit, onSelectionHandler: (T) -> Unit ) { + val mainContentDescription = stringResource(id = R.string.components_sort_menu_content_description, sortItemDescription) + val dropdownContentDescription = stringResource(id = R.string.components_sort_menu_dropdown_content_description, sortItemDescription) TextButton( modifier = modifier.semantics { - contentDescription = "Select sort mode and direction for $sortItemDescription" + contentDescription = mainContentDescription }, onClick = { onShowMenuChanged(!show) }) { Text( modifier = Modifier.padding(end = 8.dp), color = MaterialTheme.colorScheme.onSurface, - text = currentSortMode.label + text = currentSortMode.label.asString() ) Icon( modifier = Modifier.size(20.dp), @@ -62,7 +66,7 @@ fun > SortMenu( ) DropdownMenu( modifier = Modifier.semantics { - contentDescription = "List of sort modes for $sortItemDescription" + contentDescription = dropdownContentDescription }, offset = DpOffset((-10).dp, 10.dp), expanded = show, @@ -71,7 +75,7 @@ fun > SortMenu( // Menu Header Text( modifier = Modifier.padding(12.dp), - text = "Sort by", + text = stringResource(id = R.string.components_sort_menu_title), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) ) @@ -93,7 +97,7 @@ fun > SortMenu( DropdownMenuItem( text = { Text( - text = sortMode.label, + text = sortMode.label.asString(), color = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified ) diff --git a/app/src/main/java/app/musikus/core/presentation/components/SwipeToDeleteContainer.kt b/app/src/main/java/app/musikus/core/presentation/components/SwipeToDeleteContainer.kt index 98d12cf03..f37e4e8ae 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/SwipeToDeleteContainer.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/SwipeToDeleteContainer.kt @@ -108,7 +108,7 @@ private fun SwipeToDeleteBackground( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete section", + contentDescription = null, // not needed since the icon can never be selected tint = iconColor, ) } diff --git a/app/src/main/java/app/musikus/core/presentation/components/ToggleButton.kt b/app/src/main/java/app/musikus/core/presentation/components/ToggleButton.kt index 853ad4fd6..2d9161f80 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/ToggleButton.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/ToggleButton.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.core.presentation.components @@ -16,14 +16,15 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import app.musikus.core.presentation.utils.UiText data class ToggleButtonOption( val id: Int, - val name: String + val name: UiText ) @Composable -fun MyToggleButton( +fun ToggleButton( modifier: Modifier = Modifier, options: List, selected: ToggleButtonOption, @@ -54,7 +55,7 @@ fun MyToggleButton( onClick = { onSelectedChanged(option) } ) { - Text(text = option.name) + Text(text = option.name.asString()) } } } diff --git a/app/src/main/java/app/musikus/core/presentation/components/TopBar.kt b/app/src/main/java/app/musikus/core/presentation/components/TopBar.kt index e20b5aceb..f0783594d 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/TopBar.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/TopBar.kt @@ -3,17 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde * - * Parts of this software are licensed under the MIT license - * - * Copyright (c) 2022, Javier Carbone, author Matthias Emde - * Additions and modifications, author Michael Prommersberger */ package app.musikus.core.presentation.components +import app.musikus.core.presentation.utils.UiText + interface TopBarUiState { - val title: String + val title: UiText val showBackButton: Boolean } \ No newline at end of file diff --git a/app/src/main/java/app/musikus/core/presentation/components/TwoLiner.kt b/app/src/main/java/app/musikus/core/presentation/components/TwoLiner.kt index 5941937c6..b95db5a8c 100644 --- a/app/src/main/java/app/musikus/core/presentation/components/TwoLiner.kt +++ b/app/src/main/java/app/musikus/core/presentation/components/TwoLiner.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.em import app.musikus.core.presentation.theme.spacing import app.musikus.core.presentation.utils.UiIcon import app.musikus.core.presentation.utils.UiText @@ -74,7 +75,8 @@ fun TwoLiner( text = it.asString(), style = LocalTextStyle.current, fontSize = LocalTextStyle.current.fontSize * 0.9f, - color = LocalContentColor.current.copy(alpha = 0.6f) + color = LocalContentColor.current.copy(alpha = 0.6f), + lineHeight = 1.2.em ) } } @@ -86,7 +88,10 @@ fun TwoLiner( .weight(1f) .widthIn(min = MaterialTheme.spacing.medium) ) - Icon(imageVector = it.asIcon(), contentDescription = null) + Icon( + imageVector = it.asIcon(), + contentDescription = data.firstLine?.asString() ?: data.secondLine?.asString() + ) } } } \ No newline at end of file diff --git a/app/src/main/java/app/musikus/core/presentation/utils/UiText.kt b/app/src/main/java/app/musikus/core/presentation/utils/UiText.kt index 3c791d1be..394180ddf 100644 --- a/app/src/main/java/app/musikus/core/presentation/utils/UiText.kt +++ b/app/src/main/java/app/musikus/core/presentation/utils/UiText.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.core.presentation.utils @@ -14,6 +14,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.core.text.HtmlCompat // source: https://www.youtube.com/watch?v=mB1Lej0aDus (Phillip Lackner) @@ -25,12 +30,18 @@ sealed class UiText { vararg val args: Any ): UiText() + class PluralResource( @PluralsRes val resId: Int, val quantity: Int, vararg val formatArgs: Any ): UiText() + class HtmlResource( + @StringRes val resId: Int, + vararg val args: Any + ): UiText() + @Composable fun asAnnotatedString(): AnnotatedString { return when(this) { @@ -38,6 +49,7 @@ sealed class UiText { is DynamicAnnotatedString -> value is StringResource -> AnnotatedString(stringResource(resId, *args)) is PluralResource -> AnnotatedString(pluralStringResource(resId, quantity, *formatArgs)) + is HtmlResource -> htmlResource(resId, *args) } } @@ -48,9 +60,44 @@ sealed class UiText { is DynamicAnnotatedString -> value.text is StringResource -> stringResource(resId, *args) is PluralResource -> pluralStringResource(resId, quantity, *formatArgs) + is HtmlResource -> throw IllegalArgumentException("Cannot convert HTML resource to string") } } } @Composable -fun List.asAnnotatedString() = map { it.asAnnotatedString() }.joinToString(separator = " ") { it } \ No newline at end of file +fun List.asAnnotatedString( + separator: String = " " +) = map { it.asAnnotatedString() }.joinToString(separator) { it } + + +// source: https://stackoverflow.com/questions/68549248/android-jetpack-compose-how-to-show-styled-text-from-string-resources +@Composable +fun htmlResource(@StringRes resId: Int, vararg formatArgs: Any): AnnotatedString { + val rawHtml = stringResource(resId, *formatArgs) + val spanned = HtmlCompat.fromHtml(rawHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) + return AnnotatedString.Builder().apply { + append(spanned.toString()) + spanned.getSpans(0, spanned.length, Any::class.java).forEach { span -> + println(span) + val start = spanned.getSpanStart(span) + val end = spanned.getSpanEnd(span) + + when (span) { + is android.text.style.StyleSpan -> { + when (span.style) { + android.graphics.Typeface.BOLD -> { + addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) + } + android.graphics.Typeface.ITALIC -> { + addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) + } + } + } + is android.text.style.UnderlineSpan -> { + addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end) + } + } + } + }.toAnnotatedString() +} diff --git a/app/src/main/java/app/musikus/goals/data/GoalSorting.kt b/app/src/main/java/app/musikus/goals/data/GoalSorting.kt new file mode 100644 index 000000000..672b3e48a --- /dev/null +++ b/app/src/main/java/app/musikus/goals/data/GoalSorting.kt @@ -0,0 +1,88 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2024 Matthias Emde + */ + +package app.musikus.goals.data + +import app.musikus.R +import app.musikus.core.data.GoalDescriptionWithInstancesAndLibraryItems +import app.musikus.core.data.GoalInstanceWithDescriptionWithLibraryItems +import app.musikus.core.domain.SortDirection +import app.musikus.core.domain.SortInfo +import app.musikus.core.domain.SortMode +import app.musikus.core.presentation.utils.UiText +import app.musikus.goals.data.daos.GoalDescription +import app.musikus.goals.data.daos.GoalInstance + +typealias GoalSortInfo = SortInfo> + +enum class GoalsSortMode : SortMode> { + DATE_ADDED { + override val label = UiText.StringResource(R.string.goals_goal_sort_mode_date_added) + override val comparator = compareBy> { (description, _) -> + description.createdAt + } + }, + TARGET { + override val label = UiText.StringResource(R.string.goals_goal_sort_mode_target) + override val comparator = compareBy> { (_, instance) -> + instance.target + } + }, + PERIOD { + override val label = UiText.StringResource(R.string.goals_goal_sort_mode_period) + override val comparator = compareBy> { (description, _) -> + description.periodUnit + }.thenBy { (description, _) -> + description.periodInPeriodUnits + } + }, +// CUSTOM { +// override val label = "Custom" +// override val comparator = compareBy> { TODO } +// } + ; + + override val isDefault: Boolean + get() = this == DEFAULT + + companion object { + val DEFAULT = DATE_ADDED + + fun valueOrDefault(string: String?) = try { + valueOf(string ?: "") + } catch (e: Exception) { + DEFAULT + } + } +} + +@JvmName("sortedGoalInstanceWithDescriptionWithLibraryItems") +fun List.sorted( + mode: GoalsSortMode, + direction: SortDirection +) : List = this.sortedWith( + when(direction) { + SortDirection.ASCENDING -> + compareBy (mode.comparator) { it.description.description to it.instance} + SortDirection.DESCENDING -> + compareByDescending(mode.comparator) { it.description.description to it.instance } + } +) + +@JvmName("sortedGoalDescriptionWithInstancesAndLibraryItems") +fun List.sorted( + mode: GoalsSortMode, + direction: SortDirection +) : List = this.sortedWith( + when(direction) { + SortDirection.ASCENDING -> + compareBy (mode.comparator) { it.description to it.latestInstance } + SortDirection.DESCENDING -> + compareByDescending(mode.comparator) { it.description to it.latestInstance } + } +) diff --git a/app/src/main/java/app/musikus/goals/data/daos/GoalDescriptionDao.kt b/app/src/main/java/app/musikus/goals/data/daos/GoalDescriptionDao.kt index a7543e6bd..d6319dbd9 100644 --- a/app/src/main/java/app/musikus/goals/data/daos/GoalDescriptionDao.kt +++ b/app/src/main/java/app/musikus/goals/data/daos/GoalDescriptionDao.kt @@ -73,7 +73,7 @@ data class GoalDescription( fun title(item: LibraryItem? = null) = item?.let { UiText.DynamicString(item.name) - } ?: UiText.StringResource(R.string.goal_name_non_specific) + } ?: UiText.StringResource(R.string.goals_goal_type_non_specific) fun subtitle(instance: GoalInstance) = listOf( UiText.DynamicAnnotatedString( @@ -81,9 +81,9 @@ data class GoalDescription( ), UiText.PluralResource( resId = when(periodUnit) { - GoalPeriodUnit.DAY ->R.plurals.time_period_day - GoalPeriodUnit.WEEK -> R.plurals.time_period_week - GoalPeriodUnit.MONTH -> R.plurals.time_period_month + GoalPeriodUnit.DAY -> R.plurals.goals_goal_subtitle_day + GoalPeriodUnit.WEEK -> R.plurals.goals_goal_subtitle_week + GoalPeriodUnit.MONTH -> R.plurals.goals_goal_subtitle_month }, quantity = periodInPeriodUnits, periodInPeriodUnits // argument used in the format string diff --git a/app/src/main/java/app/musikus/goals/data/entities/GoalDescription.kt b/app/src/main/java/app/musikus/goals/data/entities/GoalDescription.kt index f342ca21a..a8dafe040 100644 --- a/app/src/main/java/app/musikus/goals/data/entities/GoalDescription.kt +++ b/app/src/main/java/app/musikus/goals/data/entities/GoalDescription.kt @@ -10,12 +10,14 @@ package app.musikus.goals.data.entities import androidx.room.ColumnInfo import androidx.room.Entity +import app.musikus.R import app.musikus.core.data.entities.ISoftDeleteModelCreationAttributes import app.musikus.core.data.entities.ISoftDeleteModelUpdateAttributes import app.musikus.core.data.entities.SoftDeleteModel import app.musikus.core.data.entities.SoftDeleteModelCreationAttributes import app.musikus.core.data.entities.SoftDeleteModelUpdateAttributes import app.musikus.core.data.Nullable +import app.musikus.core.presentation.utils.UiText // shows, whether a goal will count all sections // or only the one from specific libraryItems @@ -25,9 +27,9 @@ enum class GoalType { companion object { val DEFAULT = NON_SPECIFIC } - override fun toString() = when (this) { - NON_SPECIFIC -> "All items" - ITEM_SPECIFIC -> "Specific item" + fun toUiText() = when (this) { + NON_SPECIFIC -> UiText.StringResource(R.string.goals_goal_type_non_specific) + ITEM_SPECIFIC -> UiText.StringResource(R.string.goals_goal_type_item_specific) } } @@ -39,9 +41,9 @@ enum class GoalProgressType { companion object { val DEFAULT = TIME } - override fun toString() = when (this) { - TIME -> "Time" - SESSION_COUNT -> "Sessions" + fun toUiText() = when (this) { + TIME -> UiText.StringResource(R.string.goals_goal_progress_type_time) + SESSION_COUNT -> UiText.StringResource(R.string.goals_goal_progress_type_session_count) } } @@ -52,10 +54,10 @@ enum class GoalPeriodUnit { val DEFAULT = DAY } - override fun toString() = when (this) { - DAY -> "Day" - WEEK -> "Week" - MONTH -> "Month" + fun toUiText() = when (this) { + DAY -> UiText.StringResource(R.string.goals_goal_period_unit_day) + WEEK -> UiText.StringResource(R.string.goals_goal_period_unit_week) + MONTH -> UiText.StringResource(R.string.goals_goal_period_unit_month) } } diff --git a/app/src/main/java/app/musikus/goals/domain/usecase/SortGoalsUseCase.kt b/app/src/main/java/app/musikus/goals/domain/usecase/SortGoalsUseCase.kt index c46db0057..019f11417 100644 --- a/app/src/main/java/app/musikus/goals/domain/usecase/SortGoalsUseCase.kt +++ b/app/src/main/java/app/musikus/goals/domain/usecase/SortGoalsUseCase.kt @@ -3,16 +3,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.goals.domain.usecase import app.musikus.core.data.GoalDescriptionWithInstancesAndLibraryItems import app.musikus.core.data.GoalInstanceWithDescriptionWithLibraryItems +import app.musikus.goals.data.GoalsSortMode +import app.musikus.goals.data.sorted import app.musikus.settings.domain.usecase.GetGoalSortInfoUseCase -import app.musikus.core.domain.GoalsSortMode -import app.musikus.core.domain.sorted import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine diff --git a/app/src/main/java/app/musikus/goals/presentation/GoalCard.kt b/app/src/main/java/app/musikus/goals/presentation/GoalCard.kt index 745d741d2..5c0f090a6 100644 --- a/app/src/main/java/app/musikus/goals/presentation/GoalCard.kt +++ b/app/src/main/java/app/musikus/goals/presentation/GoalCard.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.goals.presentation @@ -94,8 +94,8 @@ fun GoalCard( imageVector = if(description.repeat) Icons.Rounded.Repeat else Icons.Filled.LocalFireDepartment, - contentDescription = if(description.repeat) - "Regular goal" else "One shot goal", + contentDescription = stringResource(id = if(description.repeat) + R.string.goals_repeating else R.string.goals_non_repeating), tint = libraryItemColor ?: MaterialTheme.colorScheme.primary ) @@ -135,7 +135,7 @@ fun GoalCard( modifier = Modifier.padding(8.dp), maxLines = 1, text= stringResource( - R.string.time_left, + R.string.core_time_left, getDurationString(remainingTime, DurationFormat.PRETTY_APPROX) ) ) @@ -161,7 +161,7 @@ fun GoalCard( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh / goal.instance.targetSeconds ), - label = "animated goal progress" + label = "animatedGoalProgress" ) val animatedProgressLeft = targetSeconds - animatedProgress @@ -221,7 +221,7 @@ fun GoalCard( Text( modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, - text = stringResource(id = R.string.goal_description_achieved), + text = stringResource(id = R.string.goals_goal_card_achieved), style = MaterialTheme.typography.titleLarge.copy( color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold @@ -237,14 +237,13 @@ fun GoalCard( .size(72.dp) .align(Alignment.Center), imageVector = Icons.Default.Pause, - contentDescription = "Paused Goal", + contentDescription = "", tint = MaterialTheme.colorScheme.onSurface ) Box( modifier = Modifier .matchParentSize() .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.2f)) - ) } } diff --git a/app/src/main/java/app/musikus/goals/presentation/GoalDialog.kt b/app/src/main/java/app/musikus/goals/presentation/GoalDialog.kt index 62a770b7d..2cb361027 100644 --- a/app/src/main/java/app/musikus/goals/presentation/GoalDialog.kt +++ b/app/src/main/java/app/musikus/goals/presentation/GoalDialog.kt @@ -3,8 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde - * + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.goals.presentation @@ -39,7 +38,7 @@ import app.musikus.core.presentation.components.DialogActions import app.musikus.core.presentation.components.DialogHeader import app.musikus.core.presentation.components.DurationInput import app.musikus.core.presentation.components.IntSelectionSpinnerOption -import app.musikus.core.presentation.components.MyToggleButton +import app.musikus.core.presentation.components.ToggleButton import app.musikus.core.presentation.components.NumberInput import app.musikus.core.presentation.components.SelectionSpinner import app.musikus.core.presentation.components.ToggleButtonOption @@ -50,6 +49,7 @@ import app.musikus.goals.data.entities.GoalType import app.musikus.library.presentation.DialogMode import app.musikus.core.presentation.theme.spacing import app.musikus.core.presentation.utils.TestTags +import app.musikus.core.presentation.utils.UiText import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -98,7 +98,9 @@ fun GoalDialog( ) { DialogHeader( title = stringResource( - id = if(isEditMode) R.string.goalDialogTitleEdit else R.string.addGoalDialogTitle + id = + if(isEditMode) R.string.goals_goal_dialog_title_edit + else R.string.goals_goal_dialog_title ) ) @@ -133,14 +135,14 @@ fun GoalDialog( Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) - MyToggleButton( + ToggleButton( modifier = Modifier.padding(horizontal = MaterialTheme.spacing.large), options = GoalType.entries.map { - ToggleButtonOption(it.ordinal, it.toString()) + ToggleButtonOption(it.ordinal, it.toUiText()) }, selected = ToggleButtonOption( dialogData.goalType.ordinal, - dialogData.goalType.toString() + dialogData.goalType.toUiText() ), onSelectedChanged = { option -> eventHandler(GoalDialogUiEvent.GoalTypeChanged(GoalType.entries[option.id])) @@ -155,14 +157,18 @@ fun GoalDialog( if (libraryItems.isNotEmpty()) { SelectionSpinner( expanded = libraryItemsSelectorExpanded, - placeholder = { Text(text = "Select a library item") }, + placeholder = { + Text(text = stringResource( + id = R.string.goals_goal_dialog_item_selector_placeholder + )) + }, options = libraryItems.map { - UUIDSelectionSpinnerOption(it.id, it.name) + UUIDSelectionSpinnerOption(it.id, UiText.DynamicString(it.name)) }, selected = dialogData.selectedLibraryItems.firstOrNull()?.let { - UUIDSelectionSpinnerOption(it.id, it.name) + UUIDSelectionSpinnerOption(it.id, UiText.DynamicString(it.name)) }, - semanticDescription = "Select library item", + semanticDescription = stringResource(id = R.string.goals_goal_dialog_item_selector_description), dropdownTestTag = TestTags.GOAL_DIALOG_ITEM_SELECTOR_DROPDOWN, onExpandedChange = { libraryItemsSelectorExpanded = it @@ -176,7 +182,7 @@ fun GoalDialog( } ) } else { - Text(text = "No items in your library.") + Text(text = stringResource(id = R.string.goals_goal_dialog_item_selector_no_items)) } } @@ -190,7 +196,10 @@ fun GoalDialog( onConfirmHandler = { eventHandler(GoalDialogUiEvent.Confirm) }, onDismissHandler = { eventHandler(GoalDialogUiEvent.Dismiss) }, confirmButtonEnabled = confirmButtonEnabled, - confirmButtonText = if (isEditMode) "Edit" else "Create" + confirmButtonText = stringResource( id = + if (isEditMode) R.string.goals_goal_dialog_confirm_edit + else R.string.goals_goal_dialog_confirm + ) ) } } @@ -210,7 +219,7 @@ fun PeriodInput( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - Text(text = "in") + Text(text = stringResource(id = R.string.goals_goal_dialog_period_input_prefix)) Spacer(modifier = Modifier.width(8.dp)) NumberInput( value = periodInPeriodUnits.toString(), @@ -223,9 +232,9 @@ fun PeriodInput( SelectionSpinner( modifier = Modifier.width(130.dp), expanded = periodUnitSelectorExpanded, - options = GoalPeriodUnit.entries.map { IntSelectionSpinnerOption(it.ordinal, it.toString()) }, - selected = IntSelectionSpinnerOption(periodUnit.ordinal, periodUnit.toString()), - semanticDescription = "Select period unit", + options = GoalPeriodUnit.entries.map { IntSelectionSpinnerOption(it.ordinal, it.toUiText()) }, + selected = IntSelectionSpinnerOption(periodUnit.ordinal, periodUnit.toUiText()), + semanticDescription = stringResource(id = R.string.goals_goal_dialog_period_input_description), dropdownTestTag = TestTags.GOAL_DIALOG_PERIOD_UNIT_SELECTOR_DROPDOWN, onExpandedChange = onPeriodUnitSelectorExpandedChanged, onSelectedChange = { selection -> diff --git a/app/src/main/java/app/musikus/goals/presentation/GoalsScreen.kt b/app/src/main/java/app/musikus/goals/presentation/GoalsScreen.kt index ef913e6eb..62f6151a2 100644 --- a/app/src/main/java/app/musikus/goals/presentation/GoalsScreen.kt +++ b/app/src/main/java/app/musikus/goals/presentation/GoalsScreen.kt @@ -68,10 +68,10 @@ import app.musikus.core.presentation.components.MultiFabState import app.musikus.core.presentation.components.Selectable import app.musikus.core.presentation.components.SortMenu import app.musikus.core.presentation.theme.spacing -import app.musikus.core.domain.GoalsSortMode import app.musikus.core.domain.TimeProvider import app.musikus.core.presentation.utils.UiIcon import app.musikus.core.presentation.utils.UiText +import app.musikus.goals.data.GoalsSortMode @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -113,14 +113,14 @@ fun GoalsScreen( homeEventHandler(HomeUiEvent.CollapseMultiFab) } }, - contentDescription = "Add", + contentDescription = stringResource(id = R.string.goals_screen_multi_fab_description), miniFABs = listOf( MiniFABData( onClick = { eventHandler(GoalsUiEvent.AddGoalButtonPressed(oneShot = true)) homeEventHandler(HomeUiEvent.CollapseMultiFab) }, - label = "One shot goal", + label = stringResource(id = R.string.goals_non_repeating), icon = Icons.Filled.LocalFireDepartment, ), MiniFABData( @@ -128,7 +128,7 @@ fun GoalsScreen( eventHandler(GoalsUiEvent.AddGoalButtonPressed(oneShot = false)) homeEventHandler(HomeUiEvent.CollapseMultiFab) }, - label = "Regular goal", + label = stringResource(id = R.string.goals_repeating), icon = Icons.Rounded.Repeat, ) ) @@ -138,7 +138,7 @@ fun GoalsScreen( // TODO find a way to re-use Composable in every screen val topBarUiState = uiState.topBarUiState LargeTopAppBar( - title = { Text(text = topBarUiState.title) }, + title = { Text(text = topBarUiState.title.asString()) }, scrollBehavior = scrollBehavior, actions = { val sortMenuUiState = topBarUiState.sortMenuUiState @@ -147,7 +147,7 @@ fun GoalsScreen( sortModes = GoalsSortMode.entries, currentSortMode = sortMenuUiState.mode, currentSortDirection = sortMenuUiState.direction, - sortItemDescription = "goals", + sortItemDescription = stringResource(id = R.string.goals_screen_top_bar_sort_menu_item_description), onShowMenuChanged = { eventHandler(GoalsUiEvent.GoalSortMenuPressed) }, onSelectionHandler = { eventHandler(GoalsUiEvent.GoalSortModeSelected(it)) } ) @@ -155,7 +155,10 @@ fun GoalsScreen( IconButton(onClick = { homeEventHandler(HomeUiEvent.ShowMainMenu) }) { - Icon(Icons.Default.MoreVert, contentDescription = "more") + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(id = R.string.core_kebab_menu_description) + ) } MainMenu( show = homeUiState.showMainMenu, @@ -184,7 +187,7 @@ fun GoalsScreen( IconButton(onClick = { eventHandler(GoalsUiEvent.ArchiveButtonPressed) }) { Icon( imageVector = Icons.Rounded.Archive, - contentDescription = "Archive", + contentDescription = stringResource(id = R.string.components_action_bar_archive_button_description), ) } }, @@ -247,7 +250,7 @@ fun GoalsScreen( contentAlignment = Alignment.Center ) { Text( - text = stringResource(id = R.string.goalsFragmentHint), + text = stringResource(id = R.string.goals_screen_hint), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center ) @@ -276,22 +279,38 @@ fun GoalsScreen( val deleteOrArchiveDialogUiState = dialogUiState.deleteOrArchiveDialogUiState if (deleteOrArchiveDialogUiState != null) { + val snackbarMessage = stringResource( + id = if (deleteOrArchiveDialogUiState.isArchiveAction) + R.string.goals_screen_snackbar_archived else + R.string.goals_screen_snackbar_deleted + ) + DeleteConfirmationBottomSheet( - explanation = UiText.DynamicString( - if(deleteOrArchiveDialogUiState.isArchiveAction) { - "Archive goals? They will remain in your statistics but progress towards them will no longer be tracked." - } else { - "Delete goals? They will be erased from your statistics and cannot be restored. If you want to keep your statistics, consider archiving them instead." - } + explanation = UiText.PluralResource( + resId = + if(deleteOrArchiveDialogUiState.isArchiveAction) + R.plurals.goals_screen_archive_goal_dialog_explanation + else + R.plurals.goals_screen_delete_goal_dialog_explanation, + quantity = deleteOrArchiveDialogUiState.numberOfSelections, + deleteOrArchiveDialogUiState.numberOfSelections + ), + confirmationIcon = UiIcon.DynamicIcon( + if(deleteOrArchiveDialogUiState.isArchiveAction) Icons.Rounded.Archive + else Icons.Rounded.Delete + ), + confirmationText = UiText.StringResource( + resId = if(deleteOrArchiveDialogUiState.isArchiveAction) + R.string.goals_screen_archive_goal_dialog_confirm else + R.string.goals_screen_delete_goal_dialog_confirm, + deleteOrArchiveDialogUiState.numberOfSelections ), - confirmationIcon = UiIcon.DynamicIcon(if(deleteOrArchiveDialogUiState.isArchiveAction) Icons.Rounded.Archive else Icons.Rounded.Delete), - confirmationText = UiText.DynamicString("${if(deleteOrArchiveDialogUiState.isArchiveAction) "Archive" else "Delete"} forever (${deleteOrArchiveDialogUiState.numberOfSelections})"), onDismiss = { eventHandler(GoalsUiEvent.DeleteOrArchiveDialogDismissed) }, onConfirm = { eventHandler(GoalsUiEvent.DeleteOrArchiveDialogConfirmed) mainEventHandler( MainUiEvent.ShowSnackbar( - message = if (deleteOrArchiveDialogUiState.isArchiveAction) "Archived" else "Deleted", + message = snackbarMessage, onUndo = { eventHandler(GoalsUiEvent.UndoButtonPressed) } )) } @@ -299,7 +318,6 @@ fun GoalsScreen( } // Content Scrim for multiFAB - AnimatedVisibility( modifier = Modifier.zIndex(1f), visible = homeUiState.multiFabState == MultiFabState.EXPANDED, diff --git a/app/src/main/java/app/musikus/goals/presentation/GoalsUiEvent.kt b/app/src/main/java/app/musikus/goals/presentation/GoalsUiEvent.kt index 2e7b1b50a..1d85ee20c 100644 --- a/app/src/main/java/app/musikus/goals/presentation/GoalsUiEvent.kt +++ b/app/src/main/java/app/musikus/goals/presentation/GoalsUiEvent.kt @@ -8,8 +8,8 @@ package app.musikus.goals.presentation +import app.musikus.goals.data.GoalsSortMode import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibraryItems -import app.musikus.core.domain.GoalsSortMode typealias GoalsUiEventHandler = (GoalsUiEvent) -> Unit diff --git a/app/src/main/java/app/musikus/goals/presentation/GoalsUiState.kt b/app/src/main/java/app/musikus/goals/presentation/GoalsUiState.kt index b58359846..f4bcd384c 100644 --- a/app/src/main/java/app/musikus/goals/presentation/GoalsUiState.kt +++ b/app/src/main/java/app/musikus/goals/presentation/GoalsUiState.kt @@ -12,8 +12,9 @@ import app.musikus.core.presentation.components.TopBarUiState import app.musikus.library.data.daos.LibraryItem import app.musikus.library.presentation.DialogMode import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibraryItems -import app.musikus.core.domain.GoalsSortMode import app.musikus.core.domain.SortDirection +import app.musikus.core.presentation.utils.UiText +import app.musikus.goals.data.GoalsSortMode import java.util.UUID data class GoalsSortMenuUiState( @@ -24,7 +25,7 @@ data class GoalsSortMenuUiState( ) data class GoalsTopBarUiState( - override val title: String, + override val title: UiText, override val showBackButton: Boolean, val sortMenuUiState: GoalsSortMenuUiState, ) : TopBarUiState diff --git a/app/src/main/java/app/musikus/goals/presentation/GoalsViewModel.kt b/app/src/main/java/app/musikus/goals/presentation/GoalsViewModel.kt index fd8d8f059..ffd33ada8 100644 --- a/app/src/main/java/app/musikus/goals/presentation/GoalsViewModel.kt +++ b/app/src/main/java/app/musikus/goals/presentation/GoalsViewModel.kt @@ -3,13 +3,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.goals.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.musikus.R import app.musikus.library.data.daos.LibraryItem import app.musikus.goals.data.entities.GoalDescriptionCreationAttributes import app.musikus.goals.data.entities.GoalInstanceCreationAttributes @@ -21,9 +22,10 @@ import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibrar import app.musikus.goals.domain.usecase.GoalsUseCases import app.musikus.library.domain.usecase.LibraryUseCases import app.musikus.settings.domain.usecase.UserPreferencesUseCases -import app.musikus.core.domain.GoalsSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.core.presentation.utils.UiText +import app.musikus.goals.data.GoalsSortMode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -125,7 +127,7 @@ class GoalsViewModel @Inject constructor( private val topBarUiState = sortMenuUiState.map { sortMenuUiState -> GoalsTopBarUiState( - title = "Goals", + title = UiText.StringResource(R.string.goals_title), showBackButton = false, sortMenuUiState = sortMenuUiState, ) @@ -133,7 +135,7 @@ class GoalsViewModel @Inject constructor( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = GoalsTopBarUiState( - title = "Goals", + title = UiText.StringResource(R.string.goals_title), showBackButton = false, sortMenuUiState = sortMenuUiState.value, ) diff --git a/app/src/main/java/app/musikus/library/data/LibrarySorting.kt b/app/src/main/java/app/musikus/library/data/LibrarySorting.kt new file mode 100644 index 000000000..3ab16b4df --- /dev/null +++ b/app/src/main/java/app/musikus/library/data/LibrarySorting.kt @@ -0,0 +1,112 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2024 Matthias Emde + */ + +package app.musikus.library.data + +import app.musikus.R +import app.musikus.core.data.LibraryFolderWithItems +import app.musikus.core.domain.SortDirection +import app.musikus.core.domain.SortMode +import app.musikus.core.presentation.utils.UiText +import app.musikus.library.data.daos.LibraryFolder +import app.musikus.library.data.daos.LibraryItem + + +enum class LibraryItemSortMode : SortMode { + DATE_ADDED { + override val label = UiText.StringResource(R.string.library_item_sort_mode_date_added) + override val comparator = compareBy { it.createdAt } + }, + LAST_MODIFIED { + override val label = UiText.StringResource(R.string.library_item_sort_mode_last_modified) + override val comparator = compareBy { it.modifiedAt } + }, + NAME { + override val label = UiText.StringResource(R.string.library_item_sort_mode_name) + override val comparator = compareBy { it.name } + }, + COLOR { + override val label = UiText.StringResource(R.string.library_item_sort_mode_color) + override val comparator = compareBy { it.colorIndex } + }, +// CUSTOM { +// override val label = "Custom" +// override val comparator = compareBy { TODO } +// } + ; + + override val isDefault: Boolean + get() = this == DEFAULT + + companion object { + val DEFAULT = DATE_ADDED + + fun valueOrDefault(string: String?) = try { + valueOf(string ?: "") + } catch (e: Exception) { + DEFAULT + } + } +} + +fun List.sorted( + mode: LibraryItemSortMode, + direction: SortDirection +) = this.sortedWith ( + when(direction) { + SortDirection.ASCENDING -> + compareBy (mode.comparator) { it } + SortDirection.DESCENDING -> + compareByDescending(mode.comparator) { it } + } +) + +enum class LibraryFolderSortMode : SortMode { + DATE_ADDED { + override val label = UiText.StringResource(R.string.library_folder_sort_mode_date_added) + override val comparator = compareBy { it.createdAt } + }, + LAST_MODIFIED { + override val label = UiText.StringResource(R.string.library_folder_sort_mode_last_modified) + override val comparator = compareBy { it.modifiedAt } + }, + NAME { + override val label = UiText.StringResource(R.string.library_folder_sort_mode_name) + override val comparator = compareBy { it.name } + }, +// CUSTOM { +// override val label = "Custom" +// override val comparator = compareBy { TODO } +// } + ; + + override val isDefault: Boolean + get() = this == DEFAULT + + companion object { + val DEFAULT = DATE_ADDED + + fun valueOrDefault(string: String?) = try { + valueOf(string ?: "") + } catch (e: Exception) { + DEFAULT + } + } +} + +fun List.sorted( + mode: LibraryFolderSortMode, + direction: SortDirection, +) = this.sortedWith( + when (direction) { + SortDirection.ASCENDING -> + compareBy(mode.comparator) { it.folder } + SortDirection.DESCENDING -> + compareByDescending(mode.comparator) { it.folder } + } +) \ No newline at end of file diff --git a/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCase.kt b/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCase.kt index 508209dbc..3d39f43da 100644 --- a/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCase.kt +++ b/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCase.kt @@ -3,14 +3,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.library.domain.usecase import app.musikus.core.data.LibraryFolderWithItems -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.sorted +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.sorted import app.musikus.library.domain.LibraryRepository import app.musikus.settings.domain.usecase.GetFolderSortInfoUseCase import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCase.kt b/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCase.kt index 577c2fb86..5e03e6993 100644 --- a/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCase.kt +++ b/app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCase.kt @@ -3,17 +3,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.library.domain.usecase import app.musikus.core.data.Nullable +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryItem +import app.musikus.library.data.sorted import app.musikus.library.domain.LibraryRepository import app.musikus.settings.domain.usecase.GetItemSortInfoUseCase -import app.musikus.core.domain.LibraryItemSortMode -import app.musikus.core.domain.sorted import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map diff --git a/app/src/main/java/app/musikus/library/presentation/LibraryDialogs.kt b/app/src/main/java/app/musikus/library/presentation/LibraryDialogs.kt index 0cb14691b..ce2ac7934 100644 --- a/app/src/main/java/app/musikus/library/presentation/LibraryDialogs.kt +++ b/app/src/main/java/app/musikus/library/presentation/LibraryDialogs.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.library.presentation @@ -48,6 +48,7 @@ import app.musikus.core.presentation.components.UUIDSelectionSpinnerOption import app.musikus.library.data.daos.LibraryFolder import app.musikus.core.presentation.theme.libraryItemColors import app.musikus.core.presentation.utils.TestTags +import app.musikus.core.presentation.utils.UiText import java.util.UUID @@ -62,10 +63,11 @@ fun LibraryFolderDialog( .clip(MaterialTheme.shapes.extraLarge) .background(MaterialTheme.colorScheme.surface) ) { - DialogHeader(title = when(uiState.mode) { - DialogMode.ADD -> "Create folder" - DialogMode.EDIT -> "Edit folder" - }, + DialogHeader( + title = stringResource(id = when (uiState.mode) { + DialogMode.ADD -> R.string.library_folder_dialog_title + DialogMode.EDIT ->R.string.library_folder_dialog_title_edit + }), ) Column { OutlinedTextField( @@ -74,14 +76,14 @@ fun LibraryFolderDialog( .testTag(TestTags.FOLDER_DIALOG_NAME_INPUT), value = uiState.folderData.name, onValueChange = { eventHandler(LibraryUiEvent.FolderDialogNameChanged(it)) }, - label = { Text(text = "Folder name") }, + label = { Text(text = stringResource(id = R.string.library_folder_dialog_name_label)) }, singleLine = true, ) DialogActions( - confirmButtonText = when(uiState.mode) { - DialogMode.ADD -> "Create" - DialogMode.EDIT -> "Edit" - }, + confirmButtonText = stringResource(id = when(uiState.mode) { + DialogMode.ADD -> R.string.library_folder_dialog_confirm + DialogMode.EDIT -> R.string.library_folder_dialog_confirm_edit + }), onDismissHandler = { eventHandler(LibraryUiEvent.FolderDialogDismissed) }, onConfirmHandler = { eventHandler(LibraryUiEvent.FolderDialogConfirmed) }, confirmButtonEnabled = uiState.folderData.name.isNotEmpty() @@ -123,8 +125,8 @@ fun LibraryItemDialog( .background(MaterialTheme.colorScheme.surface) ) { DialogHeader(title = when(uiState.mode) { - DialogMode.ADD -> stringResource(id = R.string.addLibraryItemDialogTitle) - DialogMode.EDIT -> stringResource(id = R.string.addLibraryItemDialogTitleEdit) + DialogMode.ADD -> stringResource(id = R.string.library_item_dialog_title) + DialogMode.EDIT -> stringResource(id = R.string.library_item_dialog_title_edit) }) Column { OutlinedTextField( @@ -136,11 +138,11 @@ fun LibraryItemDialog( leadingIcon = { Icon( imageVector = Icons.Default.MusicNote, - contentDescription = "Item name", + contentDescription = stringResource(id = R.string.library_item_dialog_name_label), tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) ) }, - label = { Text(text = "Item name") }, + label = { Text(text = stringResource(id = R.string.library_item_dialog_name_label)) }, singleLine = true, ) if(uiState.folders.isNotEmpty()) { @@ -149,22 +151,27 @@ fun LibraryItemDialog( .padding(top = 16.dp) .padding(horizontal = 24.dp), expanded = folderSelectorExpanded, - label = { Text(text = "Folder") }, + label = { Text(text = stringResource(id = R.string.library_item_dialog_folder_selector_label)) }, leadingIcon = { Icon( imageVector = Icons.Default.Folder, - contentDescription = "Folder", + contentDescription = stringResource(id = R.string.library_item_dialog_folder_selector_label), tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) ) }, - options = uiState.folders.map { folder -> UUIDSelectionSpinnerOption(folder.id, folder.name) }, + options = uiState.folders.map { folder -> + UUIDSelectionSpinnerOption(folder.id, UiText.DynamicString(folder.name)) + }, selected = UUIDSelectionSpinnerOption( id = uiState.itemData.folderId, name = uiState.folders.firstOrNull { it.id == uiState.itemData.folderId - }?.name ?: "No folder" ), - specialOption = UUIDSelectionSpinnerOption(null, "No folder"), - semanticDescription = "Select folder", + }?.name?.let { UiText.DynamicString(it) } ?: UiText.StringResource(R.string.library_item_dialog_folder_selector_no_folder) ), + specialOption = UUIDSelectionSpinnerOption( + null, + UiText.StringResource(R.string.library_item_dialog_folder_selector_no_folder) + ), + semanticDescription = stringResource(id = R.string.library_item_dialog_folder_selector_description), dropdownTestTag = TestTags.ITEM_DIALOG_FOLDER_SELECTOR_DROPDOWN, onExpandedChange = { folderSelectorExpanded = it }, onSelectedChange = { @@ -189,7 +196,7 @@ fun LibraryItemDialog( ColorSelectRadioButton( color = libraryItemColors[index], selected = uiState.itemData.colorIndex == index, - colorDescription = "Color ${index+1}", + colorDescription = stringResource(id = R.string.library_item_dialog_color_selector_description, (index + 1)), onClick = { eventHandler(LibraryItemDialogUiEvent.ColorIndexChanged(index)) } ) } @@ -199,8 +206,8 @@ fun LibraryItemDialog( } DialogActions( confirmButtonText = when(uiState.mode) { - DialogMode.ADD -> "Create" - DialogMode.EDIT -> "Edit" + DialogMode.ADD -> stringResource(id = R.string.library_item_dialog_confirm) + DialogMode.EDIT -> stringResource(id = R.string.library_item_dialog_confirm_edit) }, confirmButtonEnabled = uiState.isConfirmButtonEnabled, onDismissHandler = { eventHandler(LibraryItemDialogUiEvent.Dismissed) }, diff --git a/app/src/main/java/app/musikus/library/presentation/LibraryScreen.kt b/app/src/main/java/app/musikus/library/presentation/LibraryScreen.kt index 74f1c2385..061ed19e9 100644 --- a/app/src/main/java/app/musikus/library/presentation/LibraryScreen.kt +++ b/app/src/main/java/app/musikus/library/presentation/LibraryScreen.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -94,11 +95,11 @@ import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem import app.musikus.settings.domain.ColorSchemeSelections import app.musikus.core.domain.DateFormat -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.presentation.utils.UiIcon import app.musikus.core.presentation.utils.UiText import app.musikus.core.domain.musikusFormat +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import java.time.ZonedDateTime @OptIn(ExperimentalMaterial3Api::class) @@ -142,7 +143,7 @@ fun Library( homeEventHandler(HomeUiEvent.CollapseMultiFab) }, ) { - Icon(Icons.Default.Add, contentDescription = "Add item") + Icon(Icons.Default.Add, contentDescription = stringResource(id = R.string.library_screen_fab_description)) } } else { MultiFAB( @@ -155,14 +156,14 @@ fun Library( homeEventHandler(HomeUiEvent.CollapseMultiFab) } }, - contentDescription = "Add", + contentDescription = stringResource(id = R.string.library_screen_multi_fab_description), miniFABs = listOf( MiniFABData( onClick = { eventHandler(LibraryUiEvent.AddItemButtonPressed) homeEventHandler(HomeUiEvent.CollapseMultiFab) }, - label = "Item", + label = stringResource(id = R.string.library_screen_multi_fab_item_description), icon = Icons.Rounded.MusicNote ), MiniFABData( @@ -170,7 +171,7 @@ fun Library( eventHandler(LibraryUiEvent.AddFolderButtonPressed) homeEventHandler(HomeUiEvent.CollapseMultiFab) }, - label = "Folder", + label = stringResource(id = R.string.library_screen_multi_fab_folder_description), icon = Icons.Rounded.Folder ) ) @@ -181,11 +182,16 @@ fun Library( val topBarUiState = uiState.topBarUiState LargeTopAppBar( scrollBehavior = scrollBehavior, - title = { Text(text = topBarUiState.title) }, + title = { Text(text = topBarUiState.title.asString()) }, navigationIcon = { if(topBarUiState.showBackButton) { IconButton(onClick = { eventHandler(LibraryUiEvent.BackButtonPressed) }) { - Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource( + id = R.string.components_top_bar_back_description + ) + ) } } }, @@ -193,7 +199,10 @@ fun Library( IconButton(onClick = { homeEventHandler(HomeUiEvent.ShowMainMenu) }) { - Icon(Icons.Default.MoreVert, contentDescription = "more") + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(id = R.string.core_kebab_menu_description) + ) MainMenu ( show = homeUiState.showMainMenu, onDismiss = { homeEventHandler(HomeUiEvent.HideMainMenu) }, @@ -240,7 +249,7 @@ fun Library( contentAlignment = Alignment.Center ) { Text( - text = stringResource(id = R.string.libraryHint), + text = stringResource(id = R.string.library_screen_hint), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center ) @@ -269,6 +278,7 @@ fun Library( val deleteDialogUiState = dialogUiState.deleteDialogUiState if (deleteDialogUiState != null) { + val snackbarMessage = stringResource(id = R.string.library_screen_snackbar_deleted) val foldersSelected = deleteDialogUiState.numberOfSelectedFolders > 0 val itemsSelected = deleteDialogUiState.numberOfSelectedItems > 0 @@ -277,23 +287,41 @@ fun Library( deleteDialogUiState.numberOfSelectedFolders + deleteDialogUiState.numberOfSelectedItems DeleteConfirmationBottomSheet( - explanation = UiText.DynamicString( - "Delete " + - (if (foldersSelected) "folders" else "") + - (if (foldersSelected && itemsSelected) " and " else "") + - (if (itemsSelected) "items" else "") + - "? They will remain in your statistics, but you will no longer be able to practice them." - ), + explanation = + if (foldersSelected && itemsSelected) { + UiText.StringResource( + R.string.library_screen_deletion_dialog_explanation_both, + deleteDialogUiState.numberOfSelectedFolders, + pluralStringResource(id = R.plurals.library_folder, deleteDialogUiState.numberOfSelectedFolders), + deleteDialogUiState.numberOfSelectedItems, + pluralStringResource(id = R.plurals.library_item, deleteDialogUiState.numberOfSelectedItems) + ) + } else { + UiText.PluralResource( + R.plurals.library_screen_deletion_dialog_explanation, + totalSelections, + totalSelections, + if (foldersSelected) { + pluralStringResource(id = R.plurals.library_folder, deleteDialogUiState.numberOfSelectedFolders) + } else { + pluralStringResource(id = R.plurals.library_item, deleteDialogUiState.numberOfSelectedItems) + } + ) + }, confirmationIcon = UiIcon.DynamicIcon(Icons.Default.Delete), - confirmationText = UiText.DynamicString("Delete forever ($totalSelections)"), + confirmationText = UiText.StringResource( + R.string.library_screen_deletion_dialog_confirm, + totalSelections + ), onDismiss = { eventHandler(LibraryUiEvent.DeleteDialogDismissed) }, onConfirm = { eventHandler(LibraryUiEvent.DeleteDialogConfirmed) mainEventHandler( MainUiEvent.ShowSnackbar( - message = "Deleted", - onUndo = { eventHandler(LibraryUiEvent.RestoreButtonPressed) } - )) + message = snackbarMessage, + onUndo = { eventHandler(LibraryUiEvent.RestoreButtonPressed) } + ) + ) } ) } @@ -352,7 +380,7 @@ fun LibraryContent( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Folders", + text = stringResource(id = R.string.library_content_folders_title), style = MaterialTheme.typography.titleLarge ) val sortMenuUiState = foldersUiState.sortMenuUiState @@ -361,7 +389,7 @@ fun LibraryContent( sortModes = LibraryFolderSortMode.entries, currentSortMode = sortMenuUiState.mode, currentSortDirection = sortMenuUiState.direction, - sortItemDescription = "folders", + sortItemDescription = stringResource(id = R.string.library_content_folders_sort_menu_description), onShowMenuChanged = { eventHandler(LibraryUiEvent.FolderSortMenuPressed) }, onSelectionHandler = { eventHandler(LibraryUiEvent.FolderSortModeSelected(it as LibraryFolderSortMode)) @@ -421,7 +449,7 @@ fun LibraryContent( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Items", + text = stringResource(id = R.string.library_content_items_title), style = MaterialTheme.typography.titleLarge ) val sortMenuUiState = itemsUiState.sortMenuUiState @@ -430,7 +458,7 @@ fun LibraryContent( sortModes = LibraryItemSortMode.entries, currentSortMode = sortMenuUiState.mode, currentSortDirection = sortMenuUiState.direction, - sortItemDescription = "items", + sortItemDescription = stringResource(id = R.string.library_content_items_sort_menu_description), onShowMenuChanged = { eventHandler(LibraryUiEvent.ItemSortMenuPressed) }, onSelectionHandler = { eventHandler(LibraryUiEvent.ItemSortModeSelected(it as LibraryItemSortMode)) @@ -492,7 +520,7 @@ fun LibraryFolder( ) Text( modifier = Modifier.padding(top = 4.dp), - text = "$numItems items", + text = stringResource(id = R.string.library_content_folders_sub_title, numItems), style = MaterialTheme.typography.labelMedium, color = colorScheme.onSurface.copy(alpha = 0.8f), textAlign = TextAlign.Center, @@ -547,7 +575,12 @@ fun LibraryUiItem( maxLines = 1, ) Text( - text = "last practiced: " + (lastPracticedDate?.musikusFormat(DateFormat.DAY_MONTH_YEAR) ?: "never"), + text = stringResource( + id = R.string.library_content_items_last_practiced, + lastPracticedDate?.musikusFormat(DateFormat.DAY_MONTH_YEAR) ?: stringResource( + id = R.string.library_content_items_last_practiced_never + ) + ), style = MaterialTheme.typography.bodySmall, color = colorScheme.onSurfaceVariant, ) diff --git a/app/src/main/java/app/musikus/library/presentation/LibraryUiEvent.kt b/app/src/main/java/app/musikus/library/presentation/LibraryUiEvent.kt index 70b3bdb6a..7d5af75b6 100644 --- a/app/src/main/java/app/musikus/library/presentation/LibraryUiEvent.kt +++ b/app/src/main/java/app/musikus/library/presentation/LibraryUiEvent.kt @@ -1,9 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2024 Matthias Emde + */ + package app.musikus.library.presentation +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode typealias LibraryUiEventHandler = (LibraryUiEvent) -> Unit diff --git a/app/src/main/java/app/musikus/library/presentation/LibraryUiState.kt b/app/src/main/java/app/musikus/library/presentation/LibraryUiState.kt index 89359abd5..a48aaf88a 100644 --- a/app/src/main/java/app/musikus/library/presentation/LibraryUiState.kt +++ b/app/src/main/java/app/musikus/library/presentation/LibraryUiState.kt @@ -14,11 +14,12 @@ import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortMode +import app.musikus.core.presentation.utils.UiText import java.time.ZonedDateTime import java.util.UUID data class LibraryTopBarUiState( - override val title: String, + override val title: UiText, override val showBackButton: Boolean, ) : TopBarUiState diff --git a/app/src/main/java/app/musikus/library/presentation/LibraryViewModel.kt b/app/src/main/java/app/musikus/library/presentation/LibraryViewModel.kt index d4a1547ba..d0edee734 100644 --- a/app/src/main/java/app/musikus/library/presentation/LibraryViewModel.kt +++ b/app/src/main/java/app/musikus/library/presentation/LibraryViewModel.kt @@ -10,6 +10,7 @@ package app.musikus.library.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.musikus.R import app.musikus.core.data.Nullable import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem @@ -19,10 +20,11 @@ import app.musikus.library.data.entities.LibraryItemCreationAttributes import app.musikus.library.data.entities.LibraryItemUpdateAttributes import app.musikus.library.domain.usecase.LibraryUseCases import app.musikus.settings.domain.usecase.UserPreferencesUseCases -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.core.presentation.utils.UiText +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -137,7 +139,7 @@ class LibraryViewModel @Inject constructor( * Composing the Ui state */ private val topBarUiState = _activeFolder.map { activeFolder -> - val title = activeFolder?.name ?: "Library" + val title = activeFolder?.name?.let { UiText.DynamicString(it) } ?: UiText.StringResource(R.string.library_title) val showBackButton = activeFolder != null LibraryTopBarUiState( @@ -148,7 +150,7 @@ class LibraryViewModel @Inject constructor( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = LibraryTopBarUiState( - title = "Library", + title = UiText.StringResource(R.string.library_title), showBackButton = false, ) ) diff --git a/app/src/main/java/app/musikus/metronome/presentation/MetronomeService.kt b/app/src/main/java/app/musikus/metronome/presentation/MetronomeService.kt index 5f91d2dfc..8501635be 100644 --- a/app/src/main/java/app/musikus/metronome/presentation/MetronomeService.kt +++ b/app/src/main/java/app/musikus/metronome/presentation/MetronomeService.kt @@ -143,7 +143,10 @@ class MetronomeService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { createPendingIntent() - val notification = getNotification("Metronome", "Metronome is running") + val notification = getNotification( + title = getString(R.string.metronome_service_notification_title), + description = getString(R.string.metronome_service_notification_description) + ) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startForeground(METRONOME_NOTIFICATION_ID, notification) diff --git a/app/src/main/java/app/musikus/metronome/presentation/MetronomeUi.kt b/app/src/main/java/app/musikus/metronome/presentation/MetronomeUi.kt index 08ea7f9f9..987e99e34 100644 --- a/app/src/main/java/app/musikus/metronome/presentation/MetronomeUi.kt +++ b/app/src/main/java/app/musikus/metronome/presentation/MetronomeUi.kt @@ -44,6 +44,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -53,12 +54,14 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.musikus.R import app.musikus.core.presentation.components.ExceptionHandler import app.musikus.settings.domain.ColorSchemeSelections import app.musikus.core.presentation.theme.MusikusColorSchemeProvider import app.musikus.core.presentation.theme.MusikusThemedPreview import app.musikus.core.presentation.theme.dimensions import app.musikus.core.presentation.theme.spacing +import app.musikus.core.presentation.utils.UiText @Composable @@ -149,7 +152,7 @@ private fun PreviewMetronome( MetronomeLayout( uiState = MetronomeUiState( settings = MetronomeSettings.DEFAULT.copy(bpm = 40), - tempoDescription = "Allegro", + tempoDescription = UiText.StringResource(resId = R.string.metronome_tempo_largo), isPlaying = false, sliderValue = 120f, ), @@ -221,7 +224,7 @@ private fun MetronomeHeader( Spacer(modifier = Modifier.width(MaterialTheme.spacing.extraSmall)) Text( modifier = Modifier.alignByBaseline(), - text = "bpm", + text = stringResource(id = R.string.metronome_header_bpm), textAlign = TextAlign.Left, style = MaterialTheme.typography.labelMedium, ) @@ -262,13 +265,13 @@ private fun MetronomeHeader( Icon( modifier = Modifier.fillMaxSize(), imageVector = Icons.Default.PlayArrow, - contentDescription = "Start metronome" + contentDescription = stringResource(id = R.string.metronome_header_start_description) ) } else { Icon( modifier = Modifier.fillMaxSize(), imageVector = Icons.Default.Stop, - contentDescription = "Stop metronome" + contentDescription = stringResource(id = R.string.metronome_header_stop_description) ) } } @@ -288,11 +291,10 @@ fun MetronomeSlider( /** Tempo Slider */ Text( modifier = Modifier, - text = uiState.tempoDescription, + text = uiState.tempoDescription.asString(), style = MaterialTheme.typography.bodyLarge, ) - Slider( value = uiState.sliderValue, valueRange = @@ -338,7 +340,7 @@ fun MetronomeExtraSettingsRow( horizontalArrangement = Arrangement.SpaceBetween ) { - MetronomeExtraSettingsColumn(label = "Beats/bar") { + MetronomeExtraSettingsColumn(label = stringResource(id = R.string.metronome_extra_settings_beats_per_bar)) { MetronomeIncrementer( value = uiState.beatsPerBar, onIncrement = onIncrementBeatsPerBar, @@ -346,7 +348,7 @@ fun MetronomeExtraSettingsRow( ) } - MetronomeExtraSettingsColumn(label = "Clicks/beat") { + MetronomeExtraSettingsColumn(label = stringResource(id = R.string.metronome_extra_settings_clicks_per_beat)) { MetronomeIncrementer( value = uiState.clicksPerBeat, onIncrement = onIncrementClicksPerBear, @@ -354,14 +356,14 @@ fun MetronomeExtraSettingsRow( ) } - MetronomeExtraSettingsColumn(label = "Tab tempo") { + MetronomeExtraSettingsColumn(label = stringResource(id = R.string.metronome_extra_settings_tap_tempo)) { IconButton( onClick = onTapTempo, // modifier = Modifier.size(25.dp), ) { Icon( imageVector = Icons.Default.TouchApp, - contentDescription = "Tab tempo" + contentDescription = stringResource(id = R.string.metronome_extra_settings_tap_tempo) ) } @@ -414,7 +416,7 @@ fun MetronomeIncrementer( ) { Icon( imageVector = Icons.Default.Remove, - contentDescription = "Decrement" + contentDescription = stringResource(id = R.string.metronome_incrementer_decrement) ) } Text( @@ -432,7 +434,7 @@ fun MetronomeIncrementer( ) { Icon( imageVector = Icons.Default.Add, - contentDescription = "Decrement" + contentDescription = stringResource(id = R.string.metronome_incrementer_increment) ) } } diff --git a/app/src/main/java/app/musikus/metronome/presentation/MetronomeViewModel.kt b/app/src/main/java/app/musikus/metronome/presentation/MetronomeViewModel.kt index 8486e70c0..fe363dc7a 100644 --- a/app/src/main/java/app/musikus/metronome/presentation/MetronomeViewModel.kt +++ b/app/src/main/java/app/musikus/metronome/presentation/MetronomeViewModel.kt @@ -17,6 +17,8 @@ import android.os.Build import android.os.IBinder import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import app.musikus.R +import app.musikus.core.presentation.utils.UiText import app.musikus.permissions.domain.usecase.PermissionsUseCases import app.musikus.settings.domain.usecase.UserPreferencesUseCases import app.musikus.permissions.domain.PermissionChecker @@ -59,7 +61,7 @@ data class MetronomeSettings( data class MetronomeUiState( val settings: MetronomeSettings, val sliderValue: Float, - val tempoDescription: String, + val tempoDescription: UiText, val isPlaying: Boolean, ) @@ -142,6 +144,21 @@ class MetronomeViewModel @Inject constructor( /** ------------------ Main ViewModel --------------------- */ + private fun tempoToUiText(tempo: Int) = UiText.StringResource(resId = when(tempo) { + in 20 until 40 -> R.string.metronome_tempo_grave + in 40 until 55 -> R.string.metronome_tempo_largo + in 55 until 66 -> R.string.metronome_tempo_lento + in 66 until 76 -> R.string.metronome_tempo_adagio + in 76 until 92 -> R.string.metronome_tempo_andante + in 92 until 108 -> R.string.metronome_tempo_andante_moderato + in 108 until 116 -> R.string.metronome_tempo_moderato + in 116 until 120 -> R.string.metronome_tempo_allegro_moderato + in 120 until 156 -> R.string.metronome_tempo_allegro + in 156 until 172 -> R.string.metronome_tempo_vivace + in 172 until 200 -> R.string.metronome_tempo_presto + else -> R.string.metronome_tempo_prestissimo + }) + /** Imported flows */ private val metronomeSettings = userPreferencesUseCases.getMetronomeSettings().stateIn( scope = viewModelScope, @@ -161,24 +178,9 @@ class MetronomeViewModel @Inject constructor( serviceState, _sliderValue ) { settings, serviceState, sliderValue -> - val tempoDescription = when(settings.bpm) { - in 20 until 40 -> "Grave" - in 40 until 55 -> "Largo" - in 55 until 66 -> "Lento" - in 66 until 76 -> "Adagio" - in 76 until 92 -> "Andante" - in 92 until 108 -> "Andante moderato" - in 108 until 116 -> "Moderato" - in 116 until 120 -> "Allegro moderato" - in 120 until 156 -> "Allegro" - in 156 until 172 -> "Vivace" - in 172 until 200 -> "Presto" - else -> "Prestissimo" - } - MetronomeUiState( settings = settings, - tempoDescription = tempoDescription, + tempoDescription = tempoToUiText(settings.bpm), isPlaying = serviceState?.isPlaying ?: false, sliderValue = if(sliderValue.toInt() != settings.bpm) { settings.bpm.toFloat() @@ -191,7 +193,7 @@ class MetronomeViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5000), initialValue = MetronomeUiState( settings = metronomeSettings.value, - tempoDescription = "Allegro", + tempoDescription = tempoToUiText(metronomeSettings.value.bpm), isPlaying = serviceState.value?.isPlaying ?: false, sliderValue = _sliderValue.value ) diff --git a/app/src/main/java/app/musikus/permissions/presentation/PermissionDialog.kt b/app/src/main/java/app/musikus/permissions/presentation/PermissionDialog.kt index da2e4fc22..a4f063192 100644 --- a/app/src/main/java/app/musikus/permissions/presentation/PermissionDialog.kt +++ b/app/src/main/java/app/musikus/permissions/presentation/PermissionDialog.kt @@ -15,8 +15,10 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.window.Dialog +import app.musikus.R import app.musikus.core.presentation.components.DialogActions import app.musikus.core.presentation.components.DialogHeader import app.musikus.core.presentation.theme.MusikusColorSchemeProvider @@ -41,14 +43,17 @@ fun PermissionDialog( color = MaterialTheme.colorScheme.surfaceContainerHigh ) { Column { - DialogHeader("Permission required") + DialogHeader(stringResource(id = R.string.permission_dialog_title)) Column(Modifier.padding(horizontal = MaterialTheme.spacing.medium)) { Text(text = description) DialogActions( onDismissHandler = onDismiss, onConfirmHandler = if (isPermanentlyDeclined) onGoToAppSettingsClick else onOkClick, - confirmButtonText = if (isPermanentlyDeclined) "Go to App Settings" else "Ok", - dismissButtonText = "Cancel", + confirmButtonText = stringResource(id = if (isPermanentlyDeclined) + R.string.permission_dialog_confirm_permanently_declined else + android.R.string.ok + ), + dismissButtonText = stringResource(id = android.R.string.cancel), ) } } diff --git a/app/src/main/java/app/musikus/recorder/presentation/Recorder.kt b/app/src/main/java/app/musikus/recorder/presentation/Recorder.kt index d04ba7633..2b6559b97 100644 --- a/app/src/main/java/app/musikus/recorder/presentation/Recorder.kt +++ b/app/src/main/java/app/musikus/recorder/presentation/Recorder.kt @@ -15,6 +15,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore +import app.musikus.R import app.musikus.core.domain.TimeProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -40,11 +41,15 @@ class Recorder( @Throws(IllegalRecorderStateException::class) fun start() { if (state.value == RecorderState.RECORDING) { - throw IllegalRecorderStateException("Tried to start recorder while it is already recording") + throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_start_while_recording) + ) } if (state.value == RecorderState.PAUSED) { - throw IllegalRecorderStateException("Tried to start recorder while it is paused") + throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_start_while_paused) + ) } val startTime = timeProvider.now() @@ -81,28 +86,38 @@ class Recorder( context.contentResolver.openFileDescriptor(uri, "w")?.use { initializeMediaRecorder(it.fileDescriptor) } - } ?: throw IllegalRecorderStateException("Couldn't create MediaStore entry") + } ?: throw RecorderException.MediaStoreInsertFailed(context) mediaRecorder?.start() - ?: throw IllegalRecorderStateException("Tried to start recording without initializing mediaRecorder") + ?: throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_start_while_uninitialized) + ) state.update { RecorderState.RECORDING } } @Throws(IllegalRecorderStateException::class) fun pause() { if(state.value != RecorderState.RECORDING) { - throw IllegalRecorderStateException("Recorder is not recording") + throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_pause_while_not_recording) + ) } - mediaRecorder?.pause() ?: throw IllegalRecorderStateException("Tried to pause recording without initializing mediaRecorder") + mediaRecorder?.pause() ?: throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_pause_while_uninitialized) + ) state.update { RecorderState.PAUSED } } @Throws(IllegalRecorderStateException::class) fun resume() { if(state.value != RecorderState.PAUSED) { - throw IllegalRecorderStateException("Recorder is not paused") + throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_resume_while_not_paused) + ) } - mediaRecorder?.resume() ?: throw IllegalRecorderStateException("Tried to resume recording without initializing mediaRecorder") + mediaRecorder?.resume() ?: throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_resume_while_uninitialized) + ) state.update { RecorderState.RECORDING } } @@ -110,12 +125,16 @@ class Recorder( @Throws(IllegalRecorderStateException::class) fun delete() { if(state.value != RecorderState.PAUSED) { - throw IllegalRecorderStateException("Can only save recording from paused state") + throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_delete_while_not_paused) + ) } mediaRecorder?.apply { stop() release() - } ?: throw IllegalRecorderStateException("Tried to stop recording without initializing mediaRecorder") + } ?: throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_delete_while_uninitialized) + ) recordingUri?.let { context.contentResolver.delete(it, null, null) } @@ -126,17 +145,21 @@ class Recorder( @Throws(IllegalRecorderStateException::class) fun save(recordingName: String) { if(state.value != RecorderState.PAUSED) { - throw IllegalRecorderStateException("Can only save recording from paused state") + throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_save_while_not_paused) + ) } if(recordingName.isBlank()) { - throw IllegalRecorderStateException("Recording name is blank") + throw RecorderException.SaveWithEmptyName(context) } mediaRecorder?.apply { stop() release() - } ?: throw IllegalRecorderStateException("Tried to stop recording without initializing mediaRecorder") + } ?: throw IllegalRecorderStateException( + context.getString(R.string.recorder_illegal_state_exception_save_while_uninitialized) + ) // finally update the recordings content values to mark it as no longer pending ContentValues().apply { @@ -146,7 +169,7 @@ class Recorder( put(MediaStore.MediaColumns.DISPLAY_NAME, recordingName) put(MediaStore.MediaColumns.TITLE, recordingName) context.contentResolver.update( - recordingUri ?: throw IllegalRecorderStateException("Recording URI is null"), + recordingUri ?: throw RecorderException.MediaStoreUpdateFailed(context), this, null, null @@ -170,7 +193,7 @@ class Recorder( return ContentValues().apply { put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp4") - put(MediaStore.Audio.Media.ALBUM, "Musikus") + put(MediaStore.Audio.Media.ALBUM, context.getString(R.string.recorder_content_values_album)) put(MediaStore.Audio.Media.DATE_ADDED, time.toString()) put(MediaStore.Audio.Media.DATE_MODIFIED, time.toString()) put(MediaStore.Audio.Media.IS_MUSIC, 1) @@ -181,7 +204,7 @@ class Recorder( } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - put(MediaStore.Audio.Media.GENRE, "Recording") + put(MediaStore.Audio.Media.GENRE, context.getString(R.string.recorder_content_values_genre)) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { diff --git a/app/src/main/java/app/musikus/recorder/presentation/RecorderService.kt b/app/src/main/java/app/musikus/recorder/presentation/RecorderService.kt index 73bb42e0f..12f96e680 100644 --- a/app/src/main/java/app/musikus/recorder/presentation/RecorderService.kt +++ b/app/src/main/java/app/musikus/recorder/presentation/RecorderService.kt @@ -281,14 +281,18 @@ class RecorderService : Service() { } private fun setFinalNotification() { - val notification: Notification = getBasicNotification("Recording finished", "click to open", false) + val notification: Notification = getBasicNotification( + title = getString(R.string.recorder_service_final_notification_title), + text = getString(R.string.recorder_service_final_notification_text), + persistent = false + ) val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager mNotificationManager.notify(RECORDER_NOTIFICATION_ID, notification) } private fun getNotification(duration: Duration) : Notification { return getBasicNotification( - title = getString(R.string.recording_notification_settings_description), + title = getString(R.string.recorder_service_notification_title), text = getDurationString(duration, DurationFormat.HMS_DIGITAL) ) } diff --git a/app/src/main/java/app/musikus/recorder/presentation/RecorderUi.kt b/app/src/main/java/app/musikus/recorder/presentation/RecorderUi.kt index 6527a8160..bdab7349d 100644 --- a/app/src/main/java/app/musikus/recorder/presentation/RecorderUi.kt +++ b/app/src/main/java/app/musikus/recorder/presentation/RecorderUi.kt @@ -64,6 +64,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -74,6 +75,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.session.MediaController +import app.musikus.R import app.musikus.core.presentation.components.DialogActions import app.musikus.core.presentation.components.ExceptionHandler import app.musikus.core.presentation.components.Waveform @@ -263,7 +265,7 @@ fun RecorderToolbar( AnimatedVisibility(showDeleteAndSave) { Row { TextButton(onClick = { eventHandler(RecorderUiEvent.DeleteRecording) }) { - Text(text = "Delete") + Text(text = stringResource(id = R.string.recorder_toolbar_delete)) } } } @@ -290,7 +292,7 @@ fun RecorderToolbar( Icon( modifier = Modifier.fillMaxSize(), imageVector = Icons.Default.MicOff, - contentDescription = "Microphone not available" + contentDescription = stringResource(id = R.string.recorder_toolbar_main_button_uninitialized_description) ) } @@ -298,7 +300,7 @@ fun RecorderToolbar( Icon( modifier = Modifier.fillMaxSize(), imageVector = Icons.Default.Mic, - contentDescription = "Start recording" + contentDescription = stringResource(id = R.string.recorder_toolbar_main_button_idle_description) ) } @@ -306,7 +308,7 @@ fun RecorderToolbar( Icon( modifier = Modifier.fillMaxSize(), imageVector = Icons.Default.Pause, - contentDescription = "Pause recording" + contentDescription = stringResource(id = R.string.recorder_toolbar_main_button_recording_description) ) } @@ -314,7 +316,7 @@ fun RecorderToolbar( Icon( modifier = Modifier.fillMaxSize(), imageVector = Icons.Default.PlayArrow, - contentDescription = "Resume recording" + contentDescription = stringResource(id = R.string.recorder_toolbar_main_button_paused_description) ) } } @@ -325,7 +327,7 @@ fun RecorderToolbar( AnimatedVisibility(showDeleteAndSave) { Row { TextButton(onClick = { eventHandler(RecorderUiEvent.SaveRecording) }) { - Text(text = "Save") + Text(text = stringResource(id = R.string.recorder_toolbar_save)) } } } @@ -350,7 +352,7 @@ private fun RecordingsList( Column(modifier = modifier) { Spacer(Modifier.height(MaterialTheme.spacing.medium)) Text( - "No Recordings", + text = stringResource(id = R.string.recorder_recordings_list_empty), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -646,7 +648,7 @@ private fun WaveformMediaPlayer( } IconButton(onClick = onClear) { - Icon(Icons.Default.Close, contentDescription = "Close player") + Icon(Icons.Default.Close, contentDescription = stringResource(id = R.string.recorder_media_player_close_description)) } } } @@ -669,7 +671,7 @@ private fun DialogSaveRecording( modifier = Modifier .padding(horizontal = MaterialTheme.spacing.large) .padding(vertical = MaterialTheme.spacing.medium), - text = "Save recording as:", + text = stringResource(id = R.string.recorder_save_recording_dialog), style = MaterialTheme.typography.titleLarge, ) OutlinedTextField( @@ -677,11 +679,11 @@ private fun DialogSaveRecording( .fillMaxWidth() .padding(horizontal = MaterialTheme.spacing.medium), value = uiState.recordingName, - label = { Text(text = "Recording name") }, + label = { Text(text = stringResource(id = R.string.recorder_save_recording_dialog_name_label)) }, onValueChange = { recordingNameChanged(it) }, ) DialogActions( - confirmButtonText = "Save", + confirmButtonText = stringResource(id = R.string.recorder_save_recording_dialog_confirm), onDismissHandler = onDismiss, onConfirmHandler = onConfirm, confirmButtonEnabled = uiState.recordingName.isNotEmpty() @@ -708,10 +710,10 @@ private fun DialogDeleteRecording( .padding(horizontal = MaterialTheme.spacing.large) .padding(top = MaterialTheme.spacing.medium), style = MaterialTheme.typography.titleLarge, - text = "Delete recording?", + text = stringResource(id = R.string.recorder_delete_recording_dialog), ) DialogActions( - confirmButtonText = "Delete", + confirmButtonText = stringResource(id = R.string.recorder_delete_recording_dialog_confirm), onDismissHandler = onDismiss, onConfirmHandler = onConfirm ) diff --git a/app/src/main/java/app/musikus/recorder/presentation/RecorderUiState.kt b/app/src/main/java/app/musikus/recorder/presentation/RecorderUiState.kt index 2e32a36ec..6a274f9e1 100644 --- a/app/src/main/java/app/musikus/recorder/presentation/RecorderUiState.kt +++ b/app/src/main/java/app/musikus/recorder/presentation/RecorderUiState.kt @@ -8,9 +8,11 @@ package app.musikus.recorder.presentation +import android.content.Context import android.net.Uri import androidx.compose.runtime.Stable import androidx.media3.common.MediaItem +import app.musikus.R import app.musikus.core.presentation.utils.DurationString @Stable @@ -64,12 +66,16 @@ sealed class RecorderUiEvent { typealias RecorderUiEventHandler = (event: RecorderUiEvent) -> Unit sealed class RecorderException(message: String) : Exception(message) { - data object NoMicrophonePermission : RecorderException("Microphone permission required") - data object NoStoragePermission : RecorderException("Storage permission required") - data object NoNotificationPermission: RecorderException("Notification permission required") + class NoMicrophonePermission(context: Context) : RecorderException(context.getString(R.string.recorder_exception_no_microphone_permission)) + class NoStoragePermission(context: Context) : RecorderException(context.getString(R.string.recorder_exception_no_storage_permission)) + class NoNotificationPermission(context: Context) : RecorderException(context.getString(R.string.recorder_exception_no_notification_permission)) - data object CouldNotLoadRecording : RecorderException("Could not load recording") + class CouldNotLoadRecording(context: Context) : RecorderException(context.getString(R.string.recorder_exception_no_could_not_load)) - data object ServiceNotFound : RecorderException("Cannot find recorder service") + class MediaStoreInsertFailed(context: Context) : RecorderException(context.getString(R.string.recorder_exception_media_store_failed_insert)) + class MediaStoreUpdateFailed(context: Context) : RecorderException(context.getString(R.string.recorder_exception_media_store_failed_update)) + class SaveWithEmptyName(context: Context) : RecorderException(context.getString(R.string.recorder_exception_save_with_empty_name)) + + class ServiceNotFound(context: Context) : RecorderException(context.getString(R.string.recorder_exception_no_service_not_found)) data class ServiceException(val exception: RecorderServiceException) : RecorderException(exception.message) } \ No newline at end of file diff --git a/app/src/main/java/app/musikus/recorder/presentation/RecorderViewModel.kt b/app/src/main/java/app/musikus/recorder/presentation/RecorderViewModel.kt index 8429ed216..86fd825ca 100644 --- a/app/src/main/java/app/musikus/recorder/presentation/RecorderViewModel.kt +++ b/app/src/main/java/app/musikus/recorder/presentation/RecorderViewModel.kt @@ -160,7 +160,7 @@ class RecorderViewModel @Inject constructor( emit(currentRawRecording?.let { recordingsUseCases.getRawRecording(it).getOrElse { - _exceptionChannel.send(RecorderException.CouldNotLoadRecording) + _exceptionChannel.send(RecorderException.CouldNotLoadRecording(application)) null } }) @@ -250,7 +250,7 @@ class RecorderViewModel @Inject constructor( is RecorderUiEvent.ResumeRecording -> { recorderServiceEventHandler?.invoke(RecorderServiceEvent.ResumeRecording) ?: viewModelScope.launch { - _exceptionChannel.send(RecorderException.ServiceNotFound) + _exceptionChannel.send(RecorderException.ServiceNotFound(application)) } } is RecorderUiEvent.DeleteRecording -> { @@ -264,7 +264,7 @@ class RecorderViewModel @Inject constructor( _showDeleteRecordingDialog.update { false } recorderServiceEventHandler?.invoke(RecorderServiceEvent.DeleteRecording) ?: viewModelScope.launch { - _exceptionChannel.send(RecorderException.ServiceNotFound) + _exceptionChannel.send(RecorderException.ServiceNotFound(application)) } } is RecorderUiEvent.SaveRecording -> { @@ -283,7 +283,7 @@ class RecorderViewModel @Inject constructor( recorderServiceEventHandler?.invoke( RecorderServiceEvent.SaveRecording(_recordingName.value) ) ?: viewModelScope.launch { - _exceptionChannel.send(RecorderException.ServiceNotFound) + _exceptionChannel.send(RecorderException.ServiceNotFound(application)) } } is RecorderUiEvent.RecordingNameChanged -> _recordingName.update { event.recordingName } @@ -314,13 +314,13 @@ class RecorderViewModel @Inject constructor( if (permissionsRequestResult is PermissionChecker.PermissionsDeniedException) { if (permissionsRequestResult.permissions.contains(Manifest.permission.RECORD_AUDIO)) { - _exceptionChannel.send(RecorderException.NoMicrophonePermission) + _exceptionChannel.send(RecorderException.NoMicrophonePermission(application)) } if (permissionsRequestResult.permissions.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - _exceptionChannel.send(RecorderException.NoStoragePermission) + _exceptionChannel.send(RecorderException.NoStoragePermission(application)) } if (permissionsRequestResult.permissions.contains(Manifest.permission.POST_NOTIFICATIONS)) { - _exceptionChannel.send(RecorderException.NoNotificationPermission) + _exceptionChannel.send(RecorderException.NoNotificationPermission(application)) } return false } @@ -334,13 +334,13 @@ class RecorderViewModel @Inject constructor( } startRecorderService() recorderServiceEventHandler?.invoke(RecorderServiceEvent.StartRecording) - ?: _exceptionChannel.send(RecorderException.ServiceNotFound) + ?: _exceptionChannel.send(RecorderException.ServiceNotFound(application)) } private fun pauseRecording() { recorderServiceEventHandler?.invoke(RecorderServiceEvent.PauseRecording) ?: viewModelScope.launch { - _exceptionChannel.send(RecorderException.ServiceNotFound) + _exceptionChannel.send(RecorderException.ServiceNotFound(application)) } } @@ -354,7 +354,7 @@ class RecorderViewModel @Inject constructor( if (storagePermissionResult.isSuccess) { _readPermissionsGranted.update { true } } else { - _exceptionChannel.send(RecorderException.NoStoragePermission) + _exceptionChannel.send(RecorderException.NoStoragePermission(application)) } } } diff --git a/app/src/main/java/app/musikus/sessions/presentation/EditSession.kt b/app/src/main/java/app/musikus/sessions/presentation/EditSession.kt index 192119238..62a8e1262 100644 --- a/app/src/main/java/app/musikus/sessions/presentation/EditSession.kt +++ b/app/src/main/java/app/musikus/sessions/presentation/EditSession.kt @@ -39,10 +39,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.musikus.R import app.musikus.core.presentation.utils.DurationFormat import app.musikus.core.presentation.utils.getDurationString import app.musikus.core.presentation.theme.libraryItemColors @@ -70,7 +72,9 @@ fun EditSession( title = { Text(text = "Edit Session") }, navigationIcon = { IconButton(onClick = navigateUp) { - Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource( + id = R.string.components_top_bar_back_description + )) } } ) diff --git a/app/src/main/java/app/musikus/sessions/presentation/SessionCard.kt b/app/src/main/java/app/musikus/sessions/presentation/SessionCard.kt index 16f43e722..d9a521fa4 100644 --- a/app/src/main/java/app/musikus/sessions/presentation/SessionCard.kt +++ b/app/src/main/java/app/musikus/sessions/presentation/SessionCard.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2022 Matthias Emde + * Copyright (c) 2022-2024 Matthias Emde */ package app.musikus.sessions.presentation @@ -143,14 +143,14 @@ fun SessionCard( modifier = Modifier .width(0.dp) .weight(4f), - text = stringResource(id = R.string.sessionSummaryPracticeTime), + text = stringResource(id = R.string.sessions_session_card_practice_time), style = MaterialTheme.typography.bodyLarge ) Text( modifier = Modifier .width(0.dp) .weight(2f), - text = stringResource(id = R.string.sessionSummaryBreakTime), + text = stringResource(id = R.string.sessions_session_card_break_time), style = MaterialTheme.typography.bodyLarge ) } @@ -197,7 +197,7 @@ fun SessionCard( HorizontalDivider() Column(modifier = Modifier.padding(16.dp)) { Text( - text = stringResource(id = R.string.sessionSummaryComment), + text = stringResource(id = R.string.sessions_session_card_comment), style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) ) Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/app/musikus/sessions/presentation/SessionsScreen.kt b/app/src/main/java/app/musikus/sessions/presentation/SessionsScreen.kt index 581bd61e8..7aeb238ee 100644 --- a/app/src/main/java/app/musikus/sessions/presentation/SessionsScreen.kt +++ b/app/src/main/java/app/musikus/sessions/presentation/SessionsScreen.kt @@ -109,17 +109,20 @@ fun SessionsScreen( if(mainUiState.isSessionRunning) { Icon( imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = "resume session" + contentDescription = stringResource(id = R.string.sessions_screen_fab_resume) ) } else { Icon( imageVector = Icons.Default.Add, - contentDescription = "new session" + contentDescription = stringResource(id = R.string.sessions_screen_fab) ) } }, text = { - Text(text = if(mainUiState.isSessionRunning) "Resume session" else "Start session") + Text(text = stringResource(id = if(mainUiState.isSessionRunning) + R.string.sessions_screen_fab_resume else + R.string.sessions_screen_fab + )) }, onClick = { navigateTo(Screen.ActiveSession) }, expanded = fabExpanded, @@ -131,11 +134,14 @@ fun SessionsScreen( topBar = { val topBarUiState = uiState.topBarUiState LargeTopAppBar( - title = { Text(text = topBarUiState.title) }, + title = { Text(text = topBarUiState.title.asString()) }, scrollBehavior = scrollBehavior, actions = { IconButton(onClick = { homeEventHandler(HomeUiEvent.ShowMainMenu) } ) { - Icon(Icons.Default.MoreVert, contentDescription = "more") + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(id = R.string.core_kebab_menu_description) + ) MainMenu ( show = homeUiState.showMainMenu, onDismiss = { homeEventHandler(HomeUiEvent.HideMainMenu) }, @@ -159,7 +165,7 @@ fun SessionsScreen( numSelectedItems = actionModeUiState.numberOfSelections, onDismissHandler = { eventHandler(SessionsUiEvent.ClearActionMode) }, onEditHandler = { - Toast.makeText(context, "Coming soon", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.core_coming_soon), Toast.LENGTH_SHORT).show() eventHandler(SessionsUiEvent.EditButtonPressed(onSessionEdit)) // TODO }, onDeleteHandler = { eventHandler(SessionsUiEvent.DeleteButtonPressed) } @@ -243,7 +249,7 @@ fun SessionsScreen( contentAlignment = Alignment.Center ) { Text( - text = stringResource(id = R.string.sessionsHint), + text = stringResource(id = R.string.sessions_screen_hint), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center ) @@ -259,16 +265,25 @@ fun SessionsScreen( val deleteDialogUiState = uiState.deleteDialogUiState if(deleteDialogUiState != null) { + val snackbarMessage = stringResource(id = R.string.sessions_screen_snackbar_deleted) + DeleteConfirmationBottomSheet( - explanation = UiText.DynamicString("Delete sessions? Any progress you made towards your goals during these sessions will be lost."), - confirmationText = UiText.DynamicString("Delete forever (${deleteDialogUiState.numberOfSelections})"), + explanation = UiText.PluralResource( + resId = R.plurals.sessions_screen_delete_session_dialog_explanation, + quantity = deleteDialogUiState.numberOfSelections, + deleteDialogUiState.numberOfSelections + ), + confirmationText = UiText.StringResource( + resId = R.string.sessions_screen_delete_session_dialog_confirm, + deleteDialogUiState.numberOfSelections + ), confirmationIcon = UiIcon.DynamicIcon(Icons.Default.Delete), onDismiss = { eventHandler(SessionsUiEvent.DeleteDialogDismissed) }, onConfirm = { eventHandler(SessionsUiEvent.DeleteDialogConfirmed) mainEventHandler( MainUiEvent.ShowSnackbar( - message = "Deleted ${deleteDialogUiState.numberOfSelections} sessions", + message = snackbarMessage, onUndo = { eventHandler(SessionsUiEvent.UndoButtonPressed) } )) } diff --git a/app/src/main/java/app/musikus/sessions/presentation/SessionsUiState.kt b/app/src/main/java/app/musikus/sessions/presentation/SessionsUiState.kt index ee25ab2db..d6fd22bae 100644 --- a/app/src/main/java/app/musikus/sessions/presentation/SessionsUiState.kt +++ b/app/src/main/java/app/musikus/sessions/presentation/SessionsUiState.kt @@ -9,10 +9,11 @@ package app.musikus.sessions.presentation import app.musikus.core.presentation.components.TopBarUiState +import app.musikus.core.presentation.utils.UiText import java.util.UUID data class SessionsTopBarUiState( - override val title: String, + override val title: UiText, override val showBackButton: Boolean, ) : TopBarUiState diff --git a/app/src/main/java/app/musikus/sessions/presentation/SessionsViewModel.kt b/app/src/main/java/app/musikus/sessions/presentation/SessionsViewModel.kt index 71886aa36..a63668283 100644 --- a/app/src/main/java/app/musikus/sessions/presentation/SessionsViewModel.kt +++ b/app/src/main/java/app/musikus/sessions/presentation/SessionsViewModel.kt @@ -10,10 +10,12 @@ package app.musikus.sessions.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.musikus.R import app.musikus.core.data.SessionWithSectionsWithLibraryItems import app.musikus.sessions.domain.usecase.SessionsUseCases import app.musikus.core.presentation.utils.DurationFormat import app.musikus.core.presentation.utils.DurationString +import app.musikus.core.presentation.utils.UiText import app.musikus.core.presentation.utils.getDurationString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -114,7 +116,7 @@ class SessionsViewModel @Inject constructor( /** Composing the Ui state */ private val topBarUiState = MutableStateFlow( SessionsTopBarUiState( - title = "Sessions", + title = UiText.StringResource(R.string.sessions_title), showBackButton = false, ) ) diff --git a/app/src/main/java/app/musikus/settings/data/UserPreferencesRepository.kt b/app/src/main/java/app/musikus/settings/data/UserPreferencesRepository.kt index 0a05799a8..37d9825ab 100644 --- a/app/src/main/java/app/musikus/settings/data/UserPreferencesRepository.kt +++ b/app/src/main/java/app/musikus/settings/data/UserPreferencesRepository.kt @@ -14,12 +14,12 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey -import app.musikus.core.domain.GoalSortInfo -import app.musikus.core.domain.GoalsSortMode -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.goals.data.GoalSortInfo +import app.musikus.goals.data.GoalsSortMode +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem import app.musikus.metronome.presentation.MetronomeSettings diff --git a/app/src/main/java/app/musikus/settings/domain/Types.kt b/app/src/main/java/app/musikus/settings/domain/Types.kt index 9aa505026..ec8194b1d 100644 --- a/app/src/main/java/app/musikus/settings/domain/Types.kt +++ b/app/src/main/java/app/musikus/settings/domain/Types.kt @@ -8,12 +8,16 @@ package app.musikus.settings.domain -import app.musikus.core.domain.GoalSortInfo -import app.musikus.core.domain.GoalsSortMode -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode +import app.musikus.R +import app.musikus.core.data.EnumWithDescription +import app.musikus.core.data.EnumWithLabel import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.core.presentation.utils.UiText +import app.musikus.goals.data.GoalSortInfo +import app.musikus.goals.data.GoalsSortMode +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem import app.musikus.metronome.presentation.MetronomeSettings @@ -21,23 +25,15 @@ import kotlinx.coroutines.flow.Flow const val USER_PREFERENCES_NAME = "user_preferences" -interface EnumWithLabel { - val label: String -} - -interface EnumWithDescription { - val description: String -} - enum class ThemeSelections : EnumWithLabel { SYSTEM { - override val label = "System default" + override val label = UiText.StringResource(R.string.settings_appearance_theme_options_system) }, DAY { - override val label = "Light" + override val label = UiText.StringResource(R.string.settings_appearance_theme_options_day) }, NIGHT { - override val label = "Dark" + override val label = UiText.StringResource(R.string.settings_appearance_theme_options_night) }; companion object { @@ -53,16 +49,16 @@ enum class ThemeSelections : EnumWithLabel { enum class ColorSchemeSelections : EnumWithLabel, EnumWithDescription { MUSIKUS { - override val label = "Musikus" - override val description = "A fresh new look" + override val label = UiText.StringResource(R.string.settings_appearance_color_scheme_options_musikus_title) + override val description = UiText.StringResource(R.string.settings_appearance_color_scheme_options_musikus_text) }, LEGACY { - override val label = "PracticeTime" - override val description = "Reminds you of an old friend" + override val label = UiText.StringResource(R.string.settings_appearance_color_scheme_options_legacy_title) + override val description = UiText.StringResource(R.string.settings_appearance_color_scheme_options_legacy_text) }, DYNAMIC { - override val label = "Dynamic" - override val description = "The color scheme follows your system theme. If it looks bad, it's on you" + override val label = UiText.StringResource(R.string.settings_appearance_color_scheme_options_dynamic_title) + override val description = UiText.StringResource(R.string.settings_appearance_color_scheme_options_dynamic_text) }; companion object { diff --git a/app/src/main/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCase.kt b/app/src/main/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCase.kt index ff2045a54..ee3fa9908 100644 --- a/app/src/main/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCase.kt +++ b/app/src/main/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCase.kt @@ -3,15 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.settings.domain.usecase import app.musikus.settings.domain.UserPreferencesRepository -import app.musikus.core.domain.LibraryFolderSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.library.data.LibraryFolderSortMode import kotlinx.coroutines.flow.first class SelectFolderSortModeUseCase( diff --git a/app/src/main/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCase.kt b/app/src/main/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCase.kt index 1c94587c1..73b34a0ff 100644 --- a/app/src/main/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCase.kt +++ b/app/src/main/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCase.kt @@ -3,15 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.settings.domain.usecase import app.musikus.settings.domain.UserPreferencesRepository -import app.musikus.core.domain.GoalsSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.goals.data.GoalsSortMode import kotlinx.coroutines.flow.first class SelectGoalsSortModeUseCase( diff --git a/app/src/main/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCase.kt b/app/src/main/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCase.kt index 3a323e8d4..402c0eba9 100644 --- a/app/src/main/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCase.kt +++ b/app/src/main/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCase.kt @@ -3,15 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.settings.domain.usecase import app.musikus.settings.domain.UserPreferencesRepository -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.library.data.LibraryItemSortMode import kotlinx.coroutines.flow.first class SelectItemSortModeUseCase( diff --git a/app/src/main/java/app/musikus/settings/presentation/SettingsScreen.kt b/app/src/main/java/app/musikus/settings/presentation/SettingsScreen.kt index ae7fe6749..af386b7df 100644 --- a/app/src/main/java/app/musikus/settings/presentation/SettingsScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/SettingsScreen.kt @@ -29,15 +29,17 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import app.musikus.R import app.musikus.core.presentation.Screen import app.musikus.core.presentation.components.TwoLiner import app.musikus.core.presentation.components.TwoLinerData import app.musikus.core.presentation.navigateTo import app.musikus.settings.presentation.about.AboutScreen -import app.musikus.settings.presentation.about.LicenseScreen +import app.musikus.settings.presentation.about.LicensesScreen import app.musikus.settings.presentation.appearance.AppearanceScreen import app.musikus.settings.presentation.backup.BackupScreen import app.musikus.settings.presentation.donate.DonateScreen @@ -74,7 +76,7 @@ fun NavGraphBuilder.addSettingsNavigationGraph(navController: NavController) { ) } composable(Screen.License.route) { - LicenseScreen(navigateUp = { navController.navigateUp() }) + LicensesScreen(navigateUp = { navController.navigateUp() }) } } @@ -124,12 +126,12 @@ fun SettingsScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Settings") }, + title = { Text(stringResource(R.string.settings_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } @@ -159,7 +161,7 @@ fun SettingsScreen( horizontalArrangement = Arrangement.Center ) { Text( - text = "Made with ❤️ in Munich", + text = stringResource(R.string.settings_footer), style = MaterialTheme.typography.bodyMedium, color = LocalContentColor.current.copy(alpha = 0.8f) ) diff --git a/app/src/main/java/app/musikus/settings/presentation/about/AboutScreen.kt b/app/src/main/java/app/musikus/settings/presentation/about/AboutScreen.kt index 5ee895867..083ddd229 100644 --- a/app/src/main/java/app/musikus/settings/presentation/about/AboutScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/about/AboutScreen.kt @@ -49,38 +49,37 @@ fun AboutScreen( Scaffold( topBar = { TopAppBar( - title = { Text("About") }, + title = { Text(stringResource(R.string.settings_about_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } ) } ) { paddingValues -> - val privacyPolicyUrl = stringResource(id = R.string.url_privacy) val context = LocalContext.current val aboutScreenItems = listOf( listOf( TwoLinerData( - firstLine = UiText.StringResource(R.string.development_title), - secondLine = UiText.StringResource(R.string.development_text) - ) + firstLine = UiText.StringResource(R.string.settings_about_developers_first_line), + secondLine = UiText.StringResource(R.string.settings_about_developers_second_line) + ) ), listOf( TwoLinerData( - firstLine = UiText.DynamicString("Publisher"), - secondLine = UiText.DynamicString("Matthias Emde\nConnollystraße 25\n80809 Munich, Germany\ncontact@musikus.app"), + firstLine = UiText.StringResource(R.string.settings_about_publisher_first_line), + secondLine = UiText.StringResource(R.string.settings_about_publisher_second_line), ), TwoLinerData( - firstLine = UiText.StringResource(R.string.privacy_policy_title), + firstLine = UiText.StringResource(R.string.settings_about_privacy_policy_first_line), onClick = { val openUrlIntent = Intent(Intent.ACTION_VIEW) - openUrlIntent.data = Uri.parse(privacyPolicyUrl) + openUrlIntent.data = Uri.parse(context.getString(R.string.settings_about_privacy_policy_url)) ContextCompat.startActivity(context, openUrlIntent, null) }, trailingIcon = UiIcon.DynamicIcon(Icons.AutoMirrored.Filled.OpenInNew) @@ -88,18 +87,15 @@ fun AboutScreen( ), listOf( TwoLinerData( - firstLine = UiText.DynamicString("Version"), + firstLine = UiText.StringResource(R.string.settings_about_version_first_line), secondLine = UiText.DynamicString("${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_HASH})") ), TwoLinerData( - firstLine = UiText.DynamicString("Licenses"), + firstLine = UiText.StringResource(R.string.settings_about_licenses_first_line), onClick = { navigateTo(Screen.License) } ), TwoLinerData( - secondLine = UiText.DynamicString( - "Copyright Matthias Emde, Michael Prommersberger\n" + - "Licensed under the Mozilla Public License Version 2.0" - ) + secondLine = UiText.StringResource(R.string.settings_about_copyright_second_line) ) ), ) diff --git a/app/src/main/java/app/musikus/settings/presentation/about/LicenseScreen.kt b/app/src/main/java/app/musikus/settings/presentation/about/LicensesScreen.kt similarity index 90% rename from app/src/main/java/app/musikus/settings/presentation/about/LicenseScreen.kt rename to app/src/main/java/app/musikus/settings/presentation/about/LicensesScreen.kt index 249250d21..1f1544202 100644 --- a/app/src/main/java/app/musikus/settings/presentation/about/LicenseScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/about/LicensesScreen.kt @@ -26,13 +26,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import app.musikus.R import app.musikus.core.presentation.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LicenseScreen( +fun LicensesScreen( navigateUp: () -> Unit ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() @@ -42,12 +43,12 @@ fun LicenseScreen( topBar = { TopAppBar( scrollBehavior = scrollBehavior, - title = { Text("Licenses") }, + title = { Text(stringResource(R.string.settings_licenses_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } diff --git a/app/src/main/java/app/musikus/settings/presentation/appearance/AppearanceScreen.kt b/app/src/main/java/app/musikus/settings/presentation/appearance/AppearanceScreen.kt index e72d33e4c..e431ed943 100644 --- a/app/src/main/java/app/musikus/settings/presentation/appearance/AppearanceScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/appearance/AppearanceScreen.kt @@ -33,15 +33,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.musikus.R import app.musikus.core.presentation.components.TwoLiner import app.musikus.core.presentation.components.TwoLinerData -import app.musikus.settings.domain.ColorSchemeSelections -import app.musikus.settings.domain.ThemeSelections import app.musikus.core.presentation.theme.spacing import app.musikus.core.presentation.utils.UiText +import app.musikus.core.presentation.utils.htmlResource +import app.musikus.settings.domain.ColorSchemeSelections +import app.musikus.settings.domain.ThemeSelections @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,12 +58,12 @@ fun AppearanceScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Appearance") }, + title = { Text(stringResource(R.string.settings_appearance_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } @@ -70,18 +73,18 @@ fun AppearanceScreen( val appearanceMenuItems = listOf( TwoLinerData( - firstLine = UiText.DynamicString("Language"), - secondLine = UiText.DynamicString("System default"), + firstLine = UiText.StringResource(R.string.settings_appearance_language_first_line), + secondLine = UiText.DynamicString(uiState.languageUiState.currentLanguage), onClick = { eventHandler(AppearanceUiEvent.ShowLanguageDialog) } ), TwoLinerData( - firstLine = UiText.DynamicString("Theme"), - secondLine = UiText.DynamicString(uiState.themeUiState.currentTheme.label), + firstLine = UiText.StringResource(R.string.settings_appearance_theme_first_line), + secondLine = uiState.themeUiState.currentTheme.label, onClick = { eventHandler(AppearanceUiEvent.ShowThemeDialog) } ), TwoLinerData( - firstLine = UiText.DynamicString("Color scheme"), - secondLine = UiText.DynamicString(uiState.colorSchemeUiState.currentColorScheme.label), + firstLine = UiText.StringResource(R.string.settings_appearance_color_scheme_first_line), + secondLine = uiState.colorSchemeUiState.currentColorScheme.label, onClick = { eventHandler(AppearanceUiEvent.ShowColorSchemeDialog) } ), ) @@ -107,17 +110,14 @@ fun AppearanceScreen( .padding(horizontal = (MaterialTheme.spacing.medium + MaterialTheme.spacing.small)), ) { Text( - text = "Language", + text = stringResource(R.string.settings_appearance_language_dialog_title), style = MaterialTheme.typography.titleLarge ) } Spacer(modifier = Modifier.height(MaterialTheme.spacing.small)) Text( modifier = Modifier.padding(horizontal = MaterialTheme.spacing.medium), - text = - "More languages are coming soon! " + - "If you want to help in translating the app into your native language, " + - "please contact us at contribute@musikus.app", + text = htmlResource(R.string.settings_appearance_language_dialog_text), style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.small)) @@ -134,7 +134,7 @@ fun AppearanceScreen( Button( onClick = { eventHandler(AppearanceUiEvent.HideLanguageDialog) } ) { - Text (text = "Awesome!") + Text (text = stringResource(R.string.settings_appearance_language_dialog_confirm)) } } } @@ -155,7 +155,7 @@ fun AppearanceScreen( .padding(horizontal = (MaterialTheme.spacing.medium + MaterialTheme.spacing.small)), ) { Text( - text = "Theme", + text = stringResource(R.string.settings_appearance_theme_dialog_title), style = MaterialTheme.typography.headlineSmall ) } @@ -174,7 +174,7 @@ fun AppearanceScreen( onClick = { eventHandler(AppearanceUiEvent.ChangeTheme(selection)) } ) Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) - Text(text = selection.label) + Text(text = selection.label.asString()) } } Spacer(modifier = Modifier.height(MaterialTheme.spacing.small)) @@ -196,7 +196,7 @@ fun AppearanceScreen( .padding(horizontal = (MaterialTheme.spacing.medium + MaterialTheme.spacing.small)), ) { Text( - text = "ColorScheme", + text = stringResource(R.string.settings_appearance_color_scheme_dialog_title), style = MaterialTheme.typography.headlineSmall ) } @@ -217,8 +217,8 @@ fun AppearanceScreen( Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) TwoLiner( data = TwoLinerData( - firstLine = UiText.DynamicString(selection.label), - secondLine = UiText.DynamicString(selection.description) + firstLine = UiText.DynamicString(selection.label.asString()), + secondLine = UiText.DynamicString(selection.description.asString()) ) ) } diff --git a/app/src/main/java/app/musikus/settings/presentation/backup/BackupScreen.kt b/app/src/main/java/app/musikus/settings/presentation/backup/BackupScreen.kt index af69301ad..b54ca3c17 100644 --- a/app/src/main/java/app/musikus/settings/presentation/backup/BackupScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/backup/BackupScreen.kt @@ -31,7 +31,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import app.musikus.R import app.musikus.core.presentation.MainActivity import app.musikus.core.presentation.theme.spacing @@ -43,12 +45,12 @@ fun BackupScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Backup and Restore") }, + title = { Text(stringResource(R.string.settings_backup_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } @@ -61,9 +63,7 @@ fun BackupScreen( .padding(horizontal = MaterialTheme.spacing.large), ) { Text( - text = - "In this menu you can backup and restore your data to a file " + - "for backing up or transferring it to another device.", + text = stringResource(R.string.settings_backup_text), ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) @@ -82,7 +82,7 @@ fun BackupScreen( Icon(Icons.Default.CloudUpload, contentDescription = null) Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) Text( - text = "Create backup", + text = stringResource(R.string.settings_backup_create_backup), style = MaterialTheme.typography.titleMedium ) } @@ -102,7 +102,7 @@ fun BackupScreen( Icon(Icons.Default.CloudDownload, contentDescription = null) Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) Text( - text = "Load backup", + text = stringResource(R.string.settings_backup_restore_backup), style = MaterialTheme.typography.titleMedium ) } @@ -111,9 +111,10 @@ fun BackupScreen( Spacer(modifier = Modifier.height(MaterialTheme.spacing.extraSmall)) Text( - text = "*Note: Loading a backup will overwrite your current data.", + text = stringResource(R.string.settings_backup_footnote), style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold ) + fontWeight = FontWeight.Bold + ) } } } \ No newline at end of file diff --git a/app/src/main/java/app/musikus/settings/presentation/donate/DonateScreen.kt b/app/src/main/java/app/musikus/settings/presentation/donate/DonateScreen.kt index 96c5090e0..b88110609 100644 --- a/app/src/main/java/app/musikus/settings/presentation/donate/DonateScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/donate/DonateScreen.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.unit.em import androidx.core.content.ContextCompat.startActivity import app.musikus.R import app.musikus.core.presentation.theme.spacing -import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -43,18 +42,17 @@ import java.util.Locale fun DonateScreen( navigateUp: () -> Unit ) { - val donateUrl = stringResource(id = R.string.url_donate) val context = LocalContext.current Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(id = R.string.donations_title)) }, + title = { Text(stringResource(R.string.settings_donate_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } @@ -67,11 +65,11 @@ fun DonateScreen( .padding(horizontal = MaterialTheme.spacing.extraLarge) ) { Text( - text = stringResource(id = R.string.donations_text), + text = stringResource(R.string.settings_donate_text), lineHeight = 1.6.em, ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.small)) - for (bulletPoint in stringArrayResource(id = R.array.array_donations_text_bulletlist)) { + for (bulletPoint in stringArrayResource(R.array.settings_donate_bulletlist)) { Text(text = "\u2022\t" + bulletPoint) Spacer(modifier = Modifier.height(MaterialTheme.spacing.extraSmall)) } @@ -82,12 +80,12 @@ fun DonateScreen( modifier = Modifier.fillMaxWidth(), onClick = { val openUrlIntent = Intent(Intent.ACTION_VIEW) - openUrlIntent.data = Uri.parse(donateUrl) + openUrlIntent.data = Uri.parse(context.getString(R.string.settings_donate_url)) startActivity(context, openUrlIntent, null) } ) { Text( - text = stringResource(id = R.string.donations_button_text).uppercase(Locale.ROOT), + text = stringResource(R.string.settings_donate_button_text), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/app/musikus/settings/presentation/export/ExportScreen.kt b/app/src/main/java/app/musikus/settings/presentation/export/ExportScreen.kt index 483f01e4b..c01b887b3 100644 --- a/app/src/main/java/app/musikus/settings/presentation/export/ExportScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/export/ExportScreen.kt @@ -28,11 +28,10 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale import app.musikus.R import app.musikus.core.presentation.theme.spacing +import app.musikus.core.presentation.utils.htmlResource @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -43,12 +42,12 @@ fun ExportScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Export session data") }, + title = { Text(stringResource(R.string.settings_export_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } @@ -62,7 +61,7 @@ fun ExportScreen( ) { Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) Text( - text = stringResource(id = R.string.export_csv_dialog_text) + text = stringResource(id = R.string.settings_export_text) ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) Button( @@ -73,10 +72,10 @@ fun ExportScreen( ) { Icon(imageVector = Icons.Default.Download, contentDescription = null) Spacer(modifier = Modifier.width(MaterialTheme.spacing.medium)) - Text("Export sessions".capitalize(Locale.current)) + Text(stringResource(R.string.settings_export_button_text)) } Text( - text = stringResource(id = R.string.export_csv_dialog_note), + text = htmlResource(R.string.settings_export_footnote), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/app/musikus/settings/presentation/help/HelpScreen.kt b/app/src/main/java/app/musikus/settings/presentation/help/HelpScreen.kt index 1934f75b0..3f8351be0 100644 --- a/app/src/main/java/app/musikus/settings/presentation/help/HelpScreen.kt +++ b/app/src/main/java/app/musikus/settings/presentation/help/HelpScreen.kt @@ -30,9 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale import app.musikus.R import app.musikus.core.presentation.theme.spacing @@ -47,12 +45,12 @@ fun HelpScreen( Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(id = R.string.help_title)) }, + title = { Text(stringResource(R.string.settings_help_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.components_top_bar_back_description), ) } } @@ -67,13 +65,13 @@ fun HelpScreen( ) { Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) Text( - text = stringResource(id = R.string.help_tips_title), + text = stringResource(R.string.settings_help_tips_title), fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) - Text(stringResource(id = R.string.help_tips_text)) + Text(stringResource(R.string.settings_help_tips_text)) Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) - for (bulletPoint in stringArrayResource(id = R.array.array_help_tips_text_bulletlist)) { + for (bulletPoint in stringArrayResource(R.array.settings_help_tips_bulletlist)) { Text(text = "\u2022\t" + bulletPoint) Spacer(modifier = Modifier.height(MaterialTheme.spacing.small)) } @@ -83,7 +81,7 @@ fun HelpScreen( modifier = Modifier.padding(horizontal = MaterialTheme.spacing.large) ) { Text( - text = stringResource(id = R.string.help_tutorial_title), + text = stringResource(R.string.settings_help_tutorial_title), fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) @@ -93,10 +91,10 @@ fun HelpScreen( .padding(horizontal = MaterialTheme.spacing.extraLarge), onClick = { /* TODO restart intro */ - Toast.makeText(context, "Coming soon", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.core_coming_soon), Toast.LENGTH_SHORT).show() } ) { - Text(stringResource(id = R.string.help_replay_intro).capitalize(Locale.current)) + Text(stringResource(R.string.settings_help_tutorial_replay_intro)) } } } diff --git a/app/src/main/java/app/musikus/statistics/presentation/StatisticsScreen.kt b/app/src/main/java/app/musikus/statistics/presentation/StatisticsScreen.kt index 2b9195a1c..f6b441982 100644 --- a/app/src/main/java/app/musikus/statistics/presentation/StatisticsScreen.kt +++ b/app/src/main/java/app/musikus/statistics/presentation/StatisticsScreen.kt @@ -69,6 +69,9 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import app.musikus.R +import app.musikus.core.domain.DateFormat +import app.musikus.core.domain.TimeProvider +import app.musikus.core.domain.musikusFormat import app.musikus.core.presentation.HomeUiEvent import app.musikus.core.presentation.HomeUiEventHandler import app.musikus.core.presentation.HomeUiState @@ -77,13 +80,11 @@ import app.musikus.core.presentation.components.CommonMenuSelections import app.musikus.core.presentation.components.MainMenu import app.musikus.core.presentation.theme.libraryItemColors import app.musikus.core.presentation.theme.spacing -import app.musikus.statistics.presentation.goalstatistics.GoalStatistics -import app.musikus.statistics.presentation.sessionstatistics.SessionStatistics -import app.musikus.core.domain.DateFormat import app.musikus.core.presentation.utils.DurationFormat -import app.musikus.core.domain.TimeProvider +import app.musikus.core.presentation.utils.UiText import app.musikus.core.presentation.utils.getDurationString -import app.musikus.core.domain.musikusFormat +import app.musikus.statistics.presentation.goalstatistics.GoalStatistics +import app.musikus.statistics.presentation.sessionstatistics.SessionStatistics fun NavGraphBuilder.addStatisticsNavigationGraph( navController: NavController, @@ -113,13 +114,16 @@ fun Statistics( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { LargeTopAppBar( - title = { Text(text = "Statistics") }, + title = { Text(text = stringResource(R.string.statistics_title)) }, scrollBehavior = scrollBehavior, actions = { IconButton(onClick = { homeEventHandler(HomeUiEvent.ShowMainMenu) }) { - Icon(Icons.Default.MoreVert, contentDescription = "more") + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.core_kebab_menu_description) + ) MainMenu ( show = homeUiState.showMainMenu, onDismiss = { homeEventHandler(HomeUiEvent.HideMainMenu) }, @@ -158,12 +162,12 @@ fun Statistics( } contentUiState.practiceDurationCardUiState?.let { item { - StatisticsPracticeDurationCard(it) { navigateTo(Screen.SessionStatistics) } + StatisticsSessionsCard(it) { navigateTo(Screen.SessionStatistics) } } } contentUiState.goalCardUiState?.let { item { - StatisticsGoalCard(it) { navigateTo(Screen.GoalStatistics) } + StatisticsGoalsCard(it) { navigateTo(Screen.GoalStatistics) } } } contentUiState.ratingsCardUiState?.let { @@ -182,7 +186,7 @@ fun Statistics( contentAlignment = Alignment.Center ) { Text( - text = stringResource(id = R.string.statisticsHint), + text = stringResource(R.string.statistics_hint), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center ) @@ -203,25 +207,42 @@ fun StatisticsCurrentMonth( ) val currentMonthStats = listOf( - "Total duration" to getDurationString( - uiState.totalPracticeDuration, - DurationFormat.HUMAN_PRETTY_SHORT + Pair( + UiText.StringResource(R.string.statistics_screen_current_month_total), + getDurationString( + uiState.totalPracticeDuration, + DurationFormat.HUMAN_PRETTY_SHORT + ) ), - "Per session" to AnnotatedString(stringResource(R.string.average_sign) + " ") + getDurationString( - uiState.averageDurationPerSession, - DurationFormat.HUMAN_PRETTY_SHORT + Pair( + UiText.StringResource(R.string.statistics_screen_current_month_per_session), + AnnotatedString(stringResource(R.string.core_average_sign) + " ") + getDurationString( + uiState.averageDurationPerSession, + DurationFormat.HUMAN_PRETTY_SHORT + ) ), - "Break per hour" to getDurationString( - uiState.breakDurationPerHour, - DurationFormat.HUMAN_PRETTY_SHORT + Pair( + UiText.StringResource(R.string.statistics_screen_current_month_per_session), + getDurationString( + uiState.breakDurationPerHour, + DurationFormat.HUMAN_PRETTY_SHORT + ) ), - "Average rating" to AnnotatedString(stringResource(R.string.average_sign) + " %.1f".format( - uiState.averageRatingPerSession - )), + Pair( + UiText.StringResource(R.string.statistics_screen_current_month_per_session), + AnnotatedString(stringResource(R.string.core_average_sign) + " %.1f".format( + uiState.averageRatingPerSession + )) + ) ) Column { - Text(text = "In " + timeProvider.now().musikusFormat(DateFormat.MONTH)) + Text( + text = stringResource( + id = R.string.statistics_screen_current_month_title, + timeProvider.now().musikusFormat(DateFormat.MONTH) + ) + ) Spacer(modifier = Modifier.height(MaterialTheme.spacing.small)) Row( modifier = Modifier.fillMaxWidth(), @@ -229,12 +250,11 @@ fun StatisticsCurrentMonth( ) { currentMonthStats.forEach {(label, stat) -> Column( - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), horizontalAlignment = CenterHorizontally, ) { Text(text = stat, style = statsTextStyle) - Text(text = label, style = labelTextStyle) + Text(text = label.asString(), style = labelTextStyle) } } } @@ -242,13 +262,12 @@ fun StatisticsCurrentMonth( } @Composable -fun StatisticsPracticeDurationCard( +fun StatisticsSessionsCard( uiState: StatisticsPracticeDurationCardUiState, navigateToSessionStatistics: () -> Unit, ) { ElevatedCard( - modifier = Modifier - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier .fillMaxWidth() @@ -261,16 +280,16 @@ fun StatisticsPracticeDurationCard( verticalAlignment = CenterVertically, ) { Text( - text = "Practice Time", + text = stringResource(R.string.statistics_screen_sessions_card_title), style = MaterialTheme.typography.titleLarge, ) Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "more", + contentDescription = stringResource(R.string.statistics_screen_sessions_card_description), ) } Text( - text = "Last 7 days", + text = stringResource(R.string.statistics_screen_sessions_card_subtitle), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.7f @@ -294,7 +313,7 @@ fun StatisticsPracticeDurationCard( ), ) Text( - text = "Total", + text = stringResource(R.string.statistics_screen_sessions_card_total_duration), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.7f @@ -371,7 +390,7 @@ fun StatisticsPracticeDurationCard( } @Composable -fun StatisticsGoalCard( +fun StatisticsGoalsCard( uiState: StatisticsGoalCardUiState, navigateToGoalStatistics: () -> Unit, ) { @@ -390,17 +409,17 @@ fun StatisticsGoalCard( verticalAlignment = CenterVertically, ) { Text( - text = "Your Goals", + text = stringResource(R.string.statistics_screen_goals_card_title), style = MaterialTheme.typography.titleLarge, ) Icon( // modifier = Modifier.fillMaxHeight(), imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "more", + contentDescription = stringResource(R.string.statistics_screen_sessions_card_description), ) } Text( - text = "Last 5 expired goals", + text = stringResource(R.string.statistics_screen_sessions_card_subtitle), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.7f @@ -415,16 +434,13 @@ fun StatisticsGoalCard( uiState.successRate?.let { (successful, total) -> Column { Text( - text = "" + - "$successful" + - "/" + - "$total", + text = "$successful/$total", style = MaterialTheme.typography.titleMedium.copy( color = MaterialTheme.colorScheme.primary, ), ) Text( - text = "Achieved", + text = stringResource(R.string.statistics_screen_goals_card_achieved_goals), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.7f @@ -496,11 +512,11 @@ fun StatisticsRatingsCard( .padding(MaterialTheme.spacing.medium) ) { Text( - text = "Ratings", + text = stringResource(R.string.statistics_screen_ratings_card_title), style = MaterialTheme.typography.titleLarge, ) Text( - text = "Your session ratings", + text = stringResource(R.string.statistics_screen_ratings_card_subtitle), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.7f @@ -512,7 +528,7 @@ fun StatisticsRatingsCard( val labelTextStyle = MaterialTheme.typography.labelSmall.copy( color = MaterialTheme.colorScheme.onSurface, ) - val starCharacter = stringResource(R.string.star_sign) + val starCharacter = stringResource(R.string.core_star_sign) val textMeasurer = rememberTextMeasurer() val numOfRatingsToAngleFactor = uiState.numOfRatingsFromOneToFive .sum() diff --git a/app/src/main/java/app/musikus/statistics/presentation/goalstatistics/GoalStatisticsScreen.kt b/app/src/main/java/app/musikus/statistics/presentation/goalstatistics/GoalStatisticsScreen.kt index 91e9631b5..c70198e3f 100644 --- a/app/src/main/java/app/musikus/statistics/presentation/goalstatistics/GoalStatisticsScreen.kt +++ b/app/src/main/java/app/musikus/statistics/presentation/goalstatistics/GoalStatisticsScreen.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.statistics.presentation.goalstatistics @@ -55,8 +55,8 @@ import app.musikus.R import app.musikus.core.presentation.components.conditional import app.musikus.core.presentation.components.simpleVerticalScrollbar import app.musikus.core.presentation.theme.spacing -import app.musikus.statistics.presentation.sessionstatistics.TimeframeSelectionHeader import app.musikus.core.presentation.utils.asAnnotatedString +import app.musikus.statistics.presentation.sessionstatistics.TimeframeSelectionHeader import java.util.UUID @@ -71,10 +71,13 @@ fun GoalStatistics( Scaffold( topBar = { TopAppBar( - title = { Text(text = stringResource(id = R.string.goal_statistics)) }, + title = { Text(text = stringResource(id = R.string.statistics_goal_statistics_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { - Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.components_top_bar_back_description) + ) } }, ) @@ -86,7 +89,11 @@ fun GoalStatistics( contentUiState.headerUiState?.let { TimeframeSelectionHeader( timeframe = it.timeframe, subtitle = it.successRate?.let { (succeeded, total) -> - "$succeeded out of $total" + stringResource( + id = R.string.statistics_goal_statistics_achieved_goals, + succeeded, + total + ) } ?: "", seekBackwardEnabled = it.seekBackwardEnabled, seekForwardEnabled = it.seekForwardEnabled, @@ -212,7 +219,7 @@ fun GoalStatisticsGoalSelector( targetValue = goalInfo.successRate?.let { (successful, total) -> (successful.toFloat() / total).coerceAtMost(1f) } ?: 0f, - label = "", + label = "animate-success-rate", animationSpec = tween(1500) ) LinearProgressIndicator( diff --git a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsBarChart.kt b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsBarChart.kt index 26cdff336..a9d1106e3 100644 --- a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsBarChart.kt +++ b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsBarChart.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.statistics.presentation.sessionstatistics @@ -38,7 +38,7 @@ import app.musikus.library.data.daos.LibraryItem import app.musikus.core.presentation.utils.DurationFormat import app.musikus.core.presentation.utils.getDurationString import app.musikus.core.presentation.theme.libraryItemColors -import app.musikus.core.domain.sorted +import app.musikus.library.data.sorted import kotlinx.coroutines.launch import kotlin.time.Duration import kotlin.time.Duration.Companion.hours diff --git a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsPieChart.kt b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsPieChart.kt index bca006151..70a02893d 100644 --- a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsPieChart.kt +++ b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsPieChart.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.statistics.presentation.sessionstatistics @@ -30,7 +30,7 @@ import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.dp import app.musikus.library.data.daos.LibraryItem import app.musikus.core.presentation.theme.libraryItemColors -import app.musikus.core.domain.sorted +import app.musikus.library.data.sorted import kotlinx.coroutines.launch import kotlin.math.cos import kotlin.math.sin diff --git a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsScreen.kt b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsScreen.kt index 67f0ef7c4..7aa24402d 100644 --- a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsScreen.kt +++ b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsScreen.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.statistics.presentation.sessionstatistics @@ -30,8 +30,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.material.icons.filled.PieChart import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -55,15 +53,15 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.musikus.R +import app.musikus.core.domain.Timeframe +import app.musikus.core.domain.musikusFormat import app.musikus.core.presentation.components.conditional import app.musikus.core.presentation.components.simpleVerticalScrollbar import app.musikus.core.presentation.theme.libraryItemColors import app.musikus.core.presentation.theme.spacing -import app.musikus.library.data.daos.LibraryItem import app.musikus.core.presentation.utils.DurationFormat -import app.musikus.core.domain.Timeframe import app.musikus.core.presentation.utils.getDurationString -import app.musikus.core.domain.musikusFormat +import app.musikus.library.data.daos.LibraryItem @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -78,10 +76,13 @@ fun SessionStatistics( topBar = { val topBarUiState = uiState.topBarUiState TopAppBar( - title = { Text(text = stringResource(id = R.string.session_statistics)) }, + title = { Text(text = stringResource(id = R.string.statistics_session_statistics_title)) }, navigationIcon = { IconButton(onClick = navigateUp) { - Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(id = R.string.components_top_bar_back_description) + ) } }, actions = { @@ -93,11 +94,8 @@ fun SessionStatistics( label = "statistics-chart-icon-animation" ) { chartType -> Icon( - imageVector = when (chartType) { - SessionStatisticsChartType.PIE -> Icons.Default.BarChart - SessionStatisticsChartType.BAR -> Icons.Default.PieChart - }, - contentDescription = null + imageVector = chartType.invert().icon.asIcon(), + contentDescription = chartType.invert().label.asString() ) } } @@ -115,15 +113,7 @@ fun SessionStatistics( Tab( selected = tab == contentUiState.selectedTab, onClick = { viewModel.onTabSelected(tab) }, - text = { - Text( - text = when (tab) { - SessionStatisticsTab.DAYS -> stringResource(id = R.string.days) - SessionStatisticsTab.WEEKS -> stringResource(id = R.string.weeks) - SessionStatisticsTab.MONTHS -> stringResource(id = R.string.months) - }.replaceFirstChar { it.uppercase() } - ) - } + text = { Text(text = tab.label.asString()) } ) } } @@ -160,7 +150,10 @@ fun SessionStatisticsHeader( seekBackwards: () -> Unit = {} ) = TimeframeSelectionHeader( timeframe = uiState.timeframe, - subtitle = "Total " + getDurationString(uiState.totalPracticeDuration, DurationFormat.HUMAN_PRETTY), + subtitle = stringResource( + id = R.string.statistics_session_statistics_total_duration, + getDurationString(uiState.totalPracticeDuration, DurationFormat.HUMAN_PRETTY) + ), seekBackwardEnabled = uiState.seekBackwardEnabled, seekForwardEnabled = uiState.seekForwardEnabled, seekForwards = seekForwards, @@ -254,7 +247,7 @@ fun TimeframeSelectionHeader( Icon( modifier = Modifier.size(32.dp), imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, - contentDescription = "Back" + contentDescription = stringResource(id = R.string.statistics_seek_backwards_description) ) } Column( @@ -279,7 +272,7 @@ fun TimeframeSelectionHeader( Icon( modifier = Modifier.size(32.dp), imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "Forward" + contentDescription = stringResource(id = R.string.statistics_seek_forwards_description) ) } } diff --git a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsViewModel.kt b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsViewModel.kt index dec5a2670..9c03b4dd4 100644 --- a/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsViewModel.kt +++ b/app/src/main/java/app/musikus/statistics/presentation/sessionstatistics/SessionStatisticsViewModel.kt @@ -8,26 +8,34 @@ package app.musikus.statistics.presentation.sessionstatistics +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.PieChart import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.musikus.R +import app.musikus.core.data.EnumWithIcon +import app.musikus.core.data.EnumWithLabel import app.musikus.core.data.SectionWithLibraryItem import app.musikus.core.data.SessionWithSectionsWithLibraryItems -import app.musikus.library.data.daos.LibraryItem -import app.musikus.core.presentation.utils.DurationFormat -import app.musikus.core.presentation.utils.getDurationString -import app.musikus.sessions.domain.usecase.SessionsUseCases -import app.musikus.settings.domain.usecase.UserPreferencesUseCases import app.musikus.core.domain.DateFormat -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo import app.musikus.core.domain.TimeProvider import app.musikus.core.domain.Timeframe import app.musikus.core.domain.musikusFormat -import app.musikus.core.domain.sorted import app.musikus.core.domain.specificDay import app.musikus.core.domain.specificMonth import app.musikus.core.domain.specificWeek +import app.musikus.core.presentation.utils.DurationFormat +import app.musikus.core.presentation.utils.UiIcon +import app.musikus.core.presentation.utils.UiText +import app.musikus.core.presentation.utils.getDurationString +import app.musikus.library.data.LibraryItemSortMode +import app.musikus.library.data.daos.LibraryItem +import app.musikus.library.data.sorted +import app.musikus.sessions.domain.usecase.SessionsUseCases +import app.musikus.settings.domain.usecase.UserPreferencesUseCases import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -48,19 +56,38 @@ import kotlin.time.Duration.Companion.seconds * Ui state data classes */ -enum class SessionStatisticsTab { - DAYS, - WEEKS, - MONTHS; +enum class SessionStatisticsTab : EnumWithLabel { + DAYS { + override val label = UiText.StringResource(R.string.statistics_session_statistics_tab_days) + }, + WEEKS { + override val label = UiText.StringResource(R.string.statistics_session_statistics_tab_weeks) + }, + MONTHS { + override val label = UiText.StringResource(R.string.statistics_session_statistics_tab_months) + }; companion object { val DEFAULT = DAYS } } -enum class SessionStatisticsChartType { - PIE, - BAR; +enum class SessionStatisticsChartType : EnumWithLabel, EnumWithIcon { + PIE { + override val label = UiText.StringResource(R.string.statistics_session_statistics_chart_type_pie) + override val icon = UiIcon.DynamicIcon(Icons.Default.PieChart) + }, + BAR { + override val label = UiText.StringResource(R.string.statistics_session_statistics_chart_type_bar) + override val icon = UiIcon.DynamicIcon(Icons.Default.BarChart) + }; + + fun invert(): SessionStatisticsChartType { + return when(this) { + PIE -> BAR + BAR -> PIE + } + } companion object { val DEFAULT = BAR diff --git a/app/src/main/res/values/activesession_strings.xml b/app/src/main/res/values/activesession_strings.xml new file mode 100644 index 000000000..db5d985f8 --- /dev/null +++ b/app/src/main/res/values/activesession_strings.xml @@ -0,0 +1,57 @@ + + + + + Metronome + Recorder + Discard session? All progress will be lost! + + + Finish + + + Practice Time + Paused %1$s + + + Already practiced + + + Section deleted + Section deleted + + + Start practicing + Next item + + + Select a library item + Create item + Create folder + + + no folder + + + Finish session + Rate your session: + Comment (optional) + Keep Practicing + Save + + + Practicing for %1$s + Practicing Paused + %1$s - Total: %2$s + + Pause + Resume + Finish + + \ No newline at end of file diff --git a/app/src/main/res/values/goals_strings.xml b/app/src/main/res/values/goals_strings.xml new file mode 100644 index 000000000..a62e8018a --- /dev/null +++ b/app/src/main/res/values/goals_strings.xml @@ -0,0 +1,76 @@ + + + + Goals + + Regular goal + One shot goal + + All items + Specific item + + Time + Sessions + + Day + Week + Month + + + in one day + in %d days + + + in one week + in %d weeks + + + in one month + in %d months + + + Date added + Target + Period + + + goals + Add goal + Create some goals to get started on tracking your progress. + + + Delete %1$d goal? It will be erased from your statistics and cannot be restored. If you want to keep your statistics, consider archiving the goal instead. + Delete %1$d goals? They will be erased from your statistics and cannot be restored. If you want to keep your statistics, consider archiving the goals instead. + + + Archive %1$d goal? It will remain in your statistics but progress towards it will no longer be tracked. + Archive %1$d goals? They will remain in your statistics but progress towards them will no longer be tracked. + + Delete forever (%1$s) + Archive forever (%1$s) + Deleted + Archived + + + Goal achieved! + + + Create a new goal + Edit goal target + Select a library item + Select library item + No items in your library. + Create + Edit + + + in + Select period unit + + \ No newline at end of file diff --git a/app/src/main/res/values/library_strings.xml b/app/src/main/res/values/library_strings.xml new file mode 100644 index 000000000..184fa9c6d --- /dev/null +++ b/app/src/main/res/values/library_strings.xml @@ -0,0 +1,79 @@ + + + + + Library + + + item + items + + + + folder + folders + + + Date added + Last modified + Name + Color + + Date added + Last modified + Name + + + Create folder + Edit folder + Folder name + Create + Edit + + + Add new Item + Edit Item + Item name + Folder + No folder + Select folder + Color %1$d + Create + Edit + + + Add item + Add folder or item + Add item + Add folder + Create library items for any piece, scale or other exercise you want to practice. + + + Delete %1$d %2$s? It will remain in your statistics, but you will no longer be able to practice them. + Delete %1$d %2$s? They will remain in your statistics, but you will no longer be able to practice them. + + + Delete %1$d %2$s and %3$d %4$s? They will remain in your statistics, but you will no longer be able to practice them. + + Delete forever (%1$s) + Deleted + + + Folders + folders + %1$d items + + Items + items + last practiced: %1$s + never + + + + \ No newline at end of file diff --git a/app/src/main/res/values/metronome_strings.xml b/app/src/main/res/values/metronome_strings.xml new file mode 100644 index 000000000..93babb569 --- /dev/null +++ b/app/src/main/res/values/metronome_strings.xml @@ -0,0 +1,42 @@ + + + + + Metronome + Metronome is running + + + Grave + Largo + Lento + Adagio + Andante + Andante moderato + Moderato + Allegretto moderato + Allegro + Vivace + Presto + Prestissimo + + + bpm + Start metronome + Stop metronome + + + Beats/bar + Clicks/beat + Tap tempo + + + Decrement + Increment + + \ No newline at end of file diff --git a/app/src/main/res/values/permissions_strings.xml b/app/src/main/res/values/permissions_strings.xml new file mode 100644 index 000000000..ed8e24128 --- /dev/null +++ b/app/src/main/res/values/permissions_strings.xml @@ -0,0 +1,14 @@ + + + + + Permission required + Go to App Settings + + \ No newline at end of file diff --git a/app/src/main/res/values/quotes.xml b/app/src/main/res/values/quotes.xml index 24b811494..8be9c2ba5 100644 --- a/app/src/main/res/values/quotes.xml +++ b/app/src/main/res/values/quotes.xml @@ -1,110 +1,110 @@ - Everything we do is practice for something greater than where we currently are. Practice only makes for improvement.\u0020\u0020-\u00A0Les\u00A0Brown - Practice like you\'ve never won. Perform like you\'ve never lost.\u0020\u0020-\u00A0Michael\u00A0Jordan - Practice like it means everything in the world to you. Perform like you don\'t give a damn.\u0020\u0020-\u00A0Jascha\u00A0Heifetz - The most perfect technique is that which is not noticed at all.\u0020\u0020-\u00A0Pablo\u00A0Casals - I think it\'s beautiful to practice. I love to practice.\u0020\u0020-\u00A0Claudio\u00A0Arrau - When I am playing, I am in ecstasy; that is what I live for.\u0020\u0020-\u00A0Claudio\u00A0Arrau - The art of interpretation is not to play what is written.\u0020\u0020-\u00A0Pablo\u00A0Casals - Making mistakes is not my enemy... fear of making mistakes is my enemy\u0020\u0020-\u00A0Anne\u00A0Shih - Sweat more in practise, bleed less in war.\u0020\u0020-\u00A0Spartan\u00A0Warrior\u00A0Credo - Practice is the best of all instructors\u0020\u0020-\u00A0Publius\u00A0Syrus - Great thoughts reduced to practice become great acts\u0020\u0020-\u00A0William\u00A0Hazlitt - You can\'t hire someone to practice for you.\u0020\u0020-\u00A0H. Jackson Brown, Jr. - For the things we have to learn before we can do them, we learn by doing them.\u0020\u0020-\u00A0Aristotle - I don\'t know if I practiced more than anybody, but I sure practiced enough. I still wonder if somebody — somewhere — was practicing more than me.\u0020\u0020-\u00A0Larry\u00A0Bird - Sincere practice, makes the impossible possible.\u0020\u0020-\u00A0Dada\u00A0Vaswani - Champions keep playing until they get it right.\u0020\u0020-\u00A0Billie\u00A0Jean\u00A0King - You are what you practice most.\u0020\u0020-\u00A0Richard\u00A0Carlson - Don\'t get set into one form, adapt it and build your own, and let it grow, be like water.\u0020\u0020-\u00A0Bruce\u00A0Lee - Determination, effort, and practice are rewarded with success.\u0020\u0020-\u00A0Mary\u00A0Lydon\u00A0Simonsen - The greater danger for most of us lies not in setting our aim too high and falling short; but in setting our aim too low, and achieving our mark.\u0020\u0020-\u00A0Michelangelo - Practice without improvement is meaningless.\u0020\u0020-\u00A0Chuck\u00A0Knox - Tomorrow\'s victory is today\'s practice.\u0020\u0020-\u00A0Chris\u00A0Bradford - - Practice the way you want to perform. Practice as if you were performing.\u0020\u0020-\u00A0Master\u00A0Advice - If you don\'t believe you are the best, then you will never achieve all that you are capable of.\u0020\u0020-\u00A0Cristiano\u00A0Ronaldo - They say that nobody is perfect. Then they tell you practice makes perfect. I wish they\'d make up their minds.\u0020\u0020-\u00A0Winston\u00A0Churchill - To become really good at anything, you have to practice and repeat, practice and repeat, until the technique becomes intuitive.\u0020\u0020-\u00A0Paulo\u00A0Coelho - Persistence, persistence, and persistence. The power can be created and maintained through daily practice - continuous effort.\u0020\u0020-\u00A0Bruce\u00A0Lee - If you don\'t practice, you don\'t deserve to win.\u0020\u0020-\u00A0Andre\u00A0Agassi - Talent is a pursued interest. Anything that you\'re willing to practice, you can do.\u0020\u0020-\u00A0Bob\u00A0Ross - Practice like you\'ve never won. Play like you\'ve never lost.\u0020\u0020-\u00A0Michael\u00A0Jordan - Experts were once amateurs who kept practicing.\u0020\u0020-\u00A0Amit\u00A0Kalantri - I\'ve always considered myself to be just average talent and what I have is a ridiculous insane obsessiveness for practice and preparation.\u0020\u0020-\u00A0Will\u00A0Smith - I do not play this instrument so well as I should wish to, but I have always supposed that to be my own fault because I would not take the trouble of practicing.\u0020\u0020-\u00A0Jane\u00A0Austen - Practice makes permanent, not perfect. If you practice the wrong thing, you make the wrong act permanent.\u0020\u0020-\u00A0Hamza\u00A0Yusuf - You don\'t learn to walk by following rules. You learn by doing and falling over.\u0020\u0020-\u00A0Richard\u00A0Branson - Practice every time you get a chance.\u0020\u0020-\u00A0Bill\u00A0Monroe - Amateurs practice until they get it right, experts practice until they can\'t get it wrong.\u0020\u0020-\u00A0Julie\u00A0Andrews - You always have to be on edge. You always have to take every practice, every game, like it is your last.\u0020\u0020-\u00A0Kobe\u00A0Bryant - Knowledge is of no value unless you put it into practice.\u0020\u0020-\u00A0Anton\u00A0Chekhov - Sketching on a regular basis requires you to pay close attention to the visual world around you. With practice, you begin to see things you never noticed before.\u0020\u0020-\u00A0Paul\u00A0Laseau - In previous years I was so fired up at times I made little mistakes. So I kept telling myself to be patient, relax, play like you do in practice.\u0020\u0020-\u00A0Randall\u00A0Cunningham - You can practice shooting eight hours a day, but if your technique is wrong, then all you become is very good at shooting the wrong way. Get the fundamentals down and the level of everything you do will rise.\u0020\u0020-\u00A0Michael\u00A0Jordan - It is performance ready if you can play 7 times without error. If you mess up on the 6th run through, you have to start over.\u0020\u0020-\u00A0Musician\u00A0Truth - Talent without working hard is nothing.\u0020\u0020-\u00A0Cristiano\u00A0Ronaldo - Do or do not, there is no try.\u0020\u0020-\u00A0Yoda - It takes a long time to sound like yourself\u0020\u0020-\u00A0Miles\u00A0Davis - Give me six hours to chop down a tree and I will spend the first four sharpening the ax.\u0020\u0020-\u00A0Abraham\u00A0Lincoln - The goal of practice is always to keep our beginner\'s mind.\u0020\u0020-\u00A0Jack\u00A0Kornfield - If you think you can or you think you can\'t, you\'re right.\u0020\u0020-\u00A0Henry\u00A0Ford - Practice does not make perfect. Only perfect practice makes perfect.\u0020\u0020-\u00A0Vince\u00A0Lombardi - Hard work beats talent when talent doesn\'t work hard.\u0020\u0020-\u00A0Unknown - I\'m a strong believer that you practice like you play, little things make big things happen.\u0020\u0020-\u00A0Tony\u00A0Dorsett - - I am one of those who will go on doing till all doings are at an end.\u0020\u0020-\u00A0Wolfgang\u00A0Amadeus\u00A0Mozart - The better voice doesn\'t mean being a better singer.\u0020\u0020-\u00A0Luciano\u00A0Pavarotti - The rivalry is with ourself. I try to be better than is possible. I fight against myself, not against the other.\u0020\u0020-\u00A0Luciano\u00A0Pavarotti - If I don\'t practice one day, I know it; two days, the critics know it; three days, the public knows it.\u0020\u0020-\u00A0Jascha\u00A0Heifetz - The most important thing to do is really listen.\u0020\u0020-\u00A0Itzhak\u00A0Perlman - Passion is one great force that unleashes creativity, because if you\'re passionate about something, then you\'re more willing to take risks.\u0020\u0020-\u00A0Yo-Yo Ma - A wrong note played with the right intention is much to be preferred to the right note played with no soul.\u0020\u0020-\u00A0Janine\u00A0Jansen - If you\'re playing within your capability, what\'s the point? If you\'re not pushing your own technique to its own limits with the risk that it might just crumble at any moment, then you\'re not really doing your job.\u0020\u0020-\u00A0Nigel\u00A0Kennedy - My dear hands. Farewell, my poor hands.\u0020\u0020-\u00A0Sergei\u00A0Rachmaninov - Simplicity is the highest goal, achievable when you have overcome all difficulties.\u0020\u0020-\u00A0Frederic\u00A0Chopin - To play without passion is inexcusable!\u0020\u0020-\u00A0Ludwig\u00A0van\u00A0Beeethoven - An artist is someone who has learned to trust in himself.\u0020\u0020-\u00A0Ludwig\u00A0van\u00A0Beethoven - Play always as if in the presence of a master.\u0020\u0020-\u00A0Robert\u00A0Schumann - You have to put in the hours because there\'s always something which you can improve.\u0020\u0020-\u00A0Roger\u00A0Federer - Good ideas are not adopted automatically. They must be driven into practice with courageous patience.\u0020\u0020-\u00A0Hyman\u00A0Rickover - If I thought of gymnastics as a job, it would put too much stress on me ... At the end of the day, if I can say I had fun, it was a good day.\u0020\u0020-\u00A0Simone\u00A0Biles - You can\'t jump from little things to big things. It just takes time and patience.\u0020\u0020-\u00A0Nadia\u00A0Comaneci - It\'s very hard to get to the top. It\'s hardest to stay at the top.\u0020\u0020-\u00A0Nadia\u00A0Comaneci - I want to be able to look back and say, I\'ve done everything I can, and I was successful.\' I don\'t want to look back and say I should have done this or that.\u0020\u0020-\u00A0Michael\u00A0Phelps - I think goals should never be easy. They should force you to work, even if they are uncomfortable at the time.\u0020\u0020-\u00A0Michael\u00A0Phelps - If you want to be the best, you have to do things that other people aren\'t willing to do.\u0020\u0020-\u00A0Michael\u00A0Phelps - You can\'t put a limit on anything. The more you dream, the farther you get.\u0020\u0020-\u00A0Michael\u00A0Phelps - Tough times don\'t last — tough people do!\u0020\u0020-\u00A0Dan\u00A0Peña - Don\'t just work longer hours, \'staying busy\' as most other people do. \'Work smart\' i.e. Stay productive and don\'t get distracted.\u0020\u0020-\u00A0Dan\u00A0Peña - Just doing as well as you did last time is not good enough.\u0020\u0020-\u00A0Michael\u00A0Jackson - Practice means to perform, over and over again in the face of all obstacles, some act of vision, of faith, of desire. Practice is a means of inviting the perfection desired.\u0020\u0020-\u00A0Martha\u00A0Graham - The greatest education in the world is watching the masters at work.\u0020\u0020-\u00A0Michael\u00A0Jackson - Do not go to bed until you have gone over the day three times in your mind. What wrong did I do? What good did I accomplish? What did I forget to do?\u0020\u0020-\u00A0Pythagoras - Knowledge is not skill. Knowledge plus ten thousand times is skill.\u0020\u0020-\u00A0Sinichi\u00A0Suzuki - If you spend too much time thinking about a thing, you will never get it done.\u0020\u0020-\u00A0Bruce\u00A0Lee - Your mind must get there before your fingers will.\u0020\u0020-\u00A0Musicians\u00A0Truth - One thing I have learned is that I\'m not the owner of my talent; I\'m the manager of it.\u0020\u0020-\u00A0Madonna - Write down specific measurable goals, and check them off as you achieve them.\u0020\u0020-\u00A0Master\u00A0advice - If you practice correctly from the beginning, you will save yourself a lot of time and frustration later on. Don\'t practice bad habits!\u0020\u0020-\u00A0Master\u00A0advice - Find a way to make your practice routine exciting! And when it isn\'t exciting anymore, change up the routine.\u0020\u0020-\u00A0Master\u00A0advice - No matter how you feel, get up, dress up, show up, and practice.\u0020\u0020-\u00A0Master\u00A0advice - When you are not practising, someone else is getting better.\u0020\u0020-\u00A0Allen\u00A0Iverson - Arriving at one goal is the starting point to another.\u0020\u0020-\u00A0John\u00A0Dewey - Setting goals is the first step in turning the invisible into the visible.\u0020\u0020-\u00A0Tony\u00A0Robbins - - - I fear not the man who has practiced 10,000 kicks once, but I fear the man who has practiced one kick 10,000 times.\u0020\u0020-\u00A0Bruce\u00A0Lee - Use only that which works, and take it from any place you can find it.\u0020\u0020-\u00A0Bruce\u00A0Lee - If I wanted to play the violin, I had to work. Because anything that one wants to do really, and one loves doing, one must do everyday. It should be as easy to the artist and as natural as flying is to a bird. And you can\'t imagine a bird saying well, I\'m tired today, I\'m not going to fly!\u0020\u0020-\u00A0Yehudi\u00A0Menuhin - One must always practice slowly. If you learn something slowly, you forget it slowly.\u0020\u0020-\u00A0Itzhak\u00A0Perlman - Quick music sounds dull unless every note is articulated.\u0020\u0020-\u00A0Herbert\u00A0von\u00A0Karajan - It is only through failure and through experiment that we learn and grow.\u0020\u0020-\u00A0Isaac\u00A0Stern - - I can\'t do it... YET - - + Everything we do is practice for something greater than where we currently are. Practice only makes for improvement.\u0020\u0020-\u00A0Les\u00A0Brown]]> + Practice like you\'ve never won. Perform like you\'ve never lost.\u0020\u0020-\u00A0Michael\u00A0Jordan]]> + Practice like it means everything in the world to you. Perform like you don\'t give a damn.\u0020\u0020-\u00A0Jascha\u00A0Heifetz]]> + The most perfect technique is that which is not noticed at all.\u0020\u0020-\u00A0Pablo\u00A0Casals]]> + I think it\'s beautiful to practice. I love to practice.\u0020\u0020-\u00A0Claudio\u00A0Arrau]]> + When I am playing, I am in ecstasy; that is what I live for.\u0020\u0020-\u00A0Claudio\u00A0Arrau]]> + The art of interpretation is not to play what is written.\u0020\u0020-\u00A0Pablo\u00A0Casals]]> + Making mistakes is not my enemy… fear of making mistakes is my enemy\u0020\u0020-\u00A0Anne\u00A0Shih]]> + Sweat more in practise, bleed less in war.\u0020\u0020-\u00A0Spartan\u00A0Warrior\u00A0Credo]]> + Practice is the best of all instructors\u0020\u0020-\u00A0Publius\u00A0Syrus]]> + Great thoughts reduced to practice become great acts\u0020\u0020-\u00A0William\u00A0Hazlitt]]> + You can\'t hire someone to practice for you.\u0020\u0020-\u00A0H. Jackson Brown, Jr.]]> + For the things we have to learn before we can do them, we learn by doing them.\u0020\u0020-\u00A0Aristotle]]> + I don\'t know if I practiced more than anybody, but I sure practiced enough. I still wonder if somebody — somewhere — was practicing more than me.\u0020\u0020-\u00A0Larry\u00A0Bird]]> + Sincere practice, makes the impossible possible.\u0020\u0020-\u00A0Dada\u00A0Vaswani]]> + Champions keep playing until they get it right.\u0020\u0020-\u00A0Billie\u00A0Jean\u00A0King]]> + You are what you practice most.\u0020\u0020-\u00A0Richard\u00A0Carlson]]> + Don\'t get set into one form, adapt it and build your own, and let it grow, be like water.\u0020\u0020-\u00A0Bruce\u00A0Lee]]> + Determination, effort, and practice are rewarded with success.\u0020\u0020-\u00A0Mary\u00A0Lydon\u00A0Simonsen]]> + The greater danger for most of us lies not in setting our aim too high and falling short; but in setting our aim too low, and achieving our mark.\u0020\u0020-\u00A0Michelangelo]]> + Practice without improvement is meaningless.\u0020\u0020-\u00A0Chuck\u00A0Knox]]> + Tomorrow\'s victory is today\'s practice.\u0020\u0020-\u00A0Chris\u00A0Bradford]]> + + Practice the way you want to perform. Practice as if you were performing.\u0020\u0020-\u00A0Master\u00A0Advice]]> + If you don\'t believe you are the best, then you will never achieve all that you are capable of.\u0020\u0020-\u00A0Cristiano\u00A0Ronaldo]]> + They say that nobody is perfect. Then they tell you practice makes perfect. I wish they\'d make up their minds.\u0020\u0020-\u00A0Winston\u00A0Churchill]]> + To become really good at anything, you have to practice and repeat, practice and repeat, until the technique becomes intuitive.\u0020\u0020-\u00A0Paulo\u00A0Coelho]]> + Persistence, persistence, and persistence. The power can be created and maintained through daily practice - continuous effort.\u0020\u0020-\u00A0Bruce\u00A0Lee]]> + If you don\'t practice, you don\'t deserve to win.\u0020\u0020-\u00A0Andre\u00A0Agassi]]> + Talent is a pursued interest. Anything that you\'re willing to practice, you can do.\u0020\u0020-\u00A0Bob\u00A0Ross]]> + Practice like you\'ve never won. Play like you\'ve never lost.\u0020\u0020-\u00A0Michael\u00A0Jordan]]> + Experts were once amateurs who kept practicing.\u0020\u0020-\u00A0Amit\u00A0Kalantri]]> + I\'ve always considered myself to be just average talent and what I have is a ridiculous insane obsessiveness for practice and preparation.\u0020\u0020-\u00A0Will\u00A0Smith]]> + I do not play this instrument so well as I should wish to, but I have always supposed that to be my own fault because I would not take the trouble of practicing.\u0020\u0020-\u00A0Jane\u00A0Austen]]> + Practice makes permanent, not perfect. If you practice the wrong thing, you make the wrong act permanent.\u0020\u0020-\u00A0Hamza\u00A0Yusuf]]> + You don\'t learn to walk by following rules. You learn by doing and falling over.\u0020\u0020-\u00A0Richard\u00A0Branson]]> + Practice every time you get a chance.\u0020\u0020-\u00A0Bill\u00A0Monroe]]> + Amateurs practice until they get it right, experts practice until they can\'t get it wrong.\u0020\u0020-\u00A0Julie\u00A0Andrews]]> + You always have to be on edge. You always have to take every practice, every game, like it is your last.\u0020\u0020-\u00A0Kobe\u00A0Bryant]]> + Knowledge is of no value unless you put it into practice.\u0020\u0020-\u00A0Anton\u00A0Chekhov]]> + Sketching on a regular basis requires you to pay close attention to the visual world around you. With practice, you begin to see things you never noticed before.\u0020\u0020-\u00A0Paul\u00A0Laseau]]> + In previous years I was so fired up at times I made little mistakes. So I kept telling myself to be patient, relax, play like you do in practice.\u0020\u0020-\u00A0Randall\u00A0Cunningham]]> + You can practice shooting eight hours a day, but if your technique is wrong, then all you become is very good at shooting the wrong way. Get the fundamentals down and the level of everything you do will rise.\u0020\u0020-\u00A0Michael\u00A0Jordan]]> + It is performance ready if you can play 7 times without error. If you mess up on the 6th run through, you have to start over.\u0020\u0020-\u00A0Musician\u00A0Truth]]> + Talent without working hard is nothing.\u0020\u0020-\u00A0Cristiano\u00A0Ronaldo]]> + Do or do not, there is no try.\u0020\u0020-\u00A0Yoda]]> + It takes a long time to sound like yourself\u0020\u0020-\u00A0Miles\u00A0Davis]]> + Give me six hours to chop down a tree and I will spend the first four sharpening the ax.\u0020\u0020-\u00A0Abraham\u00A0Lincoln]]> + The goal of practice is always to keep our beginner\'s mind.\u0020\u0020-\u00A0Jack\u00A0Kornfield]]> + If you think you can or you think you can\'t, you\'re right.\u0020\u0020-\u00A0Henry\u00A0Ford]]> + Practice does not make perfect. Only perfect practice makes perfect.\u0020\u0020-\u00A0Vince\u00A0Lombardi]]> + Hard work beats talent when talent doesn\'t work hard.\u0020\u0020-\u00A0Unknown]]> + I\'m a strong believer that you practice like you play, little things make big things happen.\u0020\u0020-\u00A0Tony\u00A0Dorsett]]> + + I am one of those who will go on doing till all doings are at an end.\u0020\u0020-\u00A0Wolfgang\u00A0Amadeus\u00A0Mozart]]> + The better voice doesn\'t mean being a better singer.\u0020\u0020-\u00A0Luciano\u00A0Pavarotti]]> + The rivalry is with ourself. I try to be better than is possible. I fight against myself, not against the other.\u0020\u0020-\u00A0Luciano\u00A0Pavarotti]]> + If I don\'t practice one day, I know it; two days, the critics know it; three days, the public knows it.\u0020\u0020-\u00A0Jascha\u00A0Heifetz]]> + The most important thing to do is really listen.\u0020\u0020-\u00A0Itzhak\u00A0Perlman]]> + Passion is one great force that unleashes creativity, because if you\'re passionate about something, then you\'re more willing to take risks.\u0020\u0020-\u00A0Yo-Yo Ma]]> + A wrong note played with the right intention is much to be preferred to the right note played with no soul.\u0020\u0020-\u00A0Janine\u00A0Jansen]]> + If you\'re playing within your capability, what\'s the point? If you\'re not pushing your own technique to its own limits with the risk that it might just crumble at any moment, then you\'re not really doing your job.\u0020\u0020-\u00A0Nigel\u00A0Kennedy]]> + My dear hands. Farewell, my poor hands.\u0020\u0020-\u00A0Sergei\u00A0Rachmaninov]]> + Simplicity is the highest goal, achievable when you have overcome all difficulties.\u0020\u0020-\u00A0Frederic\u00A0Chopin]]> + To play without passion is inexcusable!\u0020\u0020-\u00A0Ludwig\u00A0van\u00A0Beeethoven]]> + An artist is someone who has learned to trust in himself.\u0020\u0020-\u00A0Ludwig\u00A0van\u00A0Beethoven]]> + Play always as if in the presence of a master.\u0020\u0020-\u00A0Robert\u00A0Schumann]]> + You have to put in the hours because there\'s always something which you can improve.\u0020\u0020-\u00A0Roger\u00A0Federer]]> + Good ideas are not adopted automatically. They must be driven into practice with courageous patience.\u0020\u0020-\u00A0Hyman\u00A0Rickover]]> + If I thought of gymnastics as a job, it would put too much stress on me… At the end of the day, if I can say I had fun, it was a good day.\u0020\u0020-\u00A0Simone\u00A0Biles]]> + You can\'t jump from little things to big things. It just takes time and patience.\u0020\u0020-\u00A0Nadia\u00A0Comaneci]]> + It\'s very hard to get to the top. It\'s hardest to stay at the top.\u0020\u0020-\u00A0Nadia\u00A0Comaneci]]> + I want to be able to look back and say, I\'ve done everything I can, and I was successful.\' I don\'t want to look back and say I should have done this or that.\u0020\u0020-\u00A0Michael\u00A0Phelps]]> + I think goals should never be easy. They should force you to work, even if they are uncomfortable at the time.\u0020\u0020-\u00A0Michael\u00A0Phelps]]> + If you want to be the best, you have to do things that other people aren\'t willing to do.\u0020\u0020-\u00A0Michael\u00A0Phelps]]> + You can\'t put a limit on anything. The more you dream, the farther you get.\u0020\u0020-\u00A0Michael\u00A0Phelps]]> + Tough times don\'t last — tough people do!\u0020\u0020-\u00A0Dan\u00A0Peña]]> + Don\'t just work longer hours, \'staying busy\' as most other people do. \'Work smart\' i.e. Stay productive and don\'t get distracted.\u0020\u0020-\u00A0Dan\u00A0Peña]]> + Just doing as well as you did last time is not good enough.\u0020\u0020-\u00A0Michael\u00A0Jackson]]> + Practice means to perform, over and over again in the face of all obstacles, some act of vision, of faith, of desire. Practice is a means of inviting the perfection desired.\u0020\u0020-\u00A0Martha\u00A0Graham]]> + The greatest education in the world is watching the masters at work.\u0020\u0020-\u00A0Michael\u00A0Jackson]]> + Do not go to bed until you have gone over the day three times in your mind. What wrong did I do? What good did I accomplish? What did I forget to do?\u0020\u0020-\u00A0Pythagoras]]> + Knowledge is not skill. Knowledge plus ten thousand times is skill.\u0020\u0020-\u00A0Sinichi\u00A0Suzuki]]> + If you spend too much time thinking about a thing, you will never get it done.\u0020\u0020-\u00A0Bruce\u00A0Lee]]> + Your mind must get there before your fingers will.\u0020\u0020-\u00A0Musicians\u00A0Truth]]> + One thing I have learned is that I\'m not the owner of my talent; I\'m the manager of it.\u0020\u0020-\u00A0Madonna]]> + Write down specific measurable goals, and check them off as you achieve them.\u0020\u0020-\u00A0Master\u00A0advice]]> + If you practice correctly from the beginning, you will save yourself a lot of time and frustration later on. Don\'t practice bad habits!\u0020\u0020-\u00A0Master\u00A0advice]]> + Find a way to make your practice routine exciting! And when it isn\'t exciting anymore, change up the routine.\u0020\u0020-\u00A0Master\u00A0advice]]> + No matter how you feel, get up, dress up, show up, and practice.\u0020\u0020-\u00A0Master\u00A0advice]]> + When you are not practising, someone else is getting better.\u0020\u0020-\u00A0Allen\u00A0Iverson]]> + Arriving at one goal is the starting point to another.\u0020\u0020-\u00A0John\u00A0Dewey]]> + Setting goals is the first step in turning the invisible into the visible.\u0020\u0020-\u00A0Tony\u00A0Robbins]]> + + + I fear not the man who has practiced 10,000 kicks once, but I fear the man who has practiced one kick 10,000 times.\u0020\u0020-\u00A0Bruce\u00A0Lee]]> + Use only that which works, and take it from any place you can find it.\u0020\u0020-\u00A0Bruce\u00A0Lee]]> + If I wanted to play the violin, I had to work. Because anything that one wants to do really, and one loves doing, one must do everyday. It should be as easy to the artist and as natural as flying is to a bird. And you can\'t imagine a bird saying well, I\'m tired today, I\'m not going to fly!\u0020\u0020-\u00A0Yehudi\u00A0Menuhin]]> + One must always practice slowly. If you learn something slowly, you forget it slowly.\u0020\u0020-\u00A0Itzhak\u00A0Perlman]]> + Quick music sounds dull unless every note is articulated.\u0020\u0020-\u00A0Herbert\u00A0von\u00A0Karajan]]> + It is only through failure and through experiment that we learn and grow.\u0020\u0020-\u00A0Isaac\u00A0Stern]]> + + I can\'t do it… YET]]> + + diff --git a/app/src/main/res/values/recorder_strings.xml b/app/src/main/res/values/recorder_strings.xml new file mode 100644 index 000000000..58f6c51cd --- /dev/null +++ b/app/src/main/res/values/recorder_strings.xml @@ -0,0 +1,68 @@ + + + + + Active recording + Recording finished + click to open + + + Delete + Microphone not available + Start recording + Pause recording + Resume recording + Save + + + No Recordings + + + Close player + + + Save recording as: + Recording name + Save + + + Delete recording + Delete + + + Musikus + Recording + + + Microphone permission required + Storage permission required + Notification permission required + Could not load recording + Cannot find recorder service + Could not create MediaStore entry + Could not update MediaStore entry + Tried to stop recording without initializing mediaRecorder + + Tried to start recorder while it is already recording + Tried to start recorder while it is paused + Tried to start recording without initializing mediaRecorder + + Tried to pause while recorder is not recording + Tried to pause recording without initializing mediaRecorder + + Tried to resume while recorder is not paused + Tried to resume recording without initializing mediaRecorder + + Can only delete recording from paused state" + Tried to delete recording without initializing mediaRecorder + + Can only save recording from paused state" + Tried to save recording without initializing mediaRecorder + + \ No newline at end of file diff --git a/app/src/main/res/values/sessions_strings.xml b/app/src/main/res/values/sessions_strings.xml new file mode 100644 index 000000000..02f1dfa69 --- /dev/null +++ b/app/src/main/res/values/sessions_strings.xml @@ -0,0 +1,30 @@ + + + + Sessions + + + Press \"Start session\" to start your first practice session. + + Start session + Resume session + + + Delete %1$d session? Any progress made during this session will be lost. + Delete %1$d sessions? Any progress made during these sessions will be lost. + + Delete forever (%1$s) + Deleted + + + Practice time + Break time + Comment + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_strings.xml b/app/src/main/res/values/settings_strings.xml new file mode 100644 index 000000000..f3792ae72 --- /dev/null +++ b/app/src/main/res/values/settings_strings.xml @@ -0,0 +1,113 @@ + + + + Settings + About the app + Appearance + Backup & Restore + Export session data + Help + Support us! + + Made with ❤️ in Munich + + + + About + + Design & Development + Matthias Emde\nMichael Prommersberger + + Publisher + Matthias Emde\nConnollystraße 25\n80809 Munich, Germany\ncontact@musikus.app + + Privacy policy + https://github.com/matthiasemde/musikus-android/blob/main/PRIVACY.md + + Version + + Licenses + + Copyright 2024 Matthias Emde, Michael Prommersberger\nLicensed under the Mozilla Public License Version 2.0 + + + Licenses + + + + Appearance + + Language + + Theme + + Color scheme + + + Language + contribute@musikus.app]]> + Awesome! + + + Theme + + System default + Light + Dark + + + Color scheme + + Musikus + A fresh new look. + PracticeTime + Reminds you of an old friend. + Dynamic + The color scheme follows your system theme. If it looks bad, it\'s on you. + + + + Backup and restore + In this menu you can backup and restore your data to a file for backing up or transferring it to another device + Create backup + Restore backup + *Note: Loading a backup will overwrite your current data. + + + + Support us! + If you are enjoying the app and would like to support us, we would be very thankful for a small donation.\nThis will help us in developing awesome new features like: + + Sharable sessions + Integrated tuner + And many more! + + DONATE 🤍 + https://ko-fi.com/musikusapp + + + + Export session data + Export all your Sessions to a CSV file. This file can be opened with spreadsheet programs like LibreOffice Calc or Microsoft Excel. + Export sessions + not export your Goals and Library items! This file cannot be imported. For Backups, use the \"Backup and Restore\" function.]]> + + + + Help + Tips and tricks + Have you tried… + + Swiping sections during an active session to delete them + Long-clicking on sessions, goals or library items to select and delete them? + + Tutorial + Replay app introduction + + \ No newline at end of file diff --git a/app/src/main/res/values/statistics_strings.xml b/app/src/main/res/values/statistics_strings.xml new file mode 100644 index 000000000..b70e03f46 --- /dev/null +++ b/app/src/main/res/values/statistics_strings.xml @@ -0,0 +1,60 @@ + + + + + Statistics + Complete a session to see your statistics here. + + Seek backwards + Seek forwards + + + + + In %1$s + Total duration + Per session + Break per hour + Average rating + + + Sessions + Last 7 days + Total + More session statistics + + + Your Goals + Last 5 expired goals + Achieved + More goal statistics + + + Ratings + Your session ratings + + + + Session History + Total %1$s + + Days + Weeks + Months + + Pie chart + Bar chart + + + + Goal History + %1$d out of %2$d + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3cb4414c4..0714c80ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,270 +4,51 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. Copyright (c) 2022-2024 Matthias Emde - - Copyright (c) 2022, Javier Carbone, author Michael Prommersberger - Additions and modifications, author Matthias Emde --> - Musikus - Comment - Practice time - Break time - Summary - Save session - PracticeTime! - Save - Keep practicing - Comment (optional) - Discard current session and lose all progress? - Discard - Keep practicing - Library - Goals - Sessions - Statistics - Add new Item - Edit Item - Item name - Add - Edit - Cancel - Finish Session - Discard Session - Practice Time - Library items: - Metronome - Recorder - Select a library item to start a new session - Hide month - Show month - Continue - Skip animation - Create a new goal - Edit goal target - Edit - Target: - Select an item - All items - Single item - h - m - in - Goals will always start at the beginning of the respective day, week, or month. - Repeat goal - Repeat the goal after the time has elapsed. If you want to create a one time goal, disable this option. - Inactive goals (%d) - Inactive goals - Restore - Restore this goal? - Restore this goal along with the item %s? - Goal restored - Delete - Irreversibly delete this goal and all of its data? - Settings - Theme - Automatic - Dark theme - Light theme - Edit - Archive - Delete - Sort by - Date added - Last modified - Name - Color - Custom - Cancel - Confirm - - Delete selected goal? - Delete selected goals? - - - Deactivate selected goal? - Deactivate selected goals? - - - Goal deactivated - Goals deactivated - - Deactivate - Delete selected session and lose all goal progress? - Delete selected sessions and lose all goal progress? - Delete goal from statistics as well - - Goal deleted - Goals deleted - - Delete selected item - Delete selected items - Library item deleted - Library items deleted - Item could not be deleted, because it is associated with at least one goal - Some items could not be deleted, because they are associated with at least one goal - Delete - %1$s left - - in one day - in %d days - - - in one week - in %d weeks - - - in one month - in %d months - - All items - Goal - selected - Section removed from session - undo - Goal progress - Create some goals to get started on tracking your progress. - Create library items for any piece, scale or other exercise you want to practice. - Press the \"+\" button to start your first practice session. - Complete a session to see your statistics here. - Start a new session - Return to running session - Create a new Item - Create a new goal - Edit Session - Cancel - Save - Your rating - Date & Time - %s at %s - Your session time - Goal achieved! - days - weeks - months - ¯\\_(ツ)_/¯ - No sessions. - Edit - Cancel - Edit section time - Discard comment? - Keep writing - Save changes to session? - Save - Discard changes? - Keep editing - 00 - Metronome - Start metronome - Stop metronome - Beats/bar - 4 - Clicks/beat - 1 - Tab tempo - Start/Pause recording - Stop recording - Location: - Music/Musikus - Open selected save location - %s saved - Delete - Recording deleted - Play recording - Duration: - Total - Practicing paused - %1$s break - Pause: %1$s - Practicing for %1$02d:%2$02d:%3$02d - Running Session - Active recording - Item cannot be deleted during running session! - Open - Today - Yesterday - No records for current goal - succeeded %1$d out of %2$d" - Practice Time - Last 7 days - Total - Your Goals - Last 5 expired goals - Achieved - Session History - Goal History - toggle chart type - Per session - Break per hour - Rating - Ø - - Ratings - Your session ratings - In %1$s - Welcome to PracticeTime! - PracticeTime helps you while practicing your instrument with time tracking and other smart features.\n\nIt is designed to be a musician\'s all-in-one tool for everyday practicing. - Create library items for any piece, scale or other exercise you want to practice.\n\nUse them during your sessions to tell PracticeTime what you are practicing. - Let\'s add your first item! - Goals help you practice consistently and prepare you for your next performance. - Add your first goal right now! - Summaries of your practice sessions as well as insightful statistics help you to stay on top of your practice habits. - Press here to start your first session! - Not now - Replay app introduction - Choose another item to add it to your session - You can swipe left on an entry to remove it from your session. - app.musikus.preferences - Help - Tips and tricks - Have you tried… - - Swiping sections during an active session to delete them - Long-clicking on sessions, goals or library items to select and delete them? - + Musikus + + + %1$s left + + + More + + Coming soon + + + Ø + + + + - Support us! - If you are enjoying the app and would like to support us, we would be very thankful for a small donation.\n\nThis will help us in developing awesome new features like: - App info - - Sharable sessions - Integrated tuner - Special layout for tablets - And many more! - + + Settings - You have denied necessary permissions for the recorder. To\u00A0enable\u00A0go\u00A0to\u00A0Settings\u00A0>\u00A0Permissions. + + Select sort mode and direction for %1$s + List of sort modes for %1$s + Sort by + + Back - Backup & Restore - Appearance - Legal Info - This project has been financed by the German music promotion program “Neustart Kultur“ by the GVL (Gesellschaft zur Verwertung von Leistungsschutzrechten mbH) - Publisher - Javier Carbone (Freelance)\nJakob Felder Weg 36\n55128, Mainz, Germany\npracticetimeapp@gmail.com - Privacy policy - https://github.com/matthiasemde/musikus-android/blob/main/PRIVACY.md - Tutorial - Open source licences - Donate 🤍 - About the app - Concept - Javier Carbone - Design & Development - Matthias Emde\nMichael Prommersberger - App Icon - Alejandra Contreras Westermeyer - https://ko-fi.com/musikusapp + + %1$d items selected + Back + Edit + Delete + Archive - MPAndroidChart - By Philipp Jahoda - https://github.com/PhilJay/MPAndroidChart + + Sessions + Goals + Statistics + Library - AppIntro - By AppIntro Developers - https://github.com/AppIntro/AppIntro - Export all your Sessions to a CSV file. This file can be opened with spreadsheet programs like LibreOffice Calc or Microsoft Excel. - *Note: This will not export your Goals and Library items! This file cannot be imported. For Backups, use the \"Backup and Restore\" function. + + Undo diff --git a/app/src/test/java/app/musikus/goals/domain/usecase/SortGoalsUseCaseTest.kt b/app/src/test/java/app/musikus/goals/domain/usecase/SortGoalsUseCaseTest.kt index 8054763a4..40bfce621 100644 --- a/app/src/test/java/app/musikus/goals/domain/usecase/SortGoalsUseCaseTest.kt +++ b/app/src/test/java/app/musikus/goals/domain/usecase/SortGoalsUseCaseTest.kt @@ -11,9 +11,9 @@ package app.musikus.goals.domain.usecase import app.musikus.core.data.GoalDescriptionWithInstancesAndLibraryItems import app.musikus.core.data.UUIDConverter import app.musikus.core.domain.FakeTimeProvider -import app.musikus.core.domain.GoalsSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.goals.data.GoalsSortMode import app.musikus.goals.data.daos.GoalDescription import app.musikus.goals.data.daos.GoalInstance import app.musikus.goals.data.entities.GoalPeriodUnit diff --git a/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCaseTest.kt b/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCaseTest.kt index 0c2cc5e39..15deffe2a 100644 --- a/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCaseTest.kt +++ b/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCaseTest.kt @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.library.domain.usecase @@ -16,10 +16,10 @@ import app.musikus.library.data.FakeLibraryRepository import app.musikus.settings.data.FakeUserPreferencesRepository import app.musikus.core.domain.FakeIdProvider import app.musikus.core.domain.FakeTimeProvider -import app.musikus.core.domain.LibraryFolderSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo import app.musikus.core.data.UUIDConverter +import app.musikus.library.data.LibraryFolderSortMode import app.musikus.settings.domain.usecase.GetFolderSortInfoUseCase import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first diff --git a/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCaseTest.kt b/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCaseTest.kt index 553297462..5f91e4bd8 100644 --- a/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCaseTest.kt +++ b/app/src/test/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCaseTest.kt @@ -3,31 +3,31 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.library.domain.usecase import app.musikus.core.data.Nullable import app.musikus.core.data.UUIDConverter +import app.musikus.core.domain.FakeIdProvider +import app.musikus.core.domain.FakeTimeProvider +import app.musikus.core.domain.SortDirection +import app.musikus.core.domain.SortInfo +import app.musikus.library.data.FakeLibraryRepository +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryItem import app.musikus.library.data.entities.LibraryFolderCreationAttributes import app.musikus.library.data.entities.LibraryItemCreationAttributes import app.musikus.library.data.entities.LibraryItemUpdateAttributes -import app.musikus.library.data.FakeLibraryRepository import app.musikus.settings.data.FakeUserPreferencesRepository import app.musikus.settings.domain.usecase.GetItemSortInfoUseCase -import app.musikus.core.domain.FakeIdProvider -import app.musikus.core.domain.FakeTimeProvider -import app.musikus.core.domain.LibraryItemSortMode -import app.musikus.core.domain.SortDirection -import app.musikus.core.domain.SortInfo +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration diff --git a/app/src/test/java/app/musikus/settings/data/FakeUserPreferencesRepository.kt b/app/src/test/java/app/musikus/settings/data/FakeUserPreferencesRepository.kt index 434cc67fe..307a42a71 100644 --- a/app/src/test/java/app/musikus/settings/data/FakeUserPreferencesRepository.kt +++ b/app/src/test/java/app/musikus/settings/data/FakeUserPreferencesRepository.kt @@ -8,12 +8,12 @@ package app.musikus.settings.data -import app.musikus.core.domain.GoalSortInfo -import app.musikus.core.domain.GoalsSortMode -import app.musikus.core.domain.LibraryFolderSortMode -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.goals.data.GoalSortInfo +import app.musikus.goals.data.GoalsSortMode +import app.musikus.library.data.LibraryFolderSortMode +import app.musikus.library.data.LibraryItemSortMode import app.musikus.library.data.daos.LibraryFolder import app.musikus.library.data.daos.LibraryItem import app.musikus.metronome.presentation.MetronomeSettings diff --git a/app/src/test/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCaseTest.kt b/app/src/test/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCaseTest.kt index def1ce724..f94a7cc57 100644 --- a/app/src/test/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCaseTest.kt +++ b/app/src/test/java/app/musikus/settings/domain/usecase/SelectFolderSortModeUseCaseTest.kt @@ -17,9 +17,9 @@ package app.musikus.settings.domain.usecase import app.musikus.settings.data.FakeUserPreferencesRepository -import app.musikus.core.domain.LibraryFolderSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.library.data.LibraryFolderSortMode import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest diff --git a/app/src/test/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCaseTest.kt b/app/src/test/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCaseTest.kt index fc5f379af..9ae72fba8 100644 --- a/app/src/test/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCaseTest.kt +++ b/app/src/test/java/app/musikus/settings/domain/usecase/SelectGoalsSortModeUseCaseTest.kt @@ -8,10 +8,10 @@ package app.musikus.settings.domain.usecase -import app.musikus.settings.data.FakeUserPreferencesRepository -import app.musikus.core.domain.GoalsSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.goals.data.GoalsSortMode +import app.musikus.settings.data.FakeUserPreferencesRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest diff --git a/app/src/test/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCaseTest.kt b/app/src/test/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCaseTest.kt index 2742d5eff..3969ebfc7 100644 --- a/app/src/test/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCaseTest.kt +++ b/app/src/test/java/app/musikus/settings/domain/usecase/SelectItemSortModeUseCaseTest.kt @@ -3,15 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * - * Copyright (c) 2023 Matthias Emde + * Copyright (c) 2023-2024 Matthias Emde */ package app.musikus.settings.domain.usecase -import app.musikus.settings.data.FakeUserPreferencesRepository -import app.musikus.core.domain.LibraryItemSortMode import app.musikus.core.domain.SortDirection import app.musikus.core.domain.SortInfo +import app.musikus.library.data.LibraryItemSortMode +import app.musikus.settings.data.FakeUserPreferencesRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking