├── .github └── workflows │ ├── ci.yml │ ├── discord-webhook.yml │ ├── release-beta.yml │ ├── release-nightly.yml │ └── validate-pr-title.yml ├── .gitignore ├── CHANGELOG.md ├── License.md ├── MPL2.0.txt ├── PRIVACY.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── config │ ├── androidLint.xml │ └── detekt.yml ├── proguard-rules.pro ├── schemas │ └── app.musikus.core.data.MusikusDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ └── 4.json └── src │ ├── androidTest │ └── java │ │ └── app │ │ ├── ComposeRule.kt │ │ ├── ScreenshotRule.kt │ │ └── musikus │ │ ├── HiltTestRunner.kt │ │ ├── activesession │ │ ├── di │ │ │ └── TestActiveSessionRepositoryModule.kt │ │ └── presentation │ │ │ ├── ActiveSessionScreenTest.kt │ │ │ └── SessionServiceTest.kt │ │ ├── core │ │ ├── data │ │ │ └── MusikusDatabaseTest.kt │ │ ├── di │ │ │ ├── TestCoroutinesDispatcherModule.kt │ │ │ ├── TestMainModule.kt │ │ │ └── TestUserPreferencesRepositoryModule.kt │ │ ├── domain │ │ │ ├── FakeIdProvider.kt │ │ │ └── FakeTimeProvider.kt │ │ └── presentation │ │ │ ├── MusikusBottomBarTest.kt │ │ │ └── MusikusNavHostTest.kt │ │ ├── goals │ │ ├── data │ │ │ └── daos │ │ │ │ ├── GoalDescriptionDaoTest.kt │ │ │ │ └── GoalInstanceDaoTest.kt │ │ ├── di │ │ │ ├── TestGoalRepositoryModule.kt │ │ │ └── TestGoalsUseCasesModule.kt │ │ └── presentation │ │ │ ├── DurationInputTest.kt │ │ │ ├── GoalDialogTest.kt │ │ │ └── NumberInputTest.kt │ │ ├── library │ │ ├── data │ │ │ └── daos │ │ │ │ ├── LibraryFolderDaoTest.kt │ │ │ │ └── LibraryItemDaoTest.kt │ │ ├── di │ │ │ └── TestLibraryRepositoryModule.kt │ │ └── presentation │ │ │ ├── LibraryFolderDetailsScreenTest.kt │ │ │ ├── LibraryIntegrationTest.kt │ │ │ └── LibraryScreenTest.kt │ │ ├── permissions │ │ ├── data │ │ │ └── TestPermissionRepository.kt │ │ └── di │ │ │ └── TestPermissionsModule.kt │ │ ├── recorder │ │ └── di │ │ │ └── TestRecordingsRepositoryModule.kt │ │ ├── repository │ │ └── FakeUserPreferencesRepository.kt │ │ └── sessionslist │ │ ├── data │ │ └── daos │ │ │ ├── SectionDaoTest.kt │ │ │ └── SessionDaoTest.kt │ │ └── di │ │ └── TestSessionRepositoryModule.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── LICENSE_APACHE-2.0.txt │ ├── ic_launcher-playstore.png │ ├── java │ │ └── app │ │ │ └── musikus │ │ │ ├── activesession │ │ │ ├── data │ │ │ │ └── ActiveSessionRepository.kt │ │ │ ├── di │ │ │ │ ├── ActiveSessionRepositoryModule.kt │ │ │ │ └── ActiveSessionUseCasesModule.kt │ │ │ ├── domain │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── ActiveSessionUseCases.kt │ │ │ │ │ ├── ComputeOngoingPauseDurationUseCase.kt │ │ │ │ │ ├── ComputeRunningItemDurationUseCase.kt │ │ │ │ │ ├── ComputeTotalPracticeDurationUseCase.kt │ │ │ │ │ ├── DeleteSectionUseCase.kt │ │ │ │ │ ├── GetActiveSessionStateUseCase.kt │ │ │ │ │ ├── GetCompletedSectionsUseCase.kt │ │ │ │ │ ├── GetFinalizedSessionUseCase.kt │ │ │ │ │ ├── GetRunningItemUseCase.kt │ │ │ │ │ ├── GetSessionStatusUseCase.kt │ │ │ │ │ ├── GetStartTimeUseCase.kt │ │ │ │ │ ├── IsSessionPausedUseCase.kt │ │ │ │ │ ├── IsSessionRunningUseCase.kt │ │ │ │ │ ├── PauseActiveSessionUseCase.kt │ │ │ │ │ ├── ResetSessionUseCase.kt │ │ │ │ │ ├── ResumeActiveSessionUseCase.kt │ │ │ │ │ └── SelectItemUseCase.kt │ │ │ └── presentation │ │ │ │ ├── ActiveSessionScreen.kt │ │ │ │ ├── ActiveSessionUiState.kt │ │ │ │ ├── ActiveSessionViewModel.kt │ │ │ │ └── SessionService.kt │ │ │ ├── core │ │ │ ├── data │ │ │ │ ├── Enums.kt │ │ │ │ ├── MusikusDatabase.kt │ │ │ │ ├── PrepopulateDatabase.kt │ │ │ │ ├── Relations.kt │ │ │ │ ├── UserPreferencesRepository.kt │ │ │ │ ├── daos │ │ │ │ │ └── GenericDaos.kt │ │ │ │ └── entities │ │ │ │ │ └── Generics.kt │ │ │ ├── di │ │ │ │ ├── CoreUseCasesModule.kt │ │ │ │ ├── CoroutineQualifiers.kt │ │ │ │ ├── CoroutineScopesModule.kt │ │ │ │ ├── CoroutinesDispatchersModule.kt │ │ │ │ ├── MainModule.kt │ │ │ │ ├── NotificationModule.kt │ │ │ │ └── UserPreferencesRepositoryModule.kt │ │ │ ├── domain │ │ │ │ ├── IdProvider.kt │ │ │ │ ├── Sorting.kt │ │ │ │ ├── TimeConversions.kt │ │ │ │ ├── TimeProvider.kt │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── ConfirmAnnouncementMessageUseCase.kt │ │ │ │ │ ├── CoreUseCases.kt │ │ │ │ │ ├── GetIdOfLastSeenAnnouncementSeenUseCase.kt │ │ │ │ │ └── ResetAnnouncementMessageUseCase.kt │ │ │ └── presentation │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── Musikus.kt │ │ │ │ ├── MusikusBottomBar.kt │ │ │ │ ├── MusikusNavHost.kt │ │ │ │ ├── MusikusNotificationManager.kt │ │ │ │ ├── MusikusTopBar.kt │ │ │ │ ├── Screen.kt │ │ │ │ ├── components │ │ │ │ ├── ActionBar.kt │ │ │ │ ├── ConditionalModifier.kt │ │ │ │ ├── DeleteConfirmationBottomSheet.kt │ │ │ │ ├── DialogActions.kt │ │ │ │ ├── DialogHeader.kt │ │ │ │ ├── DurationInput.kt │ │ │ │ ├── ExceptionHandler.kt │ │ │ │ ├── FadingEdge.kt │ │ │ │ ├── MainMenu.kt │ │ │ │ ├── MultiFab.kt │ │ │ │ ├── MusikusSegmentedButton.kt │ │ │ │ ├── NumberInput.kt │ │ │ │ ├── Scrollbar.kt │ │ │ │ ├── Selectable.kt │ │ │ │ ├── SelectionSpinner.kt │ │ │ │ ├── Snackbar.kt │ │ │ │ ├── SortMenu.kt │ │ │ │ ├── SwipeToDeleteContainer.kt │ │ │ │ ├── TwoLiner.kt │ │ │ │ └── Waveform.kt │ │ │ │ ├── theme │ │ │ │ ├── ColorScheme.kt │ │ │ │ ├── LegacyColorScheme.kt │ │ │ │ ├── PreviewStyling.kt │ │ │ │ └── Theme.kt │ │ │ │ └── utils │ │ │ │ ├── DurationFormatter.kt │ │ │ │ ├── TestTags.kt │ │ │ │ ├── UiIcon.kt │ │ │ │ └── UiText.kt │ │ │ ├── goals │ │ │ ├── data │ │ │ │ ├── GoalRepository.kt │ │ │ │ ├── GoalSorting.kt │ │ │ │ ├── daos │ │ │ │ │ ├── GoalDescriptionDao.kt │ │ │ │ │ └── GoalInstanceDao.kt │ │ │ │ └── entities │ │ │ │ │ ├── GoalDescription.kt │ │ │ │ │ ├── GoalDescriptionLibraryItemCrossRef.kt │ │ │ │ │ └── GoalInstance.kt │ │ │ ├── di │ │ │ │ ├── GoalRepositoryModule.kt │ │ │ │ └── GoalsUseCasesModule.kt │ │ │ ├── domain │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── AddGoalUseCase.kt │ │ │ │ │ ├── ArchiveGoalsUseCase.kt │ │ │ │ │ ├── CalculateGoalProgressUseCase.kt │ │ │ │ │ ├── CleanFutureGoalInstancesUseCase.kt │ │ │ │ │ ├── DeleteGoalsUseCase.kt │ │ │ │ │ ├── EditGoalUseCase.kt │ │ │ │ │ ├── GetAllGoalsUseCase.kt │ │ │ │ │ ├── GetCurrentGoalsUseCase.kt │ │ │ │ │ ├── GetGoalSortInfoUseCase.kt │ │ │ │ │ ├── GetLastFiveCompletedGoalsUseCase.kt │ │ │ │ │ ├── GetLastNBeforeInstanceUseCase.kt │ │ │ │ │ ├── GetNextNAfterInstanceUseCase.kt │ │ │ │ │ ├── GoalsUseCases.kt │ │ │ │ │ ├── PauseGoalsUseCase.kt │ │ │ │ │ ├── RestoreGoalsUseCase.kt │ │ │ │ │ ├── SelectGoalsSortModeUseCase.kt │ │ │ │ │ ├── SortGoalsUseCase.kt │ │ │ │ │ ├── UnarchiveGoalsUseCase.kt │ │ │ │ │ ├── UnpauseGoalsUseCase.kt │ │ │ │ │ └── UpdateGoalsUseCase.kt │ │ │ └── presentation │ │ │ │ ├── GoalCard.kt │ │ │ │ ├── GoalDialog.kt │ │ │ │ ├── GoalsScreen.kt │ │ │ │ ├── GoalsUiEvent.kt │ │ │ │ ├── GoalsUiState.kt │ │ │ │ ├── GoalsViewModel.kt │ │ │ │ └── PeriodInput.kt │ │ │ ├── library │ │ │ ├── data │ │ │ │ ├── LibraryRepository.kt │ │ │ │ ├── LibrarySorting.kt │ │ │ │ ├── daos │ │ │ │ │ ├── LibraryFolderDao.kt │ │ │ │ │ └── LibraryItemDao.kt │ │ │ │ └── entities │ │ │ │ │ ├── LibraryFolder.kt │ │ │ │ │ └── LibraryItem.kt │ │ │ ├── di │ │ │ │ ├── LibraryRepositoryModule.kt │ │ │ │ └── LibraryUseCasesModule.kt │ │ │ ├── domain │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── AddFolderUseCase.kt │ │ │ │ │ ├── AddItemUseCase.kt │ │ │ │ │ ├── DeleteFoldersUseCase.kt │ │ │ │ │ ├── DeleteItemsUseCase.kt │ │ │ │ │ ├── EditFolderUseCase.kt │ │ │ │ │ ├── EditItemUseCase.kt │ │ │ │ │ ├── GetAllLibraryItemsUseCase.kt │ │ │ │ │ ├── GetFolderSortInfoUseCase.kt │ │ │ │ │ ├── GetItemSortInfoUseCase.kt │ │ │ │ │ ├── GetLastPracticedDateUseCase.kt │ │ │ │ │ ├── GetSortedLibraryFoldersUseCase.kt │ │ │ │ │ ├── GetSortedLibraryItemsUseCase.kt │ │ │ │ │ ├── LibraryUseCases.kt │ │ │ │ │ ├── RestoreFoldersUseCase.kt │ │ │ │ │ ├── RestoreItemsUseCase.kt │ │ │ │ │ ├── SelectFolderSortModeUseCase.kt │ │ │ │ │ └── SelectItemSortModeUseCase.kt │ │ │ └── presentation │ │ │ │ ├── LibraryComponents.kt │ │ │ │ ├── LibraryCoreUiEvent.kt │ │ │ │ ├── LibraryCoreViewModel.kt │ │ │ │ ├── LibraryDialogs.kt │ │ │ │ ├── LibraryScreen.kt │ │ │ │ ├── LibraryUiEvent.kt │ │ │ │ ├── LibraryUiState.kt │ │ │ │ ├── LibraryViewModel.kt │ │ │ │ └── libraryfolder │ │ │ │ ├── LibraryFolderDetailsScreen.kt │ │ │ │ └── LibraryFolderDetailsViewModel.kt │ │ │ ├── menu │ │ │ ├── di │ │ │ │ └── SettingsUseCasesModule.kt │ │ │ ├── domain │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── GetColorSchemeUseCase.kt │ │ │ │ │ ├── GetThemeUseCase.kt │ │ │ │ │ ├── SelectColorSchemeUseCase.kt │ │ │ │ │ ├── SelectThemeUseCase.kt │ │ │ │ │ └── SettingsUseCases.kt │ │ │ └── presentation │ │ │ │ ├── about │ │ │ │ ├── AboutScreen.kt │ │ │ │ └── LicensesScreen.kt │ │ │ │ ├── donate │ │ │ │ └── DonateScreen.kt │ │ │ │ ├── help │ │ │ │ └── HelpScreen.kt │ │ │ │ └── settings │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── appearance │ │ │ │ ├── AppearanceScreen.kt │ │ │ │ └── AppearanceViewModel.kt │ │ │ │ ├── backup │ │ │ │ └── BackupScreen.kt │ │ │ │ └── export │ │ │ │ └── ExportScreen.kt │ │ │ ├── metronome │ │ │ ├── di │ │ │ │ └── MetronomeUseCasesModule.kt │ │ │ ├── domain │ │ │ │ └── usecase │ │ │ │ │ ├── ChangeMetronomeSettingsUseCase.kt │ │ │ │ │ ├── GetMetronomeSettingsUseCase.kt │ │ │ │ │ └── MetronomeUseCases.kt │ │ │ └── presentation │ │ │ │ ├── Metronome.kt │ │ │ │ ├── MetronomeService.kt │ │ │ │ ├── MetronomeUi.kt │ │ │ │ └── MetronomeViewModel.kt │ │ │ ├── permissions │ │ │ ├── data │ │ │ │ └── PermissionRepositoryImpl.kt │ │ │ ├── di │ │ │ │ └── PermissionsModule.kt │ │ │ ├── domain │ │ │ │ ├── PermissionChecker.kt │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── PermissionsUseCases.kt │ │ │ │ │ └── RequestPermissionsUseCase.kt │ │ │ └── presentation │ │ │ │ └── PermissionDialog.kt │ │ │ ├── recorder │ │ │ ├── data │ │ │ │ └── RecordingsRepositoryImpl.kt │ │ │ ├── di │ │ │ │ ├── RecordingsRepositoryModule.kt │ │ │ │ └── RecordingsUseCasesModule.kt │ │ │ ├── domain │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── GetRawRecordingUseCase.kt │ │ │ │ │ ├── GetRecordingsUseCase.kt │ │ │ │ │ └── RecordingsUseCases.kt │ │ │ └── presentation │ │ │ │ ├── MediaController.kt │ │ │ │ ├── PlayerState.kt │ │ │ │ ├── Recorder.kt │ │ │ │ ├── RecorderService.kt │ │ │ │ ├── RecorderUi.kt │ │ │ │ ├── RecorderUiState.kt │ │ │ │ ├── RecorderViewModel.kt │ │ │ │ └── RecordingPlaybackService.kt │ │ │ ├── sessions │ │ │ ├── data │ │ │ │ ├── SessionRepository.kt │ │ │ │ ├── daos │ │ │ │ │ ├── SectionDao.kt │ │ │ │ │ └── SessionDao.kt │ │ │ │ └── entities │ │ │ │ │ ├── Section.kt │ │ │ │ │ └── Session.kt │ │ │ ├── di │ │ │ │ ├── SessionRepositoryModule.kt │ │ │ │ └── SessionsUseCasesModule.kt │ │ │ ├── domain │ │ │ │ ├── Types.kt │ │ │ │ └── usecase │ │ │ │ │ ├── AddSessionUseCase.kt │ │ │ │ │ ├── DeleteSessionsUseCase.kt │ │ │ │ │ ├── EditSessionUseCase.kt │ │ │ │ │ ├── GetAllSessionsUseCase.kt │ │ │ │ │ ├── GetSessionByIdUseCase.kt │ │ │ │ │ ├── GetSessionsForDaysForMonthsUseCase.kt │ │ │ │ │ ├── GetSessionsInTimeframeUseCase.kt │ │ │ │ │ ├── RestoreSessionsUseCase.kt │ │ │ │ │ └── SessionsUseCases.kt │ │ │ └── presentation │ │ │ │ ├── EditSession.kt │ │ │ │ ├── EditSessionViewModel.kt │ │ │ │ ├── SessionCard.kt │ │ │ │ ├── SessionsScreen.kt │ │ │ │ ├── SessionsUiEvent.kt │ │ │ │ ├── SessionsUiState.kt │ │ │ │ └── SessionsViewModel.kt │ │ │ └── statistics │ │ │ └── presentation │ │ │ ├── StatisticsScreen.kt │ │ │ ├── StatisticsViewModel.kt │ │ │ ├── goalstatistics │ │ │ ├── GoalStatisticsBarChart.kt │ │ │ ├── GoalStatisticsScreen.kt │ │ │ └── GoalStatisticsViewModel.kt │ │ │ └── sessionstatistics │ │ │ ├── SessionStatisticsBarChart.kt │ │ │ ├── SessionStatisticsPieChart.kt │ │ │ ├── SessionStatisticsScreen.kt │ │ │ └── SessionStatisticsViewModel.kt │ └── res │ │ ├── anim │ │ ├── fake_anim.xml │ │ ├── slide_in_up.xml │ │ └── slide_out_down.xml │ │ ├── animator │ │ ├── flip_in.xml │ │ ├── flip_out.xml │ │ ├── pause_to_play.xml │ │ ├── play_to_pause.xml │ │ ├── play_to_stop.xml │ │ ├── rotate_in_180_degrees.xml │ │ ├── rotate_out_180_degrees.xml │ │ ├── start_to_stop.xml │ │ ├── stop_to_play.xml │ │ └── stop_to_start.xml │ │ ├── drawable-hdpi │ │ └── ic_notification.png │ │ ├── drawable-mdpi │ │ └── ic_notification.png │ │ ├── drawable-xhdpi │ │ └── ic_notification.png │ │ ├── drawable-xxhdpi │ │ └── ic_notification.png │ │ ├── drawable-xxxhdpi │ │ └── ic_notification.png │ │ ├── drawable │ │ ├── avd_bar_chart.xml │ │ ├── avd_goals.xml │ │ ├── avd_library.xml │ │ ├── avd_sessions.xml │ │ ├── ic_appearance.xml │ │ ├── ic_bar_chart.xml │ │ ├── ic_check_small_round.xml │ │ ├── ic_discord.xml │ │ ├── ic_export.xml │ │ ├── ic_github.xml │ │ ├── ic_goals.xml │ │ ├── ic_library.xml │ │ ├── ic_metronome.xml │ │ ├── ic_microphone.xml │ │ ├── ic_pause.xml │ │ ├── ic_play.xml │ │ ├── ic_record.xml │ │ ├── ic_sessions.xml │ │ ├── ic_stop.xml │ │ ├── musikus_logo_dark.png │ │ └── musikus_logo_light.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ ├── ic_launcher_foreground.webp │ │ ├── ic_launcher_monochrome.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ ├── ic_launcher_foreground.webp │ │ ├── ic_launcher_monochrome.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ ├── ic_launcher_foreground.webp │ │ ├── ic_launcher_monochrome.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ ├── ic_launcher_foreground.webp │ │ ├── ic_launcher_monochrome.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_background.webp │ │ ├── ic_launcher_foreground.webp │ │ ├── ic_launcher_monochrome.webp │ │ └── ic_launcher_round.webp │ │ ├── raw │ │ ├── beat_1.wav │ │ ├── beat_2.wav │ │ ├── beat_3.wav │ │ └── third_party_licenses │ │ └── values │ │ ├── activesession_strings.xml │ │ ├── custom_paths.xml │ │ ├── goals_strings.xml │ │ ├── library_strings.xml │ │ ├── menu_strings.xml │ │ ├── metronome_strings.xml │ │ ├── permissions_strings.xml │ │ ├── quotes.xml │ │ ├── recorder_strings.xml │ │ ├── sessions_strings.xml │ │ ├── statistics_strings.xml │ │ ├── strings.xml │ │ └── theme.xml │ └── test │ └── java │ └── app │ └── musikus │ ├── core │ ├── data │ │ └── FakeUserPreferencesRepository.kt │ └── domain │ │ ├── FakeIdProvider.kt │ │ ├── FakeTimeProvider.kt │ │ └── TimeProviderTest.kt │ ├── goals │ ├── data │ │ └── FakeGoalRepository.kt │ └── domain │ │ └── usecase │ │ ├── AddGoalUseCaseTest.kt │ │ ├── ArchiveGoalsUseCaseTest.kt │ │ ├── CalculateGoalProgressUseCaseTest.kt │ │ ├── CleanFutureGoalInstancesUseCaseTest.kt │ │ ├── EditGoalUseCaseTest.kt │ │ ├── GetCurrentGoalsUseCaseTest.kt │ │ ├── PauseGoalsUseCaseTest.kt │ │ ├── SelectGoalsSortModeUseCaseTest.kt │ │ ├── SortGoalsUseCaseTest.kt │ │ ├── UnarchiveGoalsUseCaseTest.kt │ │ ├── UnpauseGoalsUseCaseTest.kt │ │ └── UpdateGoalsUseCaseTest.kt │ ├── library │ ├── data │ │ └── FakeLibraryRepository.kt │ └── domain │ │ └── usecase │ │ ├── AddFolderUseCaseTest.kt │ │ ├── AddItemUseCaseTest.kt │ │ ├── EditFolderUseCaseTest.kt │ │ ├── EditItemUseCaseTest.kt │ │ ├── GetSortedLibraryFoldersUseCaseTest.kt │ │ ├── GetSortedLibraryItemsUseCaseTest.kt │ │ ├── SelectFolderSortModeUseCaseTest.kt │ │ └── SelectItemSortModeUseCaseTest.kt │ └── sessions │ ├── data │ └── FakeSessionRepository.kt │ └── domain │ └── usecase │ ├── AddSessionUseCaseTest.kt │ ├── EditSessionUseCaseTest.kt │ └── GetSessionsForDaysForMonthsUseCaseTest.kt ├── build.gradle.kts ├── build.properties ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── musikus_icon_background.png ├── musikus_icon_foreground.png ├── musikus_icon_foreground_beta.png └── musikus_icon_monochrome.png ├── renovate.json ├── settings.gradle.kts └── tools ├── check_license_headers.py ├── fix_license_headers.py ├── format_licenses.sh └── hooks ├── post-commit.hook ├── setup_hooks.bat └── setup_hooks.sh /.github/workflows/discord-webhook.yml: -------------------------------------------------------------------------------- 1 | name: DISCORD-WEBHOOK 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | notify-discord: 11 | name: Notify Discord 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Send Discord notification for PR 16 | if: github.event_name == 'pull_request' 17 | run: | 18 | BODY=$'${{ github.event.pull_request.body }}' 19 | response=$(curl -s -w "\n%{http_code}" -H "Content-Type: application/json" \ 20 | -X POST \ 21 | -d "$(jq -n --arg title "${{ github.event.pull_request.title }}" \ 22 | --arg url "${{ github.event.pull_request.html_url }}" \ 23 | --arg body "$(echo $BODY | sed 's/`/\\`/g')" \ 24 | --arg branch "${{ github.event.pull_request.head.ref }}" \ 25 | '{"content": "📣 **New Pull Request**: [\($title)](\($url))\n\n📝 **Description**: \($body)\n\n🚀 **Branch**: \($branch)"}')" \ 26 | ${{ secrets.DISCORD_WEBHOOK_URL }}) 27 | echo "$response" 28 | response_code=$(echo "$response" | tail -n1) 29 | if [ "$response_code" -ne 204 ]; then 30 | echo "Failed to send Discord notification" 31 | exit 1 32 | fi 33 | 34 | - name: Send Discord notification for Release 35 | if: github.event_name == 'release' 36 | run: | 37 | BODY=$'${{ github.event.release.body }}' 38 | response=$(curl -s -w "%{http_code}" -H "Content-Type: application/json" \ 39 | -X POST \ 40 | -d "$(jq -n --arg title "${{ github.event.release.title }}" \ 41 | --arg url "${{ github.event.release.html_url }}" \ 42 | --arg body "$(echo $BODY | sed 's/`/\\`/g')" \ 43 | --arg tag "${{ github.event.release.tag_name }}" \ 44 | '{"content": "🎉 **New Release**: [\($title)](\($url))\n\n📦 **Version**: \($tag)\n\n📝 **Description**: \($body)"}')" \ 45 | ${{ secrets.DISCORD_WEBHOOK_URL }}) 46 | echo "$response" 47 | response_code=$(echo "$response" | tail -n1) 48 | if [ "$response_code" -ne 204 ]; then 49 | echo "Failed to send Discord notification" 50 | exit 1 51 | fi 52 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: VALIDATE-PR-TITLE 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize, edited, reopened ] 6 | 7 | jobs: 8 | validate-pr-title: 9 | name: Validate PR title 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Validate PR title 16 | id: validate_title 17 | env: 18 | GH_TOKEN: ${{ secrets.RELEASE_BOT_PAT }} 19 | run: | 20 | PR_TITLE="${{ github.event.pull_request.title }}" 21 | echo "PR Title: $PR_TITLE" 22 | if [[ ! "$PR_TITLE" =~ ^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\([a-zA-Z0-9_\-]+\))?:\ [a-z].* ]]; then 23 | echo "PR title does not conform to the [conventional commit](https://www.conventionalcommits.org/) pattern." > title_error.txt 24 | gh pr comment ${{ github.event.pull_request.number }} --body-file title_error.txt 25 | exit 1 26 | fi 27 | 28 | - name: PR title is valid 29 | if: success() 30 | run: echo "PR title conforms to the conventional commit pattern" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | musikus.properties -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | ## All source files without a license header are subject to the MIT License 2 | 3 | Copyright (c) 2022 Javier Carbone, authors Matthias Emde and Michael Prommersberger\ 4 | Copyright (c) 2022 Matthias Emde\ 5 | Copyright (c) 2022 Michael Prommersberger 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ## Additional Licenses used in this project: 26 | 27 | [Mozilla Public License Version 2.0](MPL2.0.txt) 28 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | Musikus is an Open Source app. This service is provided at no cost and is intended for use as is. 3 | 4 | ## Information Collection and Use 5 | Musikus does not collect any personal data, neither does the app use any third-party services that collect personal data. All of your data is stored locally on your device and is not shared with anyone. 6 | 7 | ## Updates 8 | We will update this privacy policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our Services confirms your acceptance of our updated Privacy Policy 9 | 10 | ## Contact Us 11 | If you have questions or suggestions about our Privacy Policy please contact us at privacy@musikus.app. 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/config/androidLint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app/config/detekt.yml: -------------------------------------------------------------------------------- 1 | complexity: 2 | LongParameterList: 3 | functionThreshold: 5 4 | ignoreDefaultParameters: true 5 | TooManyFunctions: 6 | ignoreAnnotatedFunctions: ['Preview', 'PreviewLightDark'] 7 | 8 | naming: 9 | FunctionNaming: 10 | functionPattern: '[a-zA-Z][a-zA-Z0-9]*' 11 | TopLevelPropertyNaming: 12 | constantPattern: '[A-Z][A-Za-z0-9]*' 13 | 14 | style: 15 | UnusedPrivateMember: 16 | ignoreAnnotated: ['Preview', 'PreviewLightDark'] 17 | MaxLineLength: 18 | maxLineLength: 140 19 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/app/ScreenshotRule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app 10 | 11 | import android.graphics.Bitmap 12 | import androidx.compose.ui.graphics.asAndroidBitmap 13 | import androidx.compose.ui.test.captureToImage 14 | import androidx.compose.ui.test.junit4.ComposeTestRule 15 | import androidx.compose.ui.test.onRoot 16 | import androidx.test.platform.app.InstrumentationRegistry 17 | import org.junit.rules.TestWatcher 18 | import org.junit.runner.Description 19 | import java.io.File 20 | 21 | // Store screenshots in "/sdcard/Android/media/app.musikus/additional_test_output" 22 | // This way, the files will get automatically synced to app/build/outputs/managed_device_android_test_additional_output 23 | // before the emulator gets shut down. 24 | // Source: https://stackoverflow.com/questions/74069309/copy-data-from-an-android-emulator-that-is-run-by-gradle-managed-devices 25 | 26 | class ScreenshotRule( 27 | private val composeTestRule: ComposeTestRule, 28 | ) : TestWatcher() { 29 | 30 | var outputDir: File 31 | 32 | init { 33 | @Suppress("Deprecation") 34 | outputDir = File( 35 | InstrumentationRegistry.getInstrumentation().targetContext.externalMediaDirs.first(), 36 | "additional_test_output" 37 | ) 38 | 39 | // Ensure the directory exists 40 | if (!outputDir.exists()) { 41 | outputDir.mkdirs() 42 | } 43 | } 44 | 45 | override fun failed(e: Throwable?, description: Description) { 46 | val testClassDir = File( 47 | outputDir, 48 | description.className 49 | ) 50 | 51 | if (!testClassDir.exists()) { 52 | testClassDir.mkdirs() 53 | } 54 | 55 | val screenshotName = "${description.methodName}.png" 56 | val screenshotFile = File(testClassDir, screenshotName) 57 | 58 | // Capture the screenshot and save it 59 | composeTestRule.onRoot().captureToImage().asAndroidBitmap().apply { 60 | screenshotFile.outputStream().use { outputStream -> 61 | compress(Bitmap.CompressFormat.PNG, 100, outputStream) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/HiltTestRunner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus 10 | 11 | import android.app.Application 12 | import android.content.Context 13 | import androidx.test.runner.AndroidJUnitRunner 14 | import dagger.hilt.android.testing.HiltTestApplication 15 | 16 | class HiltTestRunner : AndroidJUnitRunner() { 17 | 18 | override fun newApplication( 19 | cl: ClassLoader?, 20 | className: String?, 21 | context: Context? 22 | ): Application { 23 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/activesession/di/TestActiveSessionRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.activesession.di 10 | 11 | import app.musikus.activesession.data.ActiveSessionRepositoryImpl 12 | import app.musikus.activesession.domain.ActiveSessionRepository 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.components.SingletonComponent 16 | import dagger.hilt.testing.TestInstallIn 17 | 18 | @Module 19 | @TestInstallIn( 20 | components = [SingletonComponent::class], 21 | replaces = [ActiveSessionRepositoryModule::class] 22 | ) 23 | object TestActiveSessionRepositoryModule { 24 | 25 | @Provides 26 | fun provideActiveSessionRepository(): ActiveSessionRepository { 27 | return ActiveSessionRepositoryImpl() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/activesession/presentation/SessionServiceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.presentation 10 | 11 | import android.content.Context 12 | import android.content.Intent 13 | import androidx.test.core.app.ApplicationProvider 14 | import androidx.test.rule.ServiceTestRule 15 | import com.google.common.truth.Truth.assertThat 16 | import dagger.hilt.android.testing.HiltAndroidRule 17 | import dagger.hilt.android.testing.HiltAndroidTest 18 | import org.junit.Before 19 | import org.junit.Rule 20 | import org.junit.Test 21 | import java.util.concurrent.TimeoutException 22 | 23 | @HiltAndroidTest 24 | class SessionServiceTest { 25 | 26 | @get:Rule(order = 0) 27 | val hiltRule = HiltAndroidRule(this) 28 | 29 | @get:Rule(order = 1) 30 | val serviceRule = ServiceTestRule() 31 | 32 | @Before 33 | fun setUp() { 34 | hiltRule.inject() 35 | } 36 | 37 | @Test 38 | @Throws(TimeoutException::class) 39 | fun testWithBoundService() { 40 | // Create the start intent 41 | val startIntent = Intent( 42 | ApplicationProvider.getApplicationContext(), 43 | SessionService::class.java 44 | ).apply { 45 | action = ActiveSessionServiceActions.START.name 46 | } 47 | 48 | val binder = serviceRule.bindService(startIntent) 49 | 50 | // Verify that the service has started correctly 51 | assertThat(binder.pingBinder()).isTrue() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/core/di/TestCoroutinesDispatcherModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | @file:Suppress("DEPRECATION") 10 | 11 | package app.musikus.core.di 12 | 13 | import android.os.AsyncTask 14 | import dagger.Module 15 | import dagger.Provides 16 | import dagger.hilt.components.SingletonComponent 17 | import dagger.hilt.testing.TestInstallIn 18 | import kotlinx.coroutines.CoroutineDispatcher 19 | import kotlinx.coroutines.asCoroutineDispatcher 20 | 21 | @Module 22 | @TestInstallIn( 23 | components = [SingletonComponent::class], 24 | replaces = [CoroutinesDispatchersModule::class] 25 | ) 26 | object TestCoroutinesDispatcherModule { 27 | 28 | @DefaultDispatcher 29 | @Provides 30 | fun providesDefaultDispatcher(): CoroutineDispatcher = 31 | AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher() 32 | 33 | @IoDispatcher 34 | @Provides 35 | fun provideIoDispatcher(): CoroutineDispatcher { 36 | return AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/core/di/TestMainModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import android.app.Application 12 | import androidx.room.Room 13 | import app.musikus.core.data.MusikusDatabase 14 | import app.musikus.core.domain.FakeIdProvider 15 | import app.musikus.core.domain.FakeTimeProvider 16 | import app.musikus.core.domain.IdProvider 17 | import app.musikus.core.domain.TimeProvider 18 | import dagger.Module 19 | import dagger.Provides 20 | import dagger.hilt.components.SingletonComponent 21 | import dagger.hilt.testing.TestInstallIn 22 | import javax.inject.Named 23 | import javax.inject.Singleton 24 | 25 | @Module 26 | @TestInstallIn( 27 | components = [SingletonComponent::class], 28 | replaces = [MainModule::class] 29 | ) 30 | object TestMainModule { 31 | 32 | @Provides 33 | @Singleton 34 | fun provideTimeProvider(): TimeProvider { 35 | return FakeTimeProvider() 36 | } 37 | 38 | @Provides 39 | fun provideFakeTimeProvider( 40 | timeProvider: TimeProvider 41 | ): FakeTimeProvider { 42 | return timeProvider as FakeTimeProvider 43 | } 44 | 45 | @Provides 46 | @Singleton 47 | fun provideIdProvider(): IdProvider { 48 | return FakeIdProvider() 49 | } 50 | 51 | @Provides 52 | @Singleton 53 | @Named("test_db") 54 | fun provideMusikusDatabase( 55 | app: Application, 56 | timeProvider: TimeProvider, 57 | idProvider: IdProvider 58 | ): MusikusDatabase { 59 | return Room.inMemoryDatabaseBuilder( 60 | app, 61 | MusikusDatabase::class.java 62 | ).build().apply { 63 | this.timeProvider = timeProvider 64 | this.idProvider = idProvider 65 | } 66 | } 67 | 68 | @Provides 69 | fun providesMusikusDatabase( 70 | @Named("test_db") database: MusikusDatabase 71 | ): MusikusDatabase { 72 | return database 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/core/di/TestUserPreferencesRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.repository.FakeUserPreferencesRepository 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.components.SingletonComponent 16 | import dagger.hilt.testing.TestInstallIn 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @TestInstallIn( 21 | components = [SingletonComponent::class], 22 | replaces = [UserPreferencesRepositoryModule::class] 23 | ) 24 | object TestUserPreferencesRepositoryModule { 25 | 26 | @Provides 27 | @Singleton 28 | fun provideUserPreferencesRepository(): UserPreferencesRepository { 29 | return FakeUserPreferencesRepository() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/core/domain/FakeIdProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain 10 | 11 | import java.util.UUID 12 | 13 | class FakeIdProvider : IdProvider { 14 | private var _currentId = 1L 15 | 16 | override fun generateId(): UUID { 17 | return UUID.fromString( 18 | "00000000-0000-0000-0000-${_currentId++.toString().padStart(12, '0')}" 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/core/domain/FakeTimeProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain 10 | 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.asStateFlow 14 | import kotlinx.coroutines.flow.update 15 | import java.time.ZonedDateTime 16 | import kotlin.time.Duration 17 | import kotlin.time.toJavaDuration 18 | 19 | class FakeTimeProvider : TimeProvider { 20 | private val _clock = MutableStateFlow(START_TIME) 21 | override val clock: Flow get() = _clock.asStateFlow() 22 | 23 | override fun now(): ZonedDateTime { 24 | return _clock.value 25 | } 26 | 27 | fun setCurrentDateTime(dateTime: ZonedDateTime) { 28 | _clock.update { dateTime } 29 | } 30 | 31 | fun advanceTimeBy(duration: Duration) { 32 | _clock.update { it.plus(duration.toJavaDuration()) } 33 | } 34 | 35 | fun revertTimeBy(duration: Duration) { 36 | _clock.update { it.minus(duration.toJavaDuration()) } 37 | } 38 | 39 | companion object { 40 | val START_TIME: ZonedDateTime = ZonedDateTime.parse("1969-07-20T20:17:40Z[UTC]") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/goals/di/TestGoalRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.goals.di 10 | 11 | import app.musikus.core.data.MusikusDatabase 12 | import app.musikus.goals.data.GoalRepositoryImpl 13 | import app.musikus.goals.domain.GoalRepository 14 | import dagger.Module 15 | import dagger.Provides 16 | import dagger.hilt.components.SingletonComponent 17 | import dagger.hilt.testing.TestInstallIn 18 | import javax.inject.Named 19 | 20 | @Module 21 | @TestInstallIn( 22 | components = [SingletonComponent::class], 23 | replaces = [GoalRepositoryModule::class] 24 | ) 25 | object TestGoalRepositoryModule { 26 | @Provides 27 | fun provideGoalRepository( 28 | @Named("test_db") database: MusikusDatabase 29 | ): GoalRepository { 30 | return GoalRepositoryImpl(database) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/library/di/TestLibraryRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.library.di 10 | 11 | import app.musikus.core.data.MusikusDatabase 12 | import app.musikus.library.data.LibraryRepositoryImpl 13 | import app.musikus.library.domain.LibraryRepository 14 | import dagger.Module 15 | import dagger.Provides 16 | import dagger.hilt.components.SingletonComponent 17 | import dagger.hilt.testing.TestInstallIn 18 | import javax.inject.Named 19 | 20 | @Module 21 | @TestInstallIn( 22 | components = [SingletonComponent::class], 23 | replaces = [LibraryRepositoryModule::class] 24 | ) 25 | object TestLibraryRepositoryModule { 26 | 27 | @Provides 28 | fun provideLibraryRepository( 29 | @Named("test_db") database: MusikusDatabase 30 | ): LibraryRepository { 31 | return LibraryRepositoryImpl( 32 | itemDao = database.libraryItemDao, 33 | folderDao = database.libraryFolderDao, 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/permissions/data/TestPermissionRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.data 10 | 11 | import app.musikus.permissions.domain.PermissionRepository 12 | 13 | class TestPermissionRepository : PermissionRepository { 14 | override suspend fun requestPermissions(permissions: List): Result { 15 | return Result.success(Unit) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/permissions/di/TestPermissionsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.di 10 | 11 | import android.app.Application 12 | import app.musikus.core.di.ApplicationScope 13 | import app.musikus.permissions.data.TestPermissionRepository 14 | import app.musikus.permissions.domain.PermissionChecker 15 | import app.musikus.permissions.domain.PermissionRepository 16 | import app.musikus.permissions.domain.usecase.PermissionsUseCases 17 | import app.musikus.permissions.domain.usecase.RequestPermissionsUseCase 18 | import dagger.Module 19 | import dagger.Provides 20 | import dagger.hilt.components.SingletonComponent 21 | import dagger.hilt.testing.TestInstallIn 22 | import kotlinx.coroutines.CoroutineScope 23 | import javax.inject.Singleton 24 | 25 | @Module 26 | @TestInstallIn( 27 | components = [SingletonComponent::class], 28 | replaces = [PermissionsModule::class] 29 | ) 30 | object TestPermissionsModule { 31 | 32 | @Provides 33 | @Singleton 34 | fun providePermissionChecker( 35 | application: Application, 36 | @ApplicationScope applicationScope: CoroutineScope 37 | ): PermissionChecker { 38 | return PermissionChecker( 39 | context = application, 40 | applicationScope = applicationScope 41 | ) 42 | } 43 | 44 | @Provides 45 | @Singleton 46 | fun providePermissionRepository(): PermissionRepository { 47 | return TestPermissionRepository() 48 | } 49 | 50 | @Provides 51 | @Singleton 52 | fun providePermissionsUseCases( 53 | permissionRepository: PermissionRepository 54 | ): PermissionsUseCases { 55 | return PermissionsUseCases( 56 | request = RequestPermissionsUseCase(permissionRepository) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/recorder/di/TestRecordingsRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.recorder.di 10 | 11 | import android.app.Application 12 | import app.musikus.core.di.IoScope 13 | import app.musikus.recorder.data.RecordingsRepositoryImpl 14 | import app.musikus.recorder.domain.RecordingsRepository 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.components.SingletonComponent 18 | import dagger.hilt.testing.TestInstallIn 19 | import kotlinx.coroutines.CoroutineScope 20 | 21 | @Module 22 | @TestInstallIn( 23 | components = [SingletonComponent::class], 24 | replaces = [RecordingsRepositoryModule::class] 25 | ) 26 | object TestRecordingsRepositoryModule { 27 | 28 | @Provides 29 | fun provideRecordingsRepository( 30 | application: Application, 31 | @IoScope ioScope: CoroutineScope 32 | ): RecordingsRepository { 33 | return RecordingsRepositoryImpl( 34 | application = application, 35 | contentResolver = application.contentResolver, 36 | ioScope = ioScope 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/androidTest/java/app/musikus/sessionslist/di/TestSessionRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.sessionslist.di 10 | 11 | import androidx.room.withTransaction 12 | import app.musikus.core.data.MusikusDatabase 13 | import app.musikus.core.domain.TimeProvider 14 | import app.musikus.sessions.data.SessionRepositoryImpl 15 | import app.musikus.sessions.di.SessionRepositoryModule 16 | import app.musikus.sessions.domain.SessionRepository 17 | import dagger.Module 18 | import dagger.Provides 19 | import dagger.hilt.components.SingletonComponent 20 | import dagger.hilt.testing.TestInstallIn 21 | import javax.inject.Named 22 | 23 | @Module 24 | @TestInstallIn( 25 | components = [SingletonComponent::class], 26 | replaces = [SessionRepositoryModule::class] 27 | ) 28 | object TestSessionRepositoryModule { 29 | 30 | @Provides 31 | fun provideSessionRepository( 32 | @Named("test_db") database: MusikusDatabase, 33 | timeProvider: TimeProvider 34 | ): SessionRepository { 35 | return SessionRepositoryImpl( 36 | timeProvider = timeProvider, 37 | sessionDao = database.sessionDao, 38 | sectionDao = database.sectionDao, 39 | withDatabaseTransaction = { block -> database.withTransaction(block) } 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/data/ActiveSessionRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.data 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import app.musikus.activesession.domain.SessionState 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.flow.update 16 | 17 | class ActiveSessionRepositoryImpl : ActiveSessionRepository { 18 | 19 | private val sessionState = MutableStateFlow(null) 20 | 21 | override suspend fun setSessionState(sessionState: SessionState) { 22 | this.sessionState.update { sessionState } 23 | } 24 | override fun getSessionState() = sessionState.asStateFlow() 25 | 26 | override fun reset() { 27 | sessionState.update { null } 28 | } 29 | 30 | override fun isRunning(): Boolean { 31 | return sessionState.value != null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/di/ActiveSessionRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.activesession.di 10 | 11 | import app.musikus.activesession.data.ActiveSessionRepositoryImpl 12 | import app.musikus.activesession.domain.ActiveSessionRepository 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object ActiveSessionRepositoryModule { 22 | 23 | @Provides 24 | @Singleton 25 | fun provideActiveSessionRepository(): ActiveSessionRepository { 26 | return ActiveSessionRepositoryImpl() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/Types.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain 10 | 11 | import app.musikus.library.data.daos.LibraryItem 12 | import kotlinx.coroutines.flow.Flow 13 | import java.time.ZonedDateTime 14 | import java.util.UUID 15 | import kotlin.time.Duration 16 | 17 | interface ActiveSessionRepository { 18 | suspend fun setSessionState( 19 | sessionState: SessionState 20 | ) 21 | fun getSessionState(): Flow 22 | 23 | fun reset() 24 | 25 | fun isRunning(): Boolean 26 | } 27 | 28 | data class PracticeSection( 29 | val id: UUID, 30 | val libraryItem: LibraryItem, 31 | val pauseDuration: Duration, // set when section is completed 32 | val duration: Duration, // set when section is completed 33 | val startTimestamp: ZonedDateTime 34 | ) 35 | 36 | data class SessionState( 37 | val completedSections: List, 38 | val currentSectionItem: LibraryItem, 39 | val startTimestamp: ZonedDateTime, 40 | val startTimestampSection: ZonedDateTime, 41 | val startTimestampSectionPauseCompensated: ZonedDateTime, 42 | val currentPauseStartTimestamp: ZonedDateTime?, 43 | val isPaused: Boolean, 44 | ) 45 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/ActiveSessionUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | data class ActiveSessionUseCases( 12 | val getState: GetActiveSessionStateUseCase, 13 | val selectItem: SelectItemUseCase, 14 | val deleteSection: DeleteSectionUseCase, 15 | val pause: PauseActiveSessionUseCase, 16 | val resume: ResumeActiveSessionUseCase, 17 | val computeTotalPracticeDuration: ComputeTotalPracticeDurationUseCase, 18 | val computeRunningItemDuration: ComputeRunningItemDurationUseCase, 19 | val getRunningItem: GetRunningItemUseCase, 20 | val getCompletedSections: GetCompletedSectionsUseCase, 21 | val computeOngoingPauseDuration: ComputeOngoingPauseDurationUseCase, 22 | val getStartTime: GetStartTimeUseCase, 23 | val getFinalizedSession: GetFinalizedSessionUseCase, 24 | val reset: ResetSessionUseCase, 25 | val isSessionPaused: IsSessionPausedUseCase, 26 | val isSessionRunning: IsSessionRunningUseCase, 27 | val getSessionStatus: GetSessionStatusUseCase, 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/ComputeOngoingPauseDurationUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.SessionState 12 | import app.musikus.core.domain.minus 13 | import java.time.ZonedDateTime 14 | import kotlin.time.Duration 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | class ComputeOngoingPauseDurationUseCase { 18 | operator fun invoke( 19 | state: SessionState, 20 | at: ZonedDateTime 21 | ): Duration { 22 | if (state.currentPauseStartTimestamp == null) return 0.seconds 23 | val duration = at - state.currentPauseStartTimestamp 24 | if (duration < 0.seconds) { 25 | throw IllegalStateException("Duration is negative. This should not happen.") 26 | } 27 | return duration 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/ComputeRunningItemDurationUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.SessionState 12 | import app.musikus.core.domain.minus 13 | import java.time.ZonedDateTime 14 | import kotlin.time.Duration 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | class ComputeRunningItemDurationUseCase { 18 | operator fun invoke( 19 | state: SessionState, 20 | at: ZonedDateTime 21 | ): Duration { 22 | val duration = if (state.isPaused) { 23 | if (state.currentPauseStartTimestamp == null) { 24 | throw IllegalStateException("CurrentPauseTimestamp is null although isPaused is true.") 25 | } 26 | state.currentPauseStartTimestamp - state.startTimestampSectionPauseCompensated 27 | } else { 28 | at - state.startTimestampSectionPauseCompensated 29 | } 30 | if (duration < 0.seconds) { 31 | throw IllegalStateException("Duration is negative. This should not happen.") 32 | } 33 | return duration 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/ComputeTotalPracticeDurationUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.SessionState 12 | import java.time.ZonedDateTime 13 | import kotlin.time.Duration 14 | 15 | class ComputeTotalPracticeDurationUseCase( 16 | private val computeRunningItemDuration: ComputeRunningItemDurationUseCase 17 | ) { 18 | operator fun invoke( 19 | state: SessionState, 20 | at: ZonedDateTime 21 | ): Duration { 22 | val runningItemDuration = computeRunningItemDuration(state, at) 23 | 24 | // add up all completed section durations 25 | // add running section duration on top (by using initial value of fold) 26 | return state.completedSections.fold(runningItemDuration) { acc, section -> 27 | acc + section.duration 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/DeleteSectionUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import kotlinx.coroutines.flow.first 13 | import java.util.UUID 14 | 15 | class DeleteSectionUseCase( 16 | private val activeSessionRepository: ActiveSessionRepository 17 | ) { 18 | suspend operator fun invoke(sectionId: UUID) { 19 | val state = activeSessionRepository.getSessionState().first() 20 | ?: throw IllegalStateException("Sections cannot be deleted when state is null") 21 | 22 | activeSessionRepository.setSessionState( 23 | state.copy( 24 | completedSections = state.completedSections.filter { it.id != sectionId } 25 | ) 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/GetActiveSessionStateUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | 13 | class GetActiveSessionStateUseCase( 14 | private val activeSessionRepository: ActiveSessionRepository 15 | ) { 16 | operator fun invoke() = activeSessionRepository.getSessionState() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/GetCompletedSectionsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import kotlinx.coroutines.flow.map 13 | 14 | class GetCompletedSectionsUseCase( 15 | private val activeSessionRepository: ActiveSessionRepository 16 | ) { 17 | operator fun invoke() = activeSessionRepository.getSessionState().map { 18 | it?.completedSections ?: emptyList() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/GetRunningItemUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import app.musikus.library.data.daos.LibraryItem 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.map 15 | 16 | class GetRunningItemUseCase( 17 | private val activeSessionRepository: ActiveSessionRepository, 18 | ) { 19 | operator fun invoke(): Flow { 20 | return activeSessionRepository.getSessionState().map { 21 | it?.currentSectionItem 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/GetSessionStatusUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.map 14 | 15 | enum class SessionStatus { 16 | NOT_STARTED, 17 | RUNNING, 18 | PAUSED 19 | } 20 | 21 | class GetSessionStatusUseCase( 22 | private val activeSessionRepository: ActiveSessionRepository, 23 | ) { 24 | operator fun invoke(): Flow { 25 | return activeSessionRepository.getSessionState().map { state -> 26 | if (state == null) { 27 | return@map SessionStatus.NOT_STARTED 28 | } 29 | if (state.isPaused) { 30 | return@map SessionStatus.PAUSED 31 | } 32 | 33 | SessionStatus.RUNNING 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/GetStartTimeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import kotlinx.coroutines.flow.first 13 | import java.time.ZonedDateTime 14 | 15 | class GetStartTimeUseCase( 16 | private val activeSessionRepository: ActiveSessionRepository 17 | ) { 18 | suspend operator fun invoke(): ZonedDateTime { 19 | val state = activeSessionRepository.getSessionState().first() 20 | ?: throw IllegalStateException("State is null. Cannot get start time!") 21 | 22 | return state.startTimestamp 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/IsSessionPausedUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import kotlinx.coroutines.flow.first 13 | 14 | class IsSessionPausedUseCase( 15 | private val activeSessionRepository: ActiveSessionRepository 16 | ) { 17 | suspend operator fun invoke(): Boolean { 18 | val state = activeSessionRepository.getSessionState().first() 19 | ?: throw IllegalStateException("Cannot get paused state when state is null") 20 | return state.isPaused 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/IsSessionRunningUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | 13 | class IsSessionRunningUseCase( 14 | private val activeSessionRepository: ActiveSessionRepository 15 | ) { 16 | operator fun invoke(): Boolean { 17 | return activeSessionRepository.isRunning() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/PauseActiveSessionUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import kotlinx.coroutines.flow.first 13 | import java.time.ZonedDateTime 14 | 15 | class PauseActiveSessionUseCase( 16 | private val activeSessionRepository: ActiveSessionRepository, 17 | ) { 18 | suspend operator fun invoke(at: ZonedDateTime) { 19 | val state = activeSessionRepository.getSessionState().first() 20 | ?: throw IllegalStateException("Cannot pause when state is null") 21 | 22 | if (state.isPaused) { 23 | throw IllegalStateException("Cannot pause when already paused.") 24 | } 25 | 26 | activeSessionRepository.setSessionState( 27 | state.copy( 28 | currentPauseStartTimestamp = at, 29 | isPaused = true 30 | ) 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/ResetSessionUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | 13 | class ResetSessionUseCase( 14 | private val activeSessionRepository: ActiveSessionRepository 15 | ) { 16 | operator fun invoke() { 17 | activeSessionRepository.reset() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/activesession/domain/usecase/ResumeActiveSessionUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.activesession.domain.usecase 10 | 11 | import app.musikus.activesession.domain.ActiveSessionRepository 12 | import app.musikus.core.domain.plus 13 | import kotlinx.coroutines.flow.first 14 | import java.time.ZonedDateTime 15 | 16 | class ResumeActiveSessionUseCase( 17 | private val activeSessionRepository: ActiveSessionRepository, 18 | private val computeOngoingPauseDurationUseCase: ComputeOngoingPauseDurationUseCase, 19 | ) { 20 | suspend operator fun invoke(at: ZonedDateTime) { 21 | val state = activeSessionRepository.getSessionState().first() 22 | ?: throw IllegalStateException("Cannot resume when state is null") 23 | 24 | if (!state.isPaused) { 25 | throw IllegalStateException("Cannot resume when not paused") 26 | } 27 | 28 | val currentPauseDuration = computeOngoingPauseDurationUseCase(state, at) 29 | activeSessionRepository.setSessionState( 30 | state.copy( 31 | startTimestampSectionPauseCompensated = 32 | state.startTimestampSectionPauseCompensated + currentPauseDuration, 33 | currentPauseStartTimestamp = null, 34 | isPaused = false 35 | ) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/data/Enums.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.data 10 | 11 | import app.musikus.core.presentation.utils.UiIcon 12 | import app.musikus.core.presentation.utils.UiText 13 | 14 | interface EnumWithLabel { 15 | val label: UiText 16 | } 17 | 18 | interface EnumWithIcon { 19 | val icon: UiIcon 20 | } 21 | 22 | interface EnumWithDescription { 23 | val description: UiText 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/di/CoreUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.core.domain.usecase.ConfirmAnnouncementMessageUseCase 13 | import app.musikus.core.domain.usecase.CoreUseCases 14 | import app.musikus.core.domain.usecase.GetIdOfLastSeenAnnouncementSeenUseCase 15 | import app.musikus.core.domain.usecase.ResetAnnouncementMessageUseCase 16 | import dagger.Module 17 | import dagger.Provides 18 | import dagger.hilt.InstallIn 19 | import dagger.hilt.components.SingletonComponent 20 | import javax.inject.Singleton 21 | 22 | @Module 23 | @InstallIn(SingletonComponent::class) 24 | object CoreUseCasesModule { 25 | 26 | @Provides 27 | @Singleton 28 | fun provideCoreUseCases( 29 | userPreferencesRepository: UserPreferencesRepository, 30 | ): CoreUseCases { 31 | return CoreUseCases( 32 | getIdOfLastSeenAnnouncementSeen = GetIdOfLastSeenAnnouncementSeenUseCase( 33 | userPreferencesRepository 34 | ), 35 | confirmAnnouncementMessage = ConfirmAnnouncementMessageUseCase( 36 | userPreferencesRepository 37 | ), 38 | resetAnnouncementMessage = ResetAnnouncementMessageUseCase( 39 | userPreferencesRepository 40 | ) 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/di/CoroutineQualifiers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import javax.inject.Qualifier 12 | 13 | // Source: https://medium.com/androiddevelopers/create-an-application-coroutinescope-using-hilt-dd444e721528 14 | 15 | @Retention(AnnotationRetention.RUNTIME) 16 | @Qualifier 17 | annotation class DefaultDispatcher 18 | 19 | @Retention(AnnotationRetention.RUNTIME) 20 | @Qualifier 21 | annotation class IoDispatcher 22 | 23 | @Retention(AnnotationRetention.RUNTIME) 24 | @Qualifier 25 | annotation class MainDispatcher 26 | 27 | @Retention(AnnotationRetention.BINARY) 28 | @Qualifier 29 | annotation class MainImmediateDispatcher 30 | 31 | @Retention(AnnotationRetention.RUNTIME) 32 | @Qualifier 33 | annotation class ApplicationScope 34 | 35 | @Retention(AnnotationRetention.RUNTIME) 36 | @Qualifier 37 | annotation class IoScope 38 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/di/CoroutineScopesModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.components.SingletonComponent 15 | import kotlinx.coroutines.CoroutineDispatcher 16 | import kotlinx.coroutines.CoroutineScope 17 | import kotlinx.coroutines.SupervisorJob 18 | import javax.inject.Singleton 19 | 20 | // Source: https://medium.com/androiddevelopers/create-an-application-coroutinescope-using-hilt-dd444e721528 21 | 22 | @InstallIn(SingletonComponent::class) 23 | @Module 24 | object CoroutineScopesModule { 25 | 26 | @Singleton 27 | @ApplicationScope 28 | @Provides 29 | fun providesApplicationScope( 30 | @DefaultDispatcher defaultDispatcher: CoroutineDispatcher 31 | ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher) 32 | 33 | @Singleton 34 | @IoScope 35 | @Provides 36 | fun providesIoScope( 37 | @IoDispatcher ioDispatcher: CoroutineDispatcher 38 | ): CoroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher) 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/di/CoroutinesDispatchersModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.components.SingletonComponent 15 | import kotlinx.coroutines.CoroutineDispatcher 16 | import kotlinx.coroutines.Dispatchers 17 | 18 | // Source: https://medium.com/androiddevelopers/create-an-application-coroutinescope-using-hilt-dd444e721528 19 | 20 | @InstallIn(SingletonComponent::class) 21 | @Module 22 | object CoroutinesDispatchersModule { 23 | 24 | @DefaultDispatcher 25 | @Provides 26 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 27 | 28 | @IoDispatcher 29 | @Provides 30 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 31 | 32 | @MainDispatcher 33 | @Provides 34 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main 35 | 36 | @MainImmediateDispatcher 37 | @Provides 38 | fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/di/NotificationModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024-2025 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import android.content.Context 12 | import app.musikus.core.presentation.MusikusNotificationManager 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.android.qualifiers.ApplicationContext 17 | import dagger.hilt.components.SingletonComponent 18 | import javax.inject.Singleton 19 | 20 | @Module 21 | @InstallIn(SingletonComponent::class) 22 | object NotificationModule { 23 | 24 | @Provides 25 | @Singleton 26 | fun provideMusikusNotificationManager( 27 | @ApplicationContext context: Context 28 | ): MusikusNotificationManager { 29 | return MusikusNotificationManager(context) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/di/UserPreferencesRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.core.di 10 | 11 | import androidx.datastore.core.DataStore 12 | import androidx.datastore.preferences.core.Preferences 13 | import app.musikus.core.data.UserPreferencesRepositoryImpl 14 | import app.musikus.core.domain.UserPreferencesRepository 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | object UserPreferencesRepositoryModule { 24 | 25 | @Provides 26 | @Singleton 27 | fun provideUserPreferencesRepository( 28 | dataStore: DataStore 29 | ): UserPreferencesRepository { 30 | return UserPreferencesRepositoryImpl(dataStore) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/domain/IdProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain 10 | 11 | import java.util.UUID 12 | 13 | interface IdProvider { 14 | fun generateId(): UUID 15 | } 16 | 17 | class IdProviderImpl : IdProvider { 18 | override fun generateId(): UUID { 19 | return UUID.randomUUID() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/domain/Sorting.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain 10 | 11 | import app.musikus.core.presentation.utils.UiText 12 | 13 | data class SortInfo( 14 | val mode: SortMode, 15 | val direction: SortDirection 16 | ) 17 | 18 | enum class SortDirection { 19 | ASCENDING, 20 | DESCENDING; 21 | 22 | fun invert() = when (this) { 23 | ASCENDING -> DESCENDING 24 | DESCENDING -> ASCENDING 25 | } 26 | 27 | companion object { 28 | val DEFAULT = DESCENDING 29 | 30 | fun valueOrDefault(string: String?) = try { 31 | valueOf(string ?: "") 32 | } catch (e: Exception) { 33 | DEFAULT 34 | } 35 | } 36 | } 37 | 38 | interface SortMode { 39 | val label: UiText 40 | val comparator: Comparator 41 | val name: String 42 | 43 | val isDefault: Boolean 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/domain/usecase/ConfirmAnnouncementMessageUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.core.presentation.CURRENT_ANNOUNCEMENT_ID 13 | 14 | class ConfirmAnnouncementMessageUseCase( 15 | private val userPreferencesRepository: UserPreferencesRepository 16 | ) { 17 | 18 | suspend operator fun invoke() { 19 | userPreferencesRepository.updateIdOfLastAnnouncementSeen(CURRENT_ANNOUNCEMENT_ID) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/domain/usecase/CoreUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain.usecase 10 | 11 | data class CoreUseCases( 12 | val getIdOfLastSeenAnnouncementSeen: GetIdOfLastSeenAnnouncementSeenUseCase, 13 | val confirmAnnouncementMessage: ConfirmAnnouncementMessageUseCase, 14 | val resetAnnouncementMessage: ResetAnnouncementMessageUseCase 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/domain/usecase/GetIdOfLastSeenAnnouncementSeenUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetIdOfLastSeenAnnouncementSeenUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.idOfLastAnnouncementSeen 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/domain/usecase/ResetAnnouncementMessageUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2025 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.core.presentation.CURRENT_ANNOUNCEMENT_ID 13 | 14 | class ResetAnnouncementMessageUseCase( 15 | private val userPreferencesRepository: UserPreferencesRepository 16 | ) { 17 | 18 | suspend operator fun invoke() { 19 | userPreferencesRepository.updateIdOfLastAnnouncementSeen(CURRENT_ANNOUNCEMENT_ID - 1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation 10 | 11 | import androidx.lifecycle.ViewModel 12 | import androidx.lifecycle.viewModelScope 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.SharingStarted 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.flow.stateIn 18 | import kotlinx.coroutines.flow.update 19 | import javax.inject.Inject 20 | 21 | data class HomeUiState( 22 | val showMainMenu: Boolean, 23 | ) 24 | 25 | typealias HomeUiEventHandler = (HomeUiEvent) -> Boolean 26 | 27 | sealed class HomeUiEvent { 28 | data object ShowMainMenu : HomeUiEvent() 29 | data object HideMainMenu : HomeUiEvent() 30 | } 31 | 32 | @HiltViewModel 33 | class HomeViewModel @Inject constructor() : ViewModel() { 34 | 35 | /** 36 | * Own state flows 37 | */ 38 | 39 | // Menu 40 | private val _showMainMenu = MutableStateFlow(false) 41 | 42 | /** 43 | * Composing the ui state 44 | */ 45 | 46 | val uiState = _showMainMenu.map { showMainMenu -> 47 | HomeUiState( 48 | showMainMenu = showMainMenu, 49 | ) 50 | }.stateIn( 51 | scope = viewModelScope, 52 | started = SharingStarted.WhileSubscribed(5000), 53 | initialValue = HomeUiState( 54 | showMainMenu = _showMainMenu.value, 55 | ) 56 | ) 57 | 58 | fun onUiEvent(event: HomeUiEvent): Boolean { 59 | when (event) { 60 | is HomeUiEvent.ShowMainMenu -> { 61 | _showMainMenu.update { true } 62 | } 63 | is HomeUiEvent.HideMainMenu -> { 64 | _showMainMenu.update { false } 65 | } 66 | } 67 | 68 | // events are consumed by default 69 | return true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/Musikus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.core.presentation 10 | 11 | import android.app.Application 12 | import dagger.hilt.android.HiltAndroidApp 13 | import java.util.concurrent.Executors 14 | 15 | const val CURRENT_ANNOUNCEMENT_ID = 1 16 | 17 | @HiltAndroidApp 18 | class Musikus : Application() { 19 | companion object { 20 | private val IO_EXECUTOR = Executors.newSingleThreadExecutor() 21 | 22 | fun ioThread(f: () -> Unit) { 23 | IO_EXECUTOR.execute(f) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/components/ConditionalModifier.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.components 10 | 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | 14 | // source: https://stackoverflow.com/questions/67768746/chaining-modifier-based-on-certain-conditions-in-android-compose 15 | 16 | @Composable 17 | fun Modifier.conditional( 18 | condition: Boolean, 19 | alternativeModifier: @Composable Modifier.() -> Modifier = { this }, 20 | modifier: @Composable Modifier.() -> Modifier, 21 | ): Modifier { 22 | return if (condition) { 23 | then(modifier(Modifier)) 24 | } else { 25 | then(alternativeModifier(Modifier)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/components/DialogHeader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2022-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.components 10 | 11 | import androidx.compose.foundation.background 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.Spacer 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.layout.width 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.unit.dp 25 | import app.musikus.core.presentation.theme.spacing 26 | import app.musikus.core.presentation.utils.UiIcon 27 | 28 | @Composable 29 | fun DialogHeader( 30 | title: String, 31 | icon: UiIcon? = null 32 | ) { 33 | Row( 34 | modifier = Modifier 35 | .padding(bottom = MaterialTheme.spacing.medium) // margin 36 | .fillMaxWidth() 37 | .background(MaterialTheme.colorScheme.primaryContainer) 38 | .padding(horizontal = MaterialTheme.spacing.large, vertical = MaterialTheme.spacing.medium) // padding 39 | ) { 40 | if (icon != null) { 41 | Icon( 42 | modifier = Modifier.size(24.dp).align(Alignment.CenterVertically), 43 | imageVector = icon.asIcon(), 44 | contentDescription = "jksdshf", 45 | tint = MaterialTheme.colorScheme.onPrimaryContainer, 46 | ) 47 | 48 | Spacer(Modifier.width(MaterialTheme.spacing.small)) 49 | } 50 | 51 | Text( 52 | text = title, 53 | style = MaterialTheme.typography.headlineSmall, 54 | color = MaterialTheme.colorScheme.onPrimaryContainer, 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/components/ExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024-2025 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.components 10 | 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.lifecycle.Lifecycle 14 | import androidx.lifecycle.compose.LocalLifecycleOwner 15 | import androidx.lifecycle.repeatOnLifecycle 16 | import kotlinx.coroutines.flow.Flow 17 | 18 | // inspired by: https://www.youtube.com/watch?v=njchj9d_Lf8 (Phillip Lackner) 19 | 20 | @Composable 21 | inline fun ExceptionHandler( 22 | exceptionChannel: Flow, 23 | crossinline exceptionHandler: (T) -> Unit, 24 | crossinline onUnhandledException: (Exception) -> Unit 25 | ) { 26 | val lifeCycleOwner = LocalLifecycleOwner.current 27 | 28 | LaunchedEffect(exceptionChannel, lifeCycleOwner.lifecycle) { 29 | lifeCycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 30 | exceptionChannel.collect { 31 | when (it) { 32 | is T -> exceptionHandler(it) 33 | else -> onUnhandledException(it) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/components/FadingEdge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Michael Prommersberger, Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.components 10 | 11 | import androidx.compose.foundation.gestures.ScrollableState 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.drawWithContent 14 | import androidx.compose.ui.graphics.BlendMode 15 | import androidx.compose.ui.graphics.Brush 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.CompositingStrategy 18 | import androidx.compose.ui.graphics.graphicsLayer 19 | 20 | /** 21 | * Modifier adding a fading edge to the top and bottom of the list depending on its scroll state. 22 | * */ 23 | fun Modifier.fadingEdge(scrollState: ScrollableState, vertical: Boolean = true): Modifier { 24 | val bckPossible = scrollState.canScrollBackward 25 | val fwdPossible = scrollState.canScrollForward 26 | 27 | val brush = getBrush(bckPossible, fwdPossible, vertical) 28 | return getModifiers(brush) 29 | } 30 | 31 | private fun Modifier.getModifiers(brush: Brush) = this 32 | .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) 33 | .drawWithContent { 34 | drawContent() 35 | drawRect(brush = brush, blendMode = BlendMode.DstIn) 36 | } 37 | 38 | private fun getBrush(top: Boolean, bottom: Boolean, vertical: Boolean): Brush { 39 | return if (vertical) { 40 | Brush.verticalGradient( 41 | 0f to if (top) Color.Transparent else Color.Red, 42 | 0.05f to Color.Red, 43 | 0.95f to Color.Red, 44 | 1f to if (bottom) Color.Transparent else Color.Red 45 | ) 46 | } else { 47 | Brush.horizontalGradient( 48 | 0f to if (top) Color.Transparent else Color.Red, 49 | 0.05f to Color.Red, 50 | 0.95f to Color.Red, 51 | 1f to if (bottom) Color.Transparent else Color.Red 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/components/Selectable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.components 10 | 11 | import androidx.compose.foundation.ExperimentalFoundationApi 12 | import androidx.compose.foundation.background 13 | import androidx.compose.foundation.combinedClickable 14 | import androidx.compose.foundation.layout.Box 15 | import androidx.compose.foundation.shape.CornerBasedShape 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.graphics.Color 21 | 22 | @OptIn(ExperimentalFoundationApi::class) 23 | @Composable 24 | fun Selectable( 25 | modifier: Modifier = Modifier, 26 | selected: Boolean, 27 | onShortClick: () -> Unit, 28 | onLongClick: () -> Unit, 29 | shape: CornerBasedShape = MaterialTheme.shapes.medium, 30 | enabled: Boolean = true, 31 | content: @Composable () -> Unit, 32 | ) { 33 | Box(modifier = modifier.clip(shape)) { 34 | content() 35 | Box( 36 | modifier = Modifier 37 | .matchParentSize() 38 | .conditional(enabled) { 39 | combinedClickable( 40 | onClick = onShortClick, 41 | onLongClick = onLongClick 42 | ) 43 | } 44 | .background( 45 | if (selected) { 46 | MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) 47 | } else { 48 | Color.Transparent 49 | }, 50 | ) 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/components/Snackbar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.components 10 | 11 | import android.content.Context 12 | import androidx.compose.material3.SnackbarDuration 13 | import androidx.compose.material3.SnackbarHostState 14 | import androidx.compose.material3.SnackbarResult 15 | import app.musikus.R 16 | import kotlinx.coroutines.CoroutineScope 17 | import kotlinx.coroutines.launch 18 | 19 | fun showSnackbar( 20 | context: Context, 21 | scope: CoroutineScope, 22 | hostState: SnackbarHostState, 23 | message: String, 24 | onUndo: (() -> Unit)? = null 25 | ) { 26 | scope.launch { 27 | val result = hostState.showSnackbar( 28 | message, 29 | actionLabel = if (onUndo != null) context.getString(R.string.components_snackbar_undo) else null, 30 | duration = SnackbarDuration.Long 31 | ) 32 | when (result) { 33 | SnackbarResult.ActionPerformed -> { 34 | onUndo?.invoke() 35 | } 36 | 37 | SnackbarResult.Dismissed -> { 38 | // do nothing 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/utils/TestTags.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2022-2024 Matthias Emde 7 | * 8 | * Parts of this software are licensed under the MIT license 9 | * 10 | * Copyright (c) 2022, Javier Carbone, author Matthias Emde 11 | */ 12 | 13 | package app.musikus.core.presentation.utils 14 | 15 | object TestTags { 16 | const val FOLDER_DIALOG_NAME_INPUT = "FOLDER_DIALOG_NAME_INPUT" 17 | const val ITEM_DIALOG_NAME_INPUT = "ITEM_DIALOG_NAME_INPUT" 18 | const val ITEM_DIALOG_FOLDER_SELECTOR_DROPDOWN = "ITEM_DIALOG_FOLDER_SELECTOR_DROPDOWN" 19 | const val FOLDER_LIST = "FOLDER_LIST" 20 | 21 | const val GOAL_DIALOG_PERIOD_UNIT_SELECTOR_DROPDOWN = "GOAL_DIALOG_PERIOD_UNIT_SELECTOR_DROPDOWN" 22 | const val GOAL_DIALOG_ITEM_SELECTOR_DROPDOWN = "GOAL_DIALOG_ITEM_SELECTOR_DROPDOWN" 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/core/presentation/utils/UiIcon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.core.presentation.utils 10 | 11 | import androidx.annotation.DrawableRes 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import androidx.compose.ui.res.vectorResource 15 | 16 | sealed class UiIcon { 17 | data class DynamicIcon(val value: ImageVector) : UiIcon() 18 | 19 | data class IconResource( 20 | @DrawableRes val resId: Int 21 | ) : UiIcon() 22 | 23 | @Composable 24 | fun asIcon(): ImageVector { 25 | return when (this) { 26 | is DynamicIcon -> value 27 | is IconResource -> ImageVector.vectorResource(resId) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/data/entities/GoalDescriptionLibraryItemCrossRef.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.data.entities 10 | 11 | import androidx.room.ColumnInfo 12 | import androidx.room.Entity 13 | import androidx.room.ForeignKey 14 | import app.musikus.library.data.entities.LibraryItemModel 15 | import java.util.UUID 16 | 17 | @Entity( 18 | tableName = "goal_description_library_item_cross_ref", 19 | primaryKeys = ["goal_description_id", "library_item_id"], 20 | foreignKeys = [ 21 | ForeignKey( 22 | entity = GoalDescriptionModel::class, 23 | parentColumns = ["id"], 24 | childColumns = ["goal_description_id"], 25 | onDelete = ForeignKey.CASCADE, 26 | ), 27 | ForeignKey( 28 | entity = LibraryItemModel::class, 29 | parentColumns = ["id"], 30 | childColumns = ["library_item_id"], 31 | onDelete = ForeignKey.CASCADE 32 | ) 33 | ] 34 | ) 35 | data class GoalDescriptionLibraryItemCrossRefModel( 36 | @ColumnInfo(name = "goal_description_id", index = true) val goalDescriptionId: UUID, 37 | @ColumnInfo(name = "library_item_id", index = true) val libraryItemId: UUID, 38 | ) 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/di/GoalRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.goals.di 10 | 11 | import app.musikus.core.data.MusikusDatabase 12 | import app.musikus.core.di.IoScope 13 | import app.musikus.goals.data.GoalRepositoryImpl 14 | import app.musikus.goals.domain.GoalRepository 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.launch 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | @InstallIn(SingletonComponent::class) 25 | object GoalRepositoryModule { 26 | 27 | @Provides 28 | @Singleton 29 | fun provideGoalRepository( 30 | database: MusikusDatabase, 31 | @IoScope ioScope: CoroutineScope 32 | ): GoalRepository { 33 | return GoalRepositoryImpl( 34 | database = database 35 | ).apply { 36 | ioScope.launch { 37 | clean() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/DeleteGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.goals.domain.GoalRepository 12 | import java.util.UUID 13 | 14 | class DeleteGoalsUseCase( 15 | private val goalRepository: GoalRepository 16 | ) { 17 | 18 | suspend operator fun invoke(ids: List) { 19 | goalRepository.delete(ids) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/GetAllGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.core.data.GoalDescriptionWithInstancesAndLibraryItems 12 | import app.musikus.goals.domain.GoalRepository 13 | import kotlinx.coroutines.flow.Flow 14 | 15 | class GetAllGoalsUseCase( 16 | private val goalRepository: GoalRepository, 17 | private val sortGoals: SortGoalsUseCase 18 | ) { 19 | 20 | operator fun invoke(): Flow> { 21 | return sortGoals(goalRepository.allGoals) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/GetCurrentGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibraryItems 12 | import app.musikus.goals.domain.GoalRepository 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.flatMapLatest 16 | import kotlinx.coroutines.flow.map 17 | 18 | class GetCurrentGoalsUseCase( 19 | private val goalRepository: GoalRepository, 20 | private val sortGoals: SortGoalsUseCase, 21 | private val calculateProgress: CalculateGoalProgressUseCase 22 | ) { 23 | 24 | @OptIn(ExperimentalCoroutinesApi::class) 25 | operator fun invoke( 26 | excludePaused: Boolean 27 | ): Flow> { 28 | return sortGoals(goalRepository.currentGoals).map { goals -> 29 | if (excludePaused) { 30 | goals.filter { !it.description.description.paused } 31 | } else { 32 | goals 33 | } 34 | }.flatMapLatest { goals -> 35 | calculateProgress(goals).map { progress -> 36 | goals.zip(progress).map { (goal, progress) -> 37 | GoalInstanceWithProgressAndDescriptionWithLibraryItems( 38 | description = goal.description, 39 | instance = goal.instance, 40 | progress = progress, 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/GetGoalSortInfoUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetGoalSortInfoUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.goalSortInfo 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/GetLastFiveCompletedGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibraryItems 12 | import app.musikus.goals.domain.GoalRepository 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.flatMapLatest 16 | import kotlinx.coroutines.flow.map 17 | 18 | class GetLastFiveCompletedGoalsUseCase( 19 | private val goalRepository: GoalRepository, 20 | private val calculateProgress: CalculateGoalProgressUseCase 21 | ) { 22 | 23 | @OptIn(ExperimentalCoroutinesApi::class) 24 | operator fun invoke(): Flow> { 25 | return goalRepository.lastFiveCompletedGoals.flatMapLatest { goals -> 26 | calculateProgress(goals).map { progress -> 27 | goals.zip(progress).map { (goal, progress) -> 28 | GoalInstanceWithProgressAndDescriptionWithLibraryItems( 29 | description = goal.description, 30 | instance = goal.instance, 31 | progress = progress, 32 | ) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/GoalsUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | data class GoalsUseCases( 12 | val calculateProgress: CalculateGoalProgressUseCase, 13 | val getAll: GetAllGoalsUseCase, 14 | val getCurrent: GetCurrentGoalsUseCase, 15 | val getLastFiveCompleted: GetLastFiveCompletedGoalsUseCase, 16 | val getLastNBeforeInstance: GetLastNBeforeInstanceUseCase, 17 | val getNextNAfterInstance: GetNextNAfterInstanceUseCase, 18 | val add: AddGoalUseCase, 19 | val pause: PauseGoalsUseCase, 20 | val unpause: UnpauseGoalsUseCase, 21 | val archive: ArchiveGoalsUseCase, 22 | val unarchive: UnarchiveGoalsUseCase, 23 | val update: UpdateGoalsUseCase, 24 | val edit: EditGoalUseCase, 25 | val delete: DeleteGoalsUseCase, 26 | val restore: RestoreGoalsUseCase, 27 | val getGoalSortInfo: GetGoalSortInfoUseCase, 28 | val selectGoalSortMode: SelectGoalsSortModeUseCase, 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/PauseGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.goals.data.entities.GoalDescriptionUpdateAttributes 12 | import app.musikus.goals.domain.GoalRepository 13 | import kotlinx.coroutines.flow.first 14 | import java.util.UUID 15 | 16 | class PauseGoalsUseCase( 17 | private val goalRepository: GoalRepository, 18 | private val cleanFutureGoalInstances: CleanFutureGoalInstancesUseCase 19 | ) { 20 | 21 | suspend operator fun invoke( 22 | goalDescriptionIds: List 23 | ) { 24 | val uniqueGoalDescriptionIds = goalDescriptionIds.distinct() 25 | 26 | val goals = goalRepository.allGoals.first().filter { it.description.id in uniqueGoalDescriptionIds } 27 | 28 | val missingGoalIds = uniqueGoalDescriptionIds - goals.map { it.description.id }.toSet() 29 | if (missingGoalIds.isNotEmpty()) { 30 | throw IllegalArgumentException("Could not find goal(s) with descriptionId: $missingGoalIds") 31 | } 32 | 33 | val archivedGoals = goals.filter { it.description.archived } 34 | if (archivedGoals.isNotEmpty()) { 35 | throw IllegalArgumentException("Cannot pause archived goals: ${archivedGoals.map { it.description.id }}") 36 | } 37 | 38 | val pausedGoals = goals.filter { it.description.paused } 39 | if (pausedGoals.isNotEmpty()) { 40 | throw IllegalArgumentException( 41 | "Cannot pause already paused goals: ${pausedGoals.map { it.description.id }}" 42 | ) 43 | } 44 | 45 | cleanFutureGoalInstances() 46 | 47 | goalRepository.updateGoalDescriptions( 48 | goalDescriptionIds.map { it to GoalDescriptionUpdateAttributes(paused = true) } 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/RestoreGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.goals.domain.GoalRepository 12 | import java.util.UUID 13 | 14 | class RestoreGoalsUseCase( 15 | private val goalRepository: GoalRepository 16 | ) { 17 | 18 | suspend operator fun invoke(descriptionIds: List) { 19 | goalRepository.restore(descriptionIds) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/SelectGoalsSortModeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.core.domain.SortDirection 12 | import app.musikus.core.domain.SortInfo 13 | import app.musikus.core.domain.UserPreferencesRepository 14 | import app.musikus.goals.data.GoalsSortMode 15 | import kotlinx.coroutines.flow.first 16 | 17 | class SelectGoalsSortModeUseCase( 18 | private val userPreferencesRepository: UserPreferencesRepository 19 | ) { 20 | 21 | suspend operator fun invoke(sortMode: GoalsSortMode) { 22 | val currentSortInfo = userPreferencesRepository.goalSortInfo.first() 23 | 24 | if (currentSortInfo.mode == sortMode) { 25 | userPreferencesRepository.updateGoalSortInfo( 26 | currentSortInfo.copy( 27 | direction = currentSortInfo.direction.invert() 28 | ) 29 | ) 30 | return 31 | } 32 | userPreferencesRepository.updateGoalSortInfo( 33 | SortInfo( 34 | mode = sortMode, 35 | direction = SortDirection.DEFAULT 36 | ) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/SortGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.core.data.GoalDescriptionWithInstancesAndLibraryItems 12 | import app.musikus.core.data.GoalInstanceWithDescriptionWithLibraryItems 13 | import app.musikus.goals.data.GoalsSortMode 14 | import app.musikus.goals.data.sorted 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.combine 17 | 18 | class SortGoalsUseCase( 19 | private val getGoalSortInfo: GetGoalSortInfoUseCase 20 | ) { 21 | 22 | @JvmName("sortGoalDescriptionWithInstances") 23 | operator fun invoke( 24 | goalsFlow: Flow> 25 | ): Flow> { 26 | return combine( 27 | goalsFlow, 28 | getGoalSortInfo() 29 | ) { goals, (sortMode, sortDirection) -> 30 | goals.sorted(sortMode as GoalsSortMode, sortDirection) 31 | } 32 | } 33 | 34 | @JvmName("sortGoalInstanceWithDescription") 35 | operator fun invoke( 36 | goalsFlow: Flow> 37 | ): Flow> { 38 | return combine( 39 | goalsFlow, 40 | getGoalSortInfo() 41 | ) { goals, (sortMode, sortDirection) -> 42 | goals.sorted(sortMode as GoalsSortMode, sortDirection) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/domain/usecase/UnpauseGoalsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.domain.usecase 10 | 11 | import app.musikus.goals.data.entities.GoalDescriptionUpdateAttributes 12 | import app.musikus.goals.domain.GoalRepository 13 | import kotlinx.coroutines.flow.first 14 | import java.util.UUID 15 | 16 | class UnpauseGoalsUseCase( 17 | private val goalRepository: GoalRepository 18 | ) { 19 | 20 | suspend operator fun invoke( 21 | goalDescriptionIds: List 22 | ) { 23 | val uniqueGoalDescriptionIds = goalDescriptionIds.distinct() 24 | 25 | val goals = goalRepository.allGoals.first().filter { it.description.id in uniqueGoalDescriptionIds } 26 | 27 | val missingGoalIds = uniqueGoalDescriptionIds - goals.map { it.description.id }.toSet() 28 | if (missingGoalIds.isNotEmpty()) { 29 | throw IllegalArgumentException("Could not find goal(s) with descriptionId: $missingGoalIds") 30 | } 31 | 32 | val archivedGoals = goals.filter { it.description.archived } 33 | if (archivedGoals.isNotEmpty()) { 34 | throw IllegalArgumentException("Cannot unpause archived goals: ${archivedGoals.map { it.description.id }}") 35 | } 36 | 37 | val nonPausedGoals = goals.filter { !it.description.paused } 38 | if (nonPausedGoals.isNotEmpty()) { 39 | throw IllegalArgumentException( 40 | "Cannot unpause goals that aren't paused: ${nonPausedGoals.map { it.description.id }}" 41 | ) 42 | } 43 | 44 | goalRepository.updateGoalDescriptions( 45 | goalDescriptionIds.map { it to GoalDescriptionUpdateAttributes(paused = false) } 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/presentation/GoalsUiEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.presentation 10 | 11 | import app.musikus.goals.data.GoalsSortMode 12 | import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibraryItems 13 | 14 | typealias GoalsUiEventHandler = (GoalsUiEvent) -> Boolean 15 | 16 | sealed class GoalsUiEvent { 17 | data object BackButtonPressed : GoalsUiEvent() 18 | 19 | data class GoalPressed( 20 | val goal: GoalInstanceWithProgressAndDescriptionWithLibraryItems, 21 | val longClick: Boolean 22 | ) : GoalsUiEvent() 23 | 24 | data object GoalSortMenuPressed : GoalsUiEvent() 25 | data class GoalSortModeSelected(val mode: GoalsSortMode) : GoalsUiEvent() 26 | 27 | data object ArchiveButtonPressed : GoalsUiEvent() 28 | 29 | data object DeleteButtonPressed : GoalsUiEvent() 30 | data object DeleteOrArchiveDialogDismissed : GoalsUiEvent() 31 | data object DeleteOrArchiveDialogConfirmed : GoalsUiEvent() 32 | 33 | data object UndoButtonPressed : GoalsUiEvent() 34 | data object EditButtonPressed : GoalsUiEvent() 35 | 36 | data class AddGoalButtonPressed(val oneShot: Boolean) : GoalsUiEvent() 37 | 38 | data object ClearActionMode : GoalsUiEvent() 39 | 40 | data class DialogUiEvent(val dialogEvent: GoalDialogUiEvent) : GoalsUiEvent() 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/goals/presentation/GoalsUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.goals.presentation 10 | 11 | import app.musikus.core.domain.SortDirection 12 | import app.musikus.goals.data.GoalsSortMode 13 | import app.musikus.goals.domain.GoalInstanceWithProgressAndDescriptionWithLibraryItems 14 | import app.musikus.library.data.daos.LibraryItem 15 | import app.musikus.library.presentation.DialogMode 16 | import java.util.UUID 17 | 18 | data class GoalsSortMenuUiState( 19 | val show: Boolean, 20 | 21 | val mode: GoalsSortMode, 22 | val direction: SortDirection, 23 | ) 24 | 25 | data class GoalsTopBarUiState( 26 | val sortMenuUiState: GoalsSortMenuUiState, 27 | ) 28 | 29 | data class GoalsActionModeUiState( 30 | val isActionMode: Boolean, 31 | val numberOfSelections: Int, 32 | val showEditAction: Boolean, 33 | ) 34 | 35 | data class GoalsContentUiState( 36 | val currentGoals: List, 37 | val selectedGoalIds: Set, 38 | 39 | val showHint: Boolean, 40 | ) 41 | 42 | /** 43 | * UI state for the dialog which is displayed when adding or changing a goal. 44 | */ 45 | data class GoalsAddOrEditDialogUiState( 46 | val mode: DialogMode, 47 | val oneShotGoal: Boolean, 48 | val goalToEditId: UUID?, 49 | val initialTargetHours: Int, 50 | val initialTargetMinutes: Int, 51 | val libraryItems: List, 52 | ) 53 | 54 | data class GoalsDeleteOrArchiveDialogUiState( 55 | val isArchiveAction: Boolean, 56 | val numberOfSelections: Int, 57 | ) 58 | 59 | /** 60 | * Container for both dialogs that can be shown in the goals screen. 61 | */ 62 | data class GoalsDialogUiState( 63 | val addOrEditDialogUiState: GoalsAddOrEditDialogUiState?, 64 | val deleteOrArchiveDialogUiState: GoalsDeleteOrArchiveDialogUiState?, 65 | ) 66 | 67 | data class GoalsUiState( 68 | val topBarUiState: GoalsTopBarUiState, 69 | val actionModeUiState: GoalsActionModeUiState, 70 | val contentUiState: GoalsContentUiState, 71 | val dialogUiState: GoalsDialogUiState, 72 | ) 73 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/data/entities/LibraryFolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2022-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.data.entities 10 | 11 | import androidx.room.ColumnInfo 12 | import androidx.room.Entity 13 | import app.musikus.core.data.Nullable 14 | import app.musikus.core.data.entities.ISoftDeleteModelCreationAttributes 15 | import app.musikus.core.data.entities.ISoftDeleteModelUpdateAttributes 16 | import app.musikus.core.data.entities.SoftDeleteModel 17 | import app.musikus.core.data.entities.SoftDeleteModelCreationAttributes 18 | import app.musikus.core.data.entities.SoftDeleteModelUpdateAttributes 19 | 20 | private interface ILibraryFolderCreationAttributes : ISoftDeleteModelCreationAttributes { 21 | val name: String 22 | } 23 | 24 | private interface ILibraryFolderUpdateAttributes : ISoftDeleteModelUpdateAttributes { 25 | val name: String? 26 | val customOrder: Nullable? 27 | } 28 | 29 | data class LibraryFolderCreationAttributes( 30 | override val name: String, 31 | ) : SoftDeleteModelCreationAttributes(), ILibraryFolderCreationAttributes 32 | 33 | data class LibraryFolderUpdateAttributes( 34 | override val name: String? = null, 35 | override val customOrder: Nullable? = null, 36 | ) : SoftDeleteModelUpdateAttributes(), ILibraryFolderUpdateAttributes 37 | 38 | @Entity(tableName = "library_folder") 39 | data class LibraryFolderModel( 40 | @ColumnInfo(name = "name") override var name: String, 41 | @ColumnInfo(name = "custom_order") override var customOrder: Nullable? = null, 42 | ) : SoftDeleteModel(), ILibraryFolderCreationAttributes, ILibraryFolderUpdateAttributes 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/di/LibraryRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.library.di 10 | 11 | import app.musikus.core.data.MusikusDatabase 12 | import app.musikus.core.di.IoScope 13 | import app.musikus.library.data.LibraryRepositoryImpl 14 | import app.musikus.library.domain.LibraryRepository 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.launch 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | @InstallIn(SingletonComponent::class) 25 | object LibraryRepositoryModule { 26 | 27 | @Provides 28 | @Singleton 29 | fun provideLibraryRepository( 30 | database: MusikusDatabase, 31 | @IoScope ioScope: CoroutineScope 32 | ): LibraryRepository { 33 | return LibraryRepositoryImpl( 34 | itemDao = database.libraryItemDao, 35 | folderDao = database.libraryFolderDao, 36 | ).apply { 37 | ioScope.launch { 38 | clean() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/Types.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2022-2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.library.domain 10 | 11 | import app.musikus.core.data.LibraryFolderWithItems 12 | import app.musikus.library.data.daos.LibraryItem 13 | import app.musikus.library.data.entities.LibraryFolderCreationAttributes 14 | import app.musikus.library.data.entities.LibraryFolderUpdateAttributes 15 | import app.musikus.library.data.entities.LibraryItemCreationAttributes 16 | import app.musikus.library.data.entities.LibraryItemUpdateAttributes 17 | import kotlinx.coroutines.flow.Flow 18 | import java.util.UUID 19 | 20 | interface LibraryRepository { 21 | val items: Flow> 22 | val folders: Flow> 23 | 24 | /** Mutators */ 25 | /** Add */ 26 | suspend fun addFolder(creationAttributes: LibraryFolderCreationAttributes) 27 | suspend fun addItem(creationAttributes: LibraryItemCreationAttributes) 28 | 29 | /** Edit */ 30 | suspend fun editFolder(id: UUID, updateAttributes: LibraryFolderUpdateAttributes) 31 | suspend fun editItem(id: UUID, updateAttributes: LibraryItemUpdateAttributes) 32 | 33 | /** Delete / restore */ 34 | suspend fun deleteItems(itemIds: List) 35 | suspend fun deleteFolders(folderIds: List) 36 | 37 | suspend fun restoreItems(itemIds: List) 38 | suspend fun restoreFolders(folderIds: List) 39 | 40 | /** Exists */ 41 | suspend fun existsItem(id: UUID): Boolean 42 | suspend fun existsFolder(id: UUID): Boolean 43 | 44 | /** Clean */ 45 | suspend fun clean() 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/AddFolderUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.data.daos.InvalidLibraryFolderException 12 | import app.musikus.library.data.entities.LibraryFolderCreationAttributes 13 | import app.musikus.library.domain.LibraryRepository 14 | 15 | class AddFolderUseCase( 16 | private val libraryRepository: LibraryRepository 17 | ) { 18 | 19 | @Throws(InvalidLibraryFolderException::class) 20 | suspend operator fun invoke( 21 | creationAttributes: LibraryFolderCreationAttributes 22 | ) { 23 | if (creationAttributes.name.isBlank()) { 24 | throw InvalidLibraryFolderException("Folder name can not be empty") 25 | } 26 | 27 | libraryRepository.addFolder( 28 | creationAttributes = creationAttributes 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/AddItemUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.data.entities.InvalidLibraryItemException 12 | import app.musikus.library.data.entities.LibraryItemCreationAttributes 13 | import app.musikus.library.domain.LibraryRepository 14 | 15 | class AddItemUseCase( 16 | private val libraryRepository: LibraryRepository 17 | ) { 18 | 19 | @Throws(InvalidLibraryItemException::class) 20 | suspend operator fun invoke( 21 | creationAttributes: LibraryItemCreationAttributes 22 | ) { 23 | if (creationAttributes.name.isBlank()) { 24 | throw InvalidLibraryItemException("Item name cannot be empty") 25 | } 26 | 27 | if (creationAttributes.colorIndex !in 0..9) { 28 | throw InvalidLibraryItemException("Color index must be between 0 and 9") 29 | } 30 | 31 | if ( 32 | creationAttributes.libraryFolderId.value != null && 33 | !libraryRepository.existsFolder(creationAttributes.libraryFolderId.value) 34 | ) { 35 | throw InvalidLibraryItemException("Folder (${creationAttributes.libraryFolderId.value}) does not exist") 36 | } 37 | 38 | libraryRepository.addItem( 39 | creationAttributes = creationAttributes 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/DeleteFoldersUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.domain.LibraryRepository 12 | import java.util.UUID 13 | 14 | class DeleteFoldersUseCase( 15 | private val libraryRepository: LibraryRepository 16 | ) { 17 | 18 | suspend operator fun invoke(folderIds: List) { 19 | libraryRepository.deleteFolders(folderIds) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/DeleteItemsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.domain.LibraryRepository 12 | import java.util.UUID 13 | 14 | class DeleteItemsUseCase( 15 | private val libraryRepository: LibraryRepository 16 | ) { 17 | 18 | suspend operator fun invoke(itemIds: List) { 19 | libraryRepository.deleteItems(itemIds) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/EditFolderUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.data.daos.InvalidLibraryFolderException 12 | import app.musikus.library.data.entities.LibraryFolderUpdateAttributes 13 | import app.musikus.library.domain.LibraryRepository 14 | import java.util.UUID 15 | 16 | class EditFolderUseCase( 17 | private val libraryRepository: LibraryRepository 18 | ) { 19 | 20 | @Throws(InvalidLibraryFolderException::class) 21 | suspend operator fun invoke( 22 | id: UUID, 23 | updateAttributes: LibraryFolderUpdateAttributes 24 | ) { 25 | if (!libraryRepository.existsFolder(id)) { 26 | throw InvalidLibraryFolderException("Folder not found") 27 | } 28 | 29 | if (updateAttributes.name != null && updateAttributes.name.isBlank()) { 30 | throw InvalidLibraryFolderException("Folder name can not be empty") 31 | } 32 | 33 | libraryRepository.editFolder( 34 | id = id, 35 | updateAttributes = updateAttributes 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/EditItemUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.data.entities.InvalidLibraryItemException 12 | import app.musikus.library.data.entities.LibraryItemUpdateAttributes 13 | import app.musikus.library.domain.LibraryRepository 14 | import java.util.UUID 15 | 16 | class EditItemUseCase( 17 | private val libraryRepository: LibraryRepository 18 | ) { 19 | 20 | @Throws(InvalidLibraryItemException::class) 21 | suspend operator fun invoke( 22 | id: UUID, 23 | updateAttributes: LibraryItemUpdateAttributes 24 | ) { 25 | if (!libraryRepository.existsItem(id)) { 26 | throw InvalidLibraryItemException("Item not found") 27 | } 28 | 29 | if (updateAttributes.name != null && updateAttributes.name.isBlank()) { 30 | throw InvalidLibraryItemException("Item name cannot be empty") 31 | } 32 | 33 | if (updateAttributes.colorIndex != null && updateAttributes.colorIndex !in 0..9) { 34 | throw InvalidLibraryItemException("Color index must be between 0 and 9") 35 | } 36 | 37 | if ( 38 | updateAttributes.libraryFolderId?.value != null && 39 | !libraryRepository.existsFolder(updateAttributes.libraryFolderId.value) 40 | ) { 41 | throw InvalidLibraryItemException("Folder (${updateAttributes.libraryFolderId.value}) does not exist") 42 | } 43 | 44 | libraryRepository.editItem( 45 | id = id, 46 | updateAttributes = updateAttributes 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/GetAllLibraryItemsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.domain.LibraryRepository 12 | 13 | class GetAllLibraryItemsUseCase( 14 | private val libraryRepository: LibraryRepository, 15 | ) { 16 | 17 | operator fun invoke() = libraryRepository.items 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/GetFolderSortInfoUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetFolderSortInfoUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.folderSortInfo 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/GetItemSortInfoUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetItemSortInfoUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.itemSortInfo 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/GetLastPracticedDateUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.data.daos.LibraryItem 12 | import app.musikus.sessions.domain.SessionRepository 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.map 15 | import java.time.ZonedDateTime 16 | import java.util.UUID 17 | 18 | class GetLastPracticedDateUseCase( 19 | private val sessionRepository: SessionRepository 20 | ) { 21 | 22 | operator fun invoke(items: List): Flow> { 23 | return sessionRepository.getLastSectionsForItems(items).map { sections -> 24 | sections.groupBy { it.libraryItemId }.mapValues { (_, sections) -> 25 | sections.single().startTimestamp 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryFoldersUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.core.data.LibraryFolderWithItems 12 | import app.musikus.library.data.LibraryFolderSortMode 13 | import app.musikus.library.data.sorted 14 | import app.musikus.library.domain.LibraryRepository 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.combine 17 | 18 | class GetSortedLibraryFoldersUseCase( 19 | private val libraryRepository: LibraryRepository, 20 | private val getFolderSortInfo: GetFolderSortInfoUseCase, 21 | ) { 22 | 23 | operator fun invoke(): Flow> { 24 | return combine( 25 | libraryRepository.folders, 26 | getFolderSortInfo() 27 | ) { folders, folderSortInfo -> 28 | folders.sorted( 29 | folderSortInfo.mode as LibraryFolderSortMode, 30 | folderSortInfo.direction 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/GetSortedLibraryItemsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.core.data.Nullable 12 | import app.musikus.library.data.LibraryItemSortMode 13 | import app.musikus.library.data.daos.LibraryItem 14 | import app.musikus.library.data.sorted 15 | import app.musikus.library.domain.LibraryRepository 16 | import kotlinx.coroutines.flow.Flow 17 | import kotlinx.coroutines.flow.combine 18 | import kotlinx.coroutines.flow.map 19 | import java.util.UUID 20 | 21 | class GetSortedLibraryItemsUseCase( 22 | private val libraryRepository: LibraryRepository, 23 | private val getItemSortInfo: GetItemSortInfoUseCase, 24 | ) { 25 | 26 | /** 27 | * Get a sorted list library items. 28 | * 29 | * @param folderId: If null, all items are returned. 30 | * Otherwise only items of the given folder are returned. 31 | * A value of [Nullable(null)] refers to the root folder. 32 | * */ 33 | operator fun invoke( 34 | folderId: Nullable? = null, 35 | ): Flow> { 36 | return libraryRepository.items 37 | .map { items -> 38 | if (folderId == null) { 39 | items 40 | } else { 41 | items.filter { item -> 42 | item.libraryFolderId == folderId.value 43 | } 44 | } 45 | } 46 | .combine( 47 | getItemSortInfo() 48 | ) { filteredItems, itemSortInfo -> 49 | filteredItems.sorted( 50 | itemSortInfo.mode as LibraryItemSortMode, 51 | itemSortInfo.direction 52 | ) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/LibraryUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | data class LibraryUseCases( 12 | val getAllItems: GetAllLibraryItemsUseCase, 13 | val getSortedItems: GetSortedLibraryItemsUseCase, 14 | val getSortedFolders: GetSortedLibraryFoldersUseCase, 15 | val getLastPracticedDate: GetLastPracticedDateUseCase, 16 | val addItem: AddItemUseCase, 17 | val addFolder: AddFolderUseCase, 18 | val editItem: EditItemUseCase, 19 | val editFolder: EditFolderUseCase, 20 | val deleteItems: DeleteItemsUseCase, 21 | val deleteFolders: DeleteFoldersUseCase, 22 | val restoreItems: RestoreItemsUseCase, 23 | val restoreFolders: RestoreFoldersUseCase, 24 | val getFolderSortInfo: GetFolderSortInfoUseCase, 25 | val getItemSortInfo: GetItemSortInfoUseCase, 26 | val selectFolderSortMode: SelectFolderSortModeUseCase, 27 | val selectItemSortMode: SelectItemSortModeUseCase, 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/RestoreFoldersUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.domain.LibraryRepository 12 | import java.util.UUID 13 | 14 | class RestoreFoldersUseCase( 15 | private val libraryRepository: LibraryRepository 16 | ) { 17 | 18 | suspend operator fun invoke(folderIds: List) { 19 | libraryRepository.restoreFolders(folderIds) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/RestoreItemsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.library.domain.LibraryRepository 12 | import java.util.UUID 13 | 14 | class RestoreItemsUseCase( 15 | private val libraryRepository: LibraryRepository 16 | ) { 17 | 18 | suspend operator fun invoke(itemIds: List) { 19 | libraryRepository.restoreItems(itemIds) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/SelectFolderSortModeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.core.domain.SortDirection 12 | import app.musikus.core.domain.SortInfo 13 | import app.musikus.core.domain.UserPreferencesRepository 14 | import app.musikus.library.data.LibraryFolderSortMode 15 | import kotlinx.coroutines.flow.first 16 | 17 | class SelectFolderSortModeUseCase( 18 | private val userPreferencesRepository: UserPreferencesRepository 19 | ) { 20 | 21 | suspend operator fun invoke(sortMode: LibraryFolderSortMode) { 22 | val currentSortInfo = userPreferencesRepository.folderSortInfo.first() 23 | 24 | if (currentSortInfo.mode == sortMode) { 25 | userPreferencesRepository.updateLibraryFolderSortInfo( 26 | currentSortInfo.copy( 27 | direction = currentSortInfo.direction.invert() 28 | ) 29 | ) 30 | return 31 | } 32 | userPreferencesRepository.updateLibraryFolderSortInfo( 33 | SortInfo( 34 | mode = sortMode, 35 | direction = SortDirection.DEFAULT 36 | ) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/domain/usecase/SelectItemSortModeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2023-2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.domain.usecase 10 | 11 | import app.musikus.core.domain.SortDirection 12 | import app.musikus.core.domain.SortInfo 13 | import app.musikus.core.domain.UserPreferencesRepository 14 | import app.musikus.library.data.LibraryItemSortMode 15 | import kotlinx.coroutines.flow.first 16 | 17 | class SelectItemSortModeUseCase( 18 | private val userPreferencesRepository: UserPreferencesRepository 19 | ) { 20 | 21 | suspend operator fun invoke(sortMode: LibraryItemSortMode) { 22 | val currentSortInfo = userPreferencesRepository.itemSortInfo.first() 23 | 24 | if (currentSortInfo.mode == sortMode) { 25 | userPreferencesRepository.updateLibraryItemSortInfo( 26 | currentSortInfo.copy( 27 | direction = currentSortInfo.direction.invert() 28 | ) 29 | ) 30 | return 31 | } 32 | userPreferencesRepository.updateLibraryItemSortInfo( 33 | SortInfo( 34 | mode = sortMode, 35 | direction = SortDirection.DEFAULT 36 | ) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/presentation/LibraryCoreUiEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.presentation 10 | 11 | import app.musikus.library.data.LibraryItemSortMode 12 | import app.musikus.library.data.daos.LibraryItem 13 | 14 | typealias LibraryCoreUiEventHandler = (LibraryCoreUiEvent) -> Boolean 15 | 16 | sealed class LibraryCoreUiEvent { 17 | data class ItemPressed(val item: LibraryItem, val longClick: Boolean) : LibraryCoreUiEvent() 18 | data object ItemSortMenuPressed : LibraryCoreUiEvent() 19 | data class ItemSortModeSelected(val mode: LibraryItemSortMode) : LibraryCoreUiEvent() 20 | 21 | data object DeleteButtonPressed : LibraryCoreUiEvent() 22 | data object DeleteDialogDismissed : LibraryCoreUiEvent() 23 | data object DeleteDialogConfirmed : LibraryCoreUiEvent() 24 | data object RestoreButtonPressed : LibraryCoreUiEvent() 25 | 26 | data object EditButtonPressed : LibraryCoreUiEvent() 27 | 28 | data object AddItemButtonPressed : LibraryCoreUiEvent() 29 | 30 | data object ClearActionMode : LibraryCoreUiEvent() 31 | 32 | data class ItemDialogUiEvent(val dialogEvent: LibraryItemDialogUiEvent) : LibraryCoreUiEvent() 33 | data class FolderDialogUiEvent(val dialogEvent: LibraryFolderDialogUiEvent) : LibraryCoreUiEvent() 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/library/presentation/LibraryUiEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.library.presentation 10 | 11 | import app.musikus.library.data.LibraryFolderSortMode 12 | import app.musikus.library.data.daos.LibraryFolder 13 | 14 | // returns true if the event was consumed, false otherwise 15 | typealias LibraryUiEventHandler = (LibraryUiEvent) -> Boolean 16 | 17 | sealed class LibraryUiEvent { 18 | data class CoreUiEvent(val coreEvent: LibraryCoreUiEvent) : LibraryUiEvent() 19 | 20 | data object FolderSortMenuPressed : LibraryUiEvent() 21 | data class FolderSortModeSelected(val mode: LibraryFolderSortMode) : LibraryUiEvent() 22 | 23 | data class FolderPressed(val folder: LibraryFolder, val longClick: Boolean) : LibraryUiEvent() 24 | 25 | data object AddFolderButtonPressed : LibraryUiEvent() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/menu/di/SettingsUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.menu.di 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.menu.domain.usecase.GetColorSchemeUseCase 13 | import app.musikus.menu.domain.usecase.GetThemeUseCase 14 | import app.musikus.menu.domain.usecase.SelectColorSchemeUseCase 15 | import app.musikus.menu.domain.usecase.SelectThemeUseCase 16 | import app.musikus.menu.domain.usecase.SettingsUseCases 17 | import dagger.Module 18 | import dagger.Provides 19 | import dagger.hilt.InstallIn 20 | import dagger.hilt.components.SingletonComponent 21 | import javax.inject.Singleton 22 | 23 | @Module 24 | @InstallIn(SingletonComponent::class) 25 | object SettingsUseCasesModule { 26 | 27 | @Provides 28 | @Singleton 29 | fun provideSettingsUseCases( 30 | userPreferencesRepository: UserPreferencesRepository 31 | ): SettingsUseCases { 32 | return SettingsUseCases( 33 | getTheme = GetThemeUseCase(userPreferencesRepository), 34 | getColorScheme = GetColorSchemeUseCase(userPreferencesRepository), 35 | selectTheme = SelectThemeUseCase(userPreferencesRepository), 36 | selectColorScheme = SelectColorSchemeUseCase(userPreferencesRepository), 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/menu/domain/usecase/GetColorSchemeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.menu.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetColorSchemeUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.colorScheme 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/menu/domain/usecase/GetThemeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.menu.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetThemeUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.theme 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/menu/domain/usecase/SelectColorSchemeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.menu.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.menu.domain.ColorSchemeSelections 13 | 14 | class SelectColorSchemeUseCase( 15 | private val userPreferencesRepository: UserPreferencesRepository 16 | ) { 17 | 18 | suspend operator fun invoke(colorScheme: ColorSchemeSelections) { 19 | userPreferencesRepository.updateColorScheme(colorScheme) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/menu/domain/usecase/SelectThemeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.menu.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.menu.domain.ThemeSelections 13 | 14 | class SelectThemeUseCase( 15 | private val userPreferencesRepository: UserPreferencesRepository 16 | ) { 17 | 18 | suspend operator fun invoke(theme: ThemeSelections) { 19 | userPreferencesRepository.updateTheme(theme) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/menu/domain/usecase/SettingsUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.menu.domain.usecase 10 | 11 | data class SettingsUseCases( 12 | val getTheme: GetThemeUseCase, 13 | val getColorScheme: GetColorSchemeUseCase, 14 | val selectTheme: SelectThemeUseCase, 15 | val selectColorScheme: SelectColorSchemeUseCase, 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/metronome/di/MetronomeUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.metronome.di 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.metronome.domain.usecase.ChangeMetronomeSettingsUseCase 13 | import app.musikus.metronome.domain.usecase.GetMetronomeSettingsUseCase 14 | import app.musikus.metronome.domain.usecase.MetronomeUseCases 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | object MetronomeUseCasesModule { 24 | 25 | @Provides 26 | @Singleton 27 | fun provideMetronomeUseCases( 28 | userPreferencesRepository: UserPreferencesRepository 29 | ): MetronomeUseCases { 30 | return MetronomeUseCases( 31 | getMetronomeSettings = GetMetronomeSettingsUseCase(userPreferencesRepository), 32 | changeMetronomeSettings = ChangeMetronomeSettingsUseCase(userPreferencesRepository), 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/metronome/domain/usecase/ChangeMetronomeSettingsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.metronome.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | import app.musikus.metronome.presentation.MetronomeSettings 13 | 14 | class ChangeMetronomeSettingsUseCase( 15 | private val userPreferencesRepository: UserPreferencesRepository 16 | ) { 17 | 18 | suspend operator fun invoke(settings: MetronomeSettings) { 19 | userPreferencesRepository.updateMetronomeSettings(settings) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/metronome/domain/usecase/GetMetronomeSettingsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.metronome.domain.usecase 10 | 11 | import app.musikus.core.domain.UserPreferencesRepository 12 | 13 | class GetMetronomeSettingsUseCase( 14 | private val userPreferencesRepository: UserPreferencesRepository 15 | ) { 16 | 17 | operator fun invoke() = userPreferencesRepository.metronomeSettings 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/metronome/domain/usecase/MetronomeUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.metronome.domain.usecase 10 | 11 | data class MetronomeUseCases( 12 | val getMetronomeSettings: GetMetronomeSettingsUseCase, 13 | val changeMetronomeSettings: ChangeMetronomeSettingsUseCase 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/permissions/data/PermissionRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.data 10 | 11 | import app.musikus.permissions.domain.PermissionChecker 12 | import app.musikus.permissions.domain.PermissionRepository 13 | 14 | class PermissionRepositoryImpl( 15 | private val permissionChecker: PermissionChecker 16 | ) : PermissionRepository { 17 | override suspend fun requestPermissions(permissions: List): Result { 18 | return permissionChecker.requestPermission(*permissions.toTypedArray()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/permissions/di/PermissionsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.di 10 | 11 | import android.app.Application 12 | import app.musikus.core.di.ApplicationScope 13 | import app.musikus.permissions.data.PermissionRepositoryImpl 14 | import app.musikus.permissions.domain.PermissionChecker 15 | import app.musikus.permissions.domain.PermissionRepository 16 | import app.musikus.permissions.domain.usecase.PermissionsUseCases 17 | import app.musikus.permissions.domain.usecase.RequestPermissionsUseCase 18 | import dagger.Module 19 | import dagger.Provides 20 | import dagger.hilt.InstallIn 21 | import dagger.hilt.components.SingletonComponent 22 | import kotlinx.coroutines.CoroutineScope 23 | import javax.inject.Singleton 24 | 25 | @Module 26 | @InstallIn(SingletonComponent::class) 27 | object PermissionsModule { 28 | 29 | @Provides 30 | @Singleton 31 | fun providePermissionChecker( 32 | application: Application, 33 | @ApplicationScope applicationScope: CoroutineScope 34 | ): PermissionChecker { 35 | return PermissionChecker( 36 | context = application, 37 | applicationScope = applicationScope 38 | ) 39 | } 40 | 41 | @Provides 42 | @Singleton 43 | fun providePermissionRepository( 44 | permissionChecker: PermissionChecker 45 | ): PermissionRepository { 46 | return PermissionRepositoryImpl(permissionChecker) 47 | } 48 | 49 | @Provides 50 | @Singleton 51 | fun providePermissionsUseCases( 52 | permissionRepository: PermissionRepository 53 | ): PermissionsUseCases { 54 | return PermissionsUseCases( 55 | request = RequestPermissionsUseCase(permissionRepository) 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/permissions/domain/Types.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.domain 10 | 11 | interface PermissionRepository { 12 | suspend fun requestPermissions(permissions: List): Result 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/permissions/domain/usecase/PermissionsUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.domain.usecase 10 | 11 | data class PermissionsUseCases( 12 | val request: RequestPermissionsUseCase 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/permissions/domain/usecase/RequestPermissionsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.permissions.domain.usecase 10 | 11 | import app.musikus.permissions.domain.PermissionRepository 12 | 13 | class RequestPermissionsUseCase( 14 | private val permissionRepository: PermissionRepository 15 | ) { 16 | suspend operator fun invoke(permissions: List): Result { 17 | return permissionRepository.requestPermissions(permissions) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/recorder/di/RecordingsRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.recorder.di 10 | 11 | import android.app.Application 12 | import app.musikus.core.di.IoScope 13 | import app.musikus.recorder.data.RecordingsRepositoryImpl 14 | import app.musikus.recorder.domain.RecordingsRepository 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import kotlinx.coroutines.CoroutineScope 20 | import javax.inject.Singleton 21 | 22 | @Module 23 | @InstallIn(SingletonComponent::class) 24 | object RecordingsRepositoryModule { 25 | 26 | @Provides 27 | @Singleton 28 | fun provideRecordingsRepository( 29 | application: Application, 30 | @IoScope ioScope: CoroutineScope 31 | ): RecordingsRepository { 32 | return RecordingsRepositoryImpl( 33 | application = application, 34 | contentResolver = application.contentResolver, 35 | ioScope = ioScope 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/recorder/di/RecordingsUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.recorder.di 10 | 11 | import app.musikus.recorder.domain.RecordingsRepository 12 | import app.musikus.recorder.domain.usecase.GetRawRecordingUseCase 13 | import app.musikus.recorder.domain.usecase.GetRecordingsUseCase 14 | import app.musikus.recorder.domain.usecase.RecordingsUseCases 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | object RecordingsUseCasesModule { 24 | 25 | @Provides 26 | @Singleton 27 | fun provideRecordingsUseCases( 28 | recordingsRepository: RecordingsRepository 29 | ): RecordingsUseCases { 30 | return RecordingsUseCases( 31 | get = GetRecordingsUseCase(recordingsRepository), 32 | getRawRecording = GetRawRecordingUseCase(recordingsRepository) 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/recorder/domain/Types.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.recorder.domain 10 | 11 | import android.net.Uri 12 | import androidx.media3.common.MediaItem 13 | import kotlinx.coroutines.flow.Flow 14 | import java.time.ZonedDateTime 15 | import kotlin.time.Duration 16 | 17 | data class Recording( 18 | val mediaItem: MediaItem, 19 | val title: String, 20 | val duration: Duration, 21 | val date: ZonedDateTime, 22 | val contentUri: Uri 23 | ) 24 | 25 | interface RecordingsRepository { 26 | val recordings: Flow> 27 | 28 | suspend fun getRawRecording(contentUri: Uri): Result 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/recorder/domain/usecase/GetRawRecordingUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.recorder.domain.usecase 10 | 11 | import android.net.Uri 12 | import app.musikus.recorder.domain.RecordingsRepository 13 | 14 | class GetRawRecordingUseCase( 15 | private val recordingsRepository: RecordingsRepository 16 | ) { 17 | 18 | suspend operator fun invoke(contentUri: Uri): Result = recordingsRepository.getRawRecording(contentUri) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/recorder/domain/usecase/GetRecordingsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.recorder.domain.usecase 10 | 11 | import app.musikus.recorder.domain.RecordingsRepository 12 | 13 | class GetRecordingsUseCase( 14 | private val recordingsRepository: RecordingsRepository 15 | ) { 16 | 17 | operator fun invoke() = recordingsRepository.recordings 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/recorder/domain/usecase/RecordingsUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.recorder.domain.usecase 10 | 11 | data class RecordingsUseCases( 12 | val get: GetRecordingsUseCase, 13 | val getRawRecording: GetRawRecordingUseCase 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/di/SessionRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.sessions.di 10 | 11 | import androidx.room.withTransaction 12 | import app.musikus.core.data.MusikusDatabase 13 | import app.musikus.core.di.IoScope 14 | import app.musikus.core.domain.TimeProvider 15 | import app.musikus.sessions.data.SessionRepositoryImpl 16 | import app.musikus.sessions.domain.SessionRepository 17 | import dagger.Module 18 | import dagger.Provides 19 | import dagger.hilt.InstallIn 20 | import dagger.hilt.components.SingletonComponent 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.launch 23 | import javax.inject.Singleton 24 | 25 | @Module 26 | @InstallIn(SingletonComponent::class) 27 | object SessionRepositoryModule { 28 | 29 | @Provides 30 | @Singleton 31 | fun provideSessionRepository( 32 | database: MusikusDatabase, 33 | timeProvider: TimeProvider, 34 | @IoScope ioScope: CoroutineScope 35 | ): SessionRepository { 36 | return SessionRepositoryImpl( 37 | timeProvider = timeProvider, 38 | sessionDao = database.sessionDao, 39 | sectionDao = database.sectionDao, 40 | withDatabaseTransaction = { block -> database.withTransaction(block) } 41 | ).apply { 42 | ioScope.launch { 43 | clean() 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/di/SessionsUseCasesModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde, Michael Prommersberger 7 | */ 8 | 9 | package app.musikus.sessions.di 10 | 11 | import app.musikus.library.domain.usecase.LibraryUseCases 12 | import app.musikus.sessions.domain.SessionRepository 13 | import app.musikus.sessions.domain.usecase.AddSessionUseCase 14 | import app.musikus.sessions.domain.usecase.DeleteSessionsUseCase 15 | import app.musikus.sessions.domain.usecase.EditSessionUseCase 16 | import app.musikus.sessions.domain.usecase.GetAllSessionsUseCase 17 | import app.musikus.sessions.domain.usecase.GetSessionByIdUseCase 18 | import app.musikus.sessions.domain.usecase.GetSessionsForDaysForMonthsUseCase 19 | import app.musikus.sessions.domain.usecase.GetSessionsInTimeframeUseCase 20 | import app.musikus.sessions.domain.usecase.RestoreSessionsUseCase 21 | import app.musikus.sessions.domain.usecase.SessionsUseCases 22 | import dagger.Module 23 | import dagger.Provides 24 | import dagger.hilt.InstallIn 25 | import dagger.hilt.components.SingletonComponent 26 | import javax.inject.Singleton 27 | 28 | @Module 29 | @InstallIn(SingletonComponent::class) 30 | object SessionsUseCasesModule { 31 | 32 | @Provides 33 | @Singleton 34 | fun provideSessionsUseCases( 35 | sessionRepository: SessionRepository, 36 | libraryUseCases: LibraryUseCases 37 | ): SessionsUseCases { 38 | return SessionsUseCases( 39 | getAll = GetAllSessionsUseCase(sessionRepository), 40 | getSessionsForDaysForMonths = GetSessionsForDaysForMonthsUseCase(sessionRepository), 41 | getInTimeframe = GetSessionsInTimeframeUseCase(sessionRepository), 42 | getById = GetSessionByIdUseCase(sessionRepository), 43 | add = AddSessionUseCase(sessionRepository, libraryUseCases.getAllItems), 44 | edit = EditSessionUseCase(sessionRepository), 45 | delete = DeleteSessionsUseCase(sessionRepository), 46 | restore = RestoreSessionsUseCase(sessionRepository), 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/domain/usecase/DeleteSessionsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.domain.usecase 10 | 11 | import app.musikus.sessions.domain.SessionRepository 12 | import java.util.UUID 13 | 14 | class DeleteSessionsUseCase( 15 | private val sessionRepository: SessionRepository 16 | ) { 17 | 18 | suspend operator fun invoke(ids: List) { 19 | sessionRepository.delete(ids) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/domain/usecase/GetAllSessionsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.domain.usecase 10 | 11 | import app.musikus.core.data.SessionWithSectionsWithLibraryItems 12 | import app.musikus.sessions.domain.SessionRepository 13 | import kotlinx.coroutines.flow.Flow 14 | 15 | class GetAllSessionsUseCase( 16 | private val sessionsRepository: SessionRepository 17 | ) { 18 | 19 | operator fun invoke(): Flow> { 20 | return sessionsRepository.sessionsWithSectionsWithLibraryItems 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/domain/usecase/GetSessionByIdUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.domain.usecase 10 | 11 | import app.musikus.core.data.SessionWithSectionsWithLibraryItems 12 | import app.musikus.sessions.domain.SessionRepository 13 | import java.util.UUID 14 | 15 | class GetSessionByIdUseCase( 16 | private val sessionRepository: SessionRepository 17 | ) { 18 | 19 | suspend operator fun invoke(sessionId: UUID): SessionWithSectionsWithLibraryItems { 20 | return sessionRepository.getSession(sessionId) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/domain/usecase/GetSessionsInTimeframeUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.domain.usecase 10 | 11 | import app.musikus.core.data.SessionWithSectionsWithLibraryItems 12 | import app.musikus.core.domain.Timeframe 13 | import app.musikus.sessions.domain.SessionRepository 14 | import kotlinx.coroutines.flow.Flow 15 | 16 | class GetSessionsInTimeframeUseCase( 17 | private val sessionRepository: SessionRepository 18 | ) { 19 | 20 | operator fun invoke(timeframe: Timeframe): Flow> { 21 | return sessionRepository.sessionsInTimeframe(timeframe = timeframe) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/domain/usecase/RestoreSessionsUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.domain.usecase 10 | 11 | import app.musikus.sessions.domain.SessionRepository 12 | import java.util.UUID 13 | 14 | class RestoreSessionsUseCase( 15 | private val sessionRepository: SessionRepository 16 | ) { 17 | 18 | suspend operator fun invoke(ids: List) { 19 | sessionRepository.restore(ids) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/domain/usecase/SessionsUseCases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.domain.usecase 10 | 11 | data class SessionsUseCases( 12 | val getAll: GetAllSessionsUseCase, 13 | val getSessionsForDaysForMonths: GetSessionsForDaysForMonthsUseCase, 14 | val getInTimeframe: GetSessionsInTimeframeUseCase, 15 | val getById: GetSessionByIdUseCase, 16 | val add: AddSessionUseCase, 17 | val edit: EditSessionUseCase, 18 | val delete: DeleteSessionsUseCase, 19 | val restore: RestoreSessionsUseCase, 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/presentation/SessionsUiEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.presentation 10 | 11 | import java.util.UUID 12 | 13 | typealias SessionsUiEventHandler = (SessionsUiEvent) -> Boolean 14 | 15 | sealed class SessionsUiEvent { 16 | data class SessionPressed( 17 | val sessionId: UUID, 18 | val longClick: Boolean 19 | ) : SessionsUiEvent() 20 | 21 | data class MonthHeaderPressed(val specificMonth: Int) : SessionsUiEvent() 22 | data class EditButtonPressed(val editSession: (id: UUID) -> Unit) : SessionsUiEvent() 23 | data object DeleteButtonPressed : SessionsUiEvent() 24 | data object DeleteDialogDismissed : SessionsUiEvent() 25 | data object DeleteDialogConfirmed : SessionsUiEvent() 26 | 27 | data object UndoButtonPressed : SessionsUiEvent() 28 | 29 | data object ClearActionMode : SessionsUiEvent() 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/musikus/sessions/presentation/SessionsUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | * 6 | * Copyright (c) 2024 Matthias Emde 7 | */ 8 | 9 | package app.musikus.sessions.presentation 10 | 11 | import java.util.UUID 12 | 13 | data class SessionsActionModeUiState( 14 | val isActionMode: Boolean, 15 | val numberOfSelections: Int, 16 | ) 17 | 18 | data class SessionsContentUiState( 19 | val monthData: List, 20 | val selectedSessions: Set, 21 | 22 | val showHint: Boolean, 23 | ) 24 | 25 | data class SessionsDeleteDialogUiState( 26 | val numberOfSelections: Int, 27 | ) 28 | 29 | data class SessionsUiState( 30 | val actionModeUiState: SessionsActionModeUiState, 31 | val contentUiState: SessionsContentUiState, 32 | val deleteDialogUiState: SessionsDeleteDialogUiState?, 33 | ) 34 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fake_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/flip_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 17 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/animator/flip_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 17 | 18 | 19 | 25 | 26 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/animator/pause_to_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/animator/play_to_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/animator/play_to_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/animator/rotate_in_180_degrees.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/animator/rotate_out_180_degrees.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/animator/start_to_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/animator/stop_to_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/animator/stop_to_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/avd_goals.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 14 | 15 | 16 | 17 | 25 | 33 | 41 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avd_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 14 | 15 | 16 | 17 | 25 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avd_sessions.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 14 | 15 | 16 | 17 | 25 | 33 | 34 | 35 | 36 | 38 | 39 | 45 | 46 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_appearance.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bar_chart.xml: -------------------------------------------------------------------------------- 1 | 8 | 15 | 20 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_small_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_discord.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_export.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_goals.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_library.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_metronome.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_microphone.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_record.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sessions.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 17 | 18 | 22 | 23 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/musikus_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable/musikus_logo_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/musikus_logo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/drawable/musikus_logo_light.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/beat_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/raw/beat_1.wav -------------------------------------------------------------------------------- /app/src/main/res/raw/beat_2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/raw/beat_2.wav -------------------------------------------------------------------------------- /app/src/main/res/raw/beat_3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasemde/musikus-android/b18fc987cd1563ef17260d8a7433e9e5d251126e/app/src/main/res/raw/beat_3.wav -------------------------------------------------------------------------------- /app/src/main/res/values/permissions_strings.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | Permission required 12 | Go to App Settings 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/sessions_strings.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | Sessions 11 | 12 | 13 | Press \"Start session\" to start your first practice session. 14 | 15 | Start session 16 | Resume session 17 | 18 | 19 | Delete %1$d session? Any progress made during this session will be lost. 20 | Delete %1$d sessions? Any progress made during these sessions will be lost. 21 | 22 | Delete forever (%1$s) 23 | Deleted 24 | 25 | 26 | Practice time 27 | Break time 28 | Comment 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |