├── .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