├── .github └── workflows │ ├── android_ci.yml │ └── codeql.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── appInsightsSettings.xml ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── other.xml ├── studiobot.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── mediaplayer │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── bobbyesp │ │ │ └── mediaplayer │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── bobbyesp │ │ │ │ └── mediaplayer │ │ │ │ ├── di │ │ │ │ └── MediaPlayerModule.kt │ │ │ │ ├── ext │ │ │ │ └── MediaMetadata.kt │ │ │ │ └── service │ │ │ │ ├── ConnectionState.kt │ │ │ │ ├── MediaLibrarySessionCallback.kt │ │ │ │ ├── MediaServiceHandler.kt │ │ │ │ ├── MediaSessionConstants.kt │ │ │ │ ├── MediaplayerService.kt │ │ │ │ ├── notifications │ │ │ │ ├── MediaCustomActionReceiver.kt │ │ │ │ ├── MediaNotificationAdapter.kt │ │ │ │ ├── MediaNotificationManager.kt │ │ │ │ └── customLayout │ │ │ │ │ ├── MediaSessionLayoutHandler.kt │ │ │ │ │ └── MediaSessionLayoutHandlerImpl.kt │ │ │ │ └── queue │ │ │ │ ├── EmptyQueue.kt │ │ │ │ ├── Queue.kt │ │ │ │ └── SongsQueue.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── metadator_logo_player.xml │ │ │ ├── repeat.xml │ │ │ ├── repeat_on.xml │ │ │ ├── repeat_one_on.xml │ │ │ ├── shuffle.xml │ │ │ └── shuffle_on.xml │ │ │ ├── values-es │ │ │ └── strings.xml │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── bobbyesp │ │ └── mediaplayer │ │ └── ExampleUnitTest.kt ├── proguard-rules.pro ├── src │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── bobbyesp │ │ │ └── metadator │ │ │ └── ExampleInstrumentedTest.kt │ ├── foss │ │ └── kotlin │ │ │ └── FirebaseSetup.kt │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── bobbyesp │ │ │ │ └── metadator │ │ │ │ ├── App.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── Navigation.kt │ │ │ │ ├── core │ │ │ │ ├── data │ │ │ │ │ └── local │ │ │ │ │ │ ├── DarkThemePreference.kt │ │ │ │ │ │ └── preferences │ │ │ │ │ │ ├── AppPreferences.kt │ │ │ │ │ │ ├── AppPreferencesController.kt │ │ │ │ │ │ ├── PreferencesKey.kt │ │ │ │ │ │ ├── UserPreferences.kt │ │ │ │ │ │ └── datastore │ │ │ │ │ │ └── DataStorePreferences.kt │ │ │ │ ├── di │ │ │ │ │ ├── CoresModule.kt │ │ │ │ │ └── SystemManagersModule.kt │ │ │ │ ├── domain │ │ │ │ │ └── model │ │ │ │ │ │ └── ParcelableSong.kt │ │ │ │ ├── ext │ │ │ │ │ ├── KClass.kt │ │ │ │ │ ├── List.kt │ │ │ │ │ ├── MediaItem.kt │ │ │ │ │ ├── Player.kt │ │ │ │ │ ├── ReleaseDate.kt │ │ │ │ │ ├── Song.kt │ │ │ │ │ └── String.kt │ │ │ │ ├── presentation │ │ │ │ │ ├── SettingsRouting.kt │ │ │ │ │ ├── common │ │ │ │ │ │ ├── CompositionLocals.kt │ │ │ │ │ │ ├── DestinationInfo.kt │ │ │ │ │ │ └── Route.kt │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AppDetails.kt │ │ │ │ │ │ ├── image │ │ │ │ │ │ │ └── AsyncImage.kt │ │ │ │ │ │ └── text │ │ │ │ │ │ │ └── ConditionedMarqueeText.kt │ │ │ │ │ ├── pages │ │ │ │ │ │ └── settings │ │ │ │ │ │ │ ├── SettingsPage.kt │ │ │ │ │ │ │ └── modules │ │ │ │ │ │ │ └── GeneralSettingsPage.kt │ │ │ │ │ └── theme │ │ │ │ │ │ ├── Shapes.kt │ │ │ │ │ │ ├── Theme.kt │ │ │ │ │ │ └── Type.kt │ │ │ │ └── util │ │ │ │ │ ├── Debugging.kt │ │ │ │ │ ├── Navigation.kt │ │ │ │ │ ├── Permissions.kt │ │ │ │ │ └── getAppVersionName.kt │ │ │ │ ├── features │ │ │ │ └── spotify │ │ │ │ │ ├── data │ │ │ │ │ └── remote │ │ │ │ │ │ ├── SpotifyServiceImpl.kt │ │ │ │ │ │ ├── repository │ │ │ │ │ │ └── SearchRepositoryImpl.kt │ │ │ │ │ │ └── search │ │ │ │ │ │ └── SpotifySearchServiceImpl.kt │ │ │ │ │ ├── di │ │ │ │ │ └── SpotifyModule.kt │ │ │ │ │ └── domain │ │ │ │ │ ├── pagination │ │ │ │ │ └── TracksPagingSource.kt │ │ │ │ │ ├── repositories │ │ │ │ │ └── SearchRepository.kt │ │ │ │ │ └── services │ │ │ │ │ ├── SpotifyService.kt │ │ │ │ │ └── search │ │ │ │ │ └── SpotifySearchService.kt │ │ │ │ ├── mediaplayer │ │ │ │ ├── MediaplayerRouting.kt │ │ │ │ ├── data │ │ │ │ │ └── local │ │ │ │ │ │ └── MediaplayerServiceConnection.kt │ │ │ │ ├── di │ │ │ │ │ └── MediaplayerViewModelsModule.kt │ │ │ │ └── presentation │ │ │ │ │ ├── components │ │ │ │ │ ├── buttons │ │ │ │ │ │ └── PlayPauseAnimatedButton.kt │ │ │ │ │ └── others │ │ │ │ │ │ ├── RepeatStateIcon.kt │ │ │ │ │ │ └── ShuffleStateIcon.kt │ │ │ │ │ └── pages │ │ │ │ │ └── mediaplayer │ │ │ │ │ ├── MediaplayerPage.kt │ │ │ │ │ ├── MediaplayerViewModel.kt │ │ │ │ │ └── player │ │ │ │ │ ├── MediaplayerConstants.kt │ │ │ │ │ ├── MediaplayerSheet.kt │ │ │ │ │ ├── MediaplayerSheetView.kt │ │ │ │ │ ├── PlayerControls.kt │ │ │ │ │ ├── PlayerOptions.kt │ │ │ │ │ └── views │ │ │ │ │ ├── MediaplayerCollapsedContent.kt │ │ │ │ │ ├── MediaplayerExpandedContent.kt │ │ │ │ │ ├── MiniplayerContent.kt │ │ │ │ │ └── PlayerQueue.kt │ │ │ │ ├── mediastore │ │ │ │ ├── di │ │ │ │ │ └── MediaStoreViewModelsModule.kt │ │ │ │ ├── domain │ │ │ │ │ └── enums │ │ │ │ │ │ ├── CompactCardSize.kt │ │ │ │ │ │ └── LayoutType.kt │ │ │ │ └── presentation │ │ │ │ │ ├── MediaStorePage.kt │ │ │ │ │ ├── MediaStorePageViewModel.kt │ │ │ │ │ ├── components │ │ │ │ │ ├── card │ │ │ │ │ │ └── songs │ │ │ │ │ │ │ ├── HorizontalSongCard.kt │ │ │ │ │ │ │ ├── VerticalSongCard.kt │ │ │ │ │ │ │ ├── compact │ │ │ │ │ │ │ └── CompactSongCard.kt │ │ │ │ │ │ │ └── spotify │ │ │ │ │ │ │ └── SpotifyHorizontalSongCard.kt │ │ │ │ │ └── others │ │ │ │ │ │ └── status │ │ │ │ │ │ └── EmptyMediaStoreWarning.kt │ │ │ │ │ └── pages │ │ │ │ │ └── home │ │ │ │ │ └── HomePage.kt │ │ │ │ ├── onboarding │ │ │ │ ├── OnboardingRouting.kt │ │ │ │ ├── domain │ │ │ │ │ └── PermissionItem.kt │ │ │ │ └── presentation │ │ │ │ │ ├── components │ │ │ │ │ └── OnboardingScreenHeader.kt │ │ │ │ │ └── pages │ │ │ │ │ ├── OnboardingPermissionsPage.kt │ │ │ │ │ └── OnboardingWelcomePage.kt │ │ │ │ └── tageditor │ │ │ │ ├── TagEditorRouting.kt │ │ │ │ ├── di │ │ │ │ └── TagEditorViewModelsModule.kt │ │ │ │ └── presentation │ │ │ │ └── pages │ │ │ │ └── tageditor │ │ │ │ ├── MediaStoreInfoDialog.kt │ │ │ │ ├── MetadataEditorPage.kt │ │ │ │ ├── MetadataEditorViewModel.kt │ │ │ │ ├── SongSyncNeededDialog.kt │ │ │ │ └── spotify │ │ │ │ ├── MetadataBottomSheetViewModel.kt │ │ │ │ ├── SpMetadataBottomSheetContent.kt │ │ │ │ └── stages │ │ │ │ ├── NoSongInformationProvided.kt │ │ │ │ ├── SpMetadataBsDetails.kt │ │ │ │ └── SpMetadataBsSearch.kt │ │ ├── play_store_512.png │ │ └── res │ │ │ ├── drawable │ │ │ ├── metadator_logo_background.xml │ │ │ └── metadator_logo_foreground.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── resources.properties │ │ │ ├── values-es │ │ │ └── strings.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ ├── playstore │ │ └── kotlin │ │ │ └── FirebaseSetup.kt │ └── test │ │ └── java │ │ └── com │ │ └── bobbyesp │ │ └── metadator │ │ └── ExampleUnitTest.kt ├── ui │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── bobbyesp │ │ │ └── ui │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── bobbyesp │ │ │ │ └── ui │ │ │ │ ├── common │ │ │ │ └── pages │ │ │ │ │ ├── ErrorPage.kt │ │ │ │ │ ├── IdlePage.kt │ │ │ │ │ └── LoadingPage.kt │ │ │ │ ├── components │ │ │ │ ├── animatable │ │ │ │ │ └── rememberAnimatable.kt │ │ │ │ ├── bottomsheet │ │ │ │ │ └── draggable │ │ │ │ │ │ ├── DraggableBottomSheet.kt │ │ │ │ │ │ ├── DraggableBottomSheetAnchor.kt │ │ │ │ │ │ └── DraggableBottomSheetState.kt │ │ │ │ ├── button │ │ │ │ │ ├── BackButton.kt │ │ │ │ │ ├── FilledButtons.kt │ │ │ │ │ ├── OutlinedButtons.kt │ │ │ │ │ ├── SquaredButton.kt │ │ │ │ │ └── navigation │ │ │ │ │ │ └── HorizontalFloatingAppBarItem.kt │ │ │ │ ├── card │ │ │ │ │ ├── ExpandableElevatedCard.kt │ │ │ │ │ ├── OnboardingCard.kt │ │ │ │ │ ├── UtilityCard.kt │ │ │ │ │ └── WarningCard.kt │ │ │ │ ├── chip │ │ │ │ │ ├── ButtonChips.kt │ │ │ │ │ └── SingleChoiceChip.kt │ │ │ │ ├── dropdown │ │ │ │ │ ├── AnimatedDropdownMenu.kt │ │ │ │ │ ├── DropdownItemContainer.kt │ │ │ │ │ ├── DropdownMenuImplementation.kt │ │ │ │ │ └── M3ElevationTokens.kt │ │ │ │ ├── image │ │ │ │ │ └── ProfilePictureGenerator.kt │ │ │ │ ├── others │ │ │ │ │ ├── AdditionalInformation.kt │ │ │ │ │ ├── LoadingPlaceholder.kt │ │ │ │ │ ├── MetadataTag.kt │ │ │ │ │ ├── Placeholder.kt │ │ │ │ │ └── SelectableSurface.kt │ │ │ │ ├── preferences │ │ │ │ │ ├── PreferencesItems.kt │ │ │ │ │ ├── SettingOption.kt │ │ │ │ │ ├── SettingOptionsRow.kt │ │ │ │ │ ├── SettingSegmentOption.kt │ │ │ │ │ ├── SettingSegmentOptions.kt │ │ │ │ │ ├── SettingSlider.kt │ │ │ │ │ ├── SettingSwitch.kt │ │ │ │ │ └── SettingsItem.kt │ │ │ │ ├── pulltorefresh │ │ │ │ │ ├── PullState.kt │ │ │ │ │ ├── PullToRefreshIndicator.kt │ │ │ │ │ └── PullToRefreshLayout.kt │ │ │ │ ├── state │ │ │ │ │ └── LoadingState.kt │ │ │ │ ├── tags │ │ │ │ │ └── RoundedTag.kt │ │ │ │ ├── text │ │ │ │ │ ├── AnimatedCounter.kt │ │ │ │ │ ├── AutoResizableText.kt │ │ │ │ │ ├── CategoryTitles.kt │ │ │ │ │ ├── DotWithText.kt │ │ │ │ │ ├── ExpandableText.kt │ │ │ │ │ ├── MarqueeText.kt │ │ │ │ │ ├── PreConfiguredOutlinedTextField.kt │ │ │ │ │ ├── TextSizedComponents.kt │ │ │ │ │ └── bottomSheet │ │ │ │ │ │ └── BottomSheetTexts.kt │ │ │ │ └── topbar │ │ │ │ │ └── ColumnWithCollapsibleTopBar.kt │ │ │ │ ├── ext │ │ │ │ ├── Color.kt │ │ │ │ └── CornerBasedShape.kt │ │ │ │ ├── motion │ │ │ │ ├── AnimatedComposables.kt │ │ │ │ ├── AnimationSpecs.kt │ │ │ │ ├── MaterialSharedAxis.kt │ │ │ │ └── MotionConstants.kt │ │ │ │ └── util │ │ │ │ ├── AppBar.kt │ │ │ │ ├── Compose.kt │ │ │ │ ├── FadingEdge.kt │ │ │ │ ├── RememberSaveableWithInitialValue.kt │ │ │ │ ├── Savers.kt │ │ │ │ └── Scroll.kt │ │ └── res │ │ │ ├── values-es │ │ │ └── strings.xml │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── bobbyesp │ │ └── ui │ │ └── ExampleUnitTest.kt └── utilities │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── bobbyesp │ │ └── utilities │ │ ├── ConcurrentList.kt │ │ ├── Logging.kt │ │ ├── Packages.kt │ │ ├── Time.kt │ │ ├── ext │ │ ├── Array.kt │ │ ├── Int.kt │ │ ├── Long.kt │ │ ├── PropertyMap.kt │ │ └── String.kt │ │ ├── mediastore │ │ ├── AudioFileMetadata.kt │ │ ├── MediaStoreReceiver.kt │ │ ├── advanced │ │ │ ├── AdvancedContentResolverQuery.kt │ │ │ └── ContentResolverObserver.kt │ │ └── model │ │ │ └── Song.kt │ │ ├── navigation │ │ └── CustomNavigationArguments.kt │ │ ├── states │ │ ├── ResourceState.kt │ │ └── ScreenState.kt │ │ └── ui │ │ ├── Assets.kt │ │ ├── Colors.kt │ │ ├── LazyGridStateSaver.kt │ │ ├── PagingStateHandler.kt │ │ └── permission │ │ ├── PermissionNotGrantedDialog.kt │ │ └── PermissionRequestHandler.kt │ └── res │ ├── values-es │ └── strings.xml │ └── values │ └── strings.xml ├── assets ├── app_logo.png ├── feature_header.png └── mockups │ ├── Mockup1_FINAL.png │ ├── Mockup2_FINAL.png │ └── Mockup3_FINAL.png ├── build.gradle.kts ├── crashhandler ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── bobbyesp │ │ └── crashhandler │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── bobbyesp │ │ │ └── crashhandler │ │ │ ├── CrashHandler.kt │ │ │ ├── CrashHandlerActivity.kt │ │ │ ├── ReportInfo.kt │ │ │ └── ui │ │ │ ├── CrashHandlerPage.kt │ │ │ ├── UiUtils.kt │ │ │ ├── components │ │ │ ├── ExpandableElevatedCard.kt │ │ │ └── FilledButtonWithIcon.kt │ │ │ └── theme │ │ │ └── Theme.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── bobbyesp │ └── crashhandler │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/android_ci.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ "master" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-debug: 10 | name: Build FOSS APKs 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: set up JDK 21 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Setup Android SDK 24 | uses: android-actions/setup-android@v3 25 | 26 | - name: Create local.properties 27 | run: | 28 | echo "CLIENT_ID=${{ secrets.CLIENT_ID }}" >> local.properties 29 | echo "CLIENT_SECRET=${{ secrets.CLIENT_SECRET }}" >> local.properties 30 | 31 | - uses: gradle/actions/setup-gradle@v3 32 | 33 | - name: Grant execute permission for gradlew 34 | run: chmod +x gradlew 35 | 36 | - name: Decode Keystore 37 | env: 38 | ENCODED_KEYSTORE_STRING: ${{ secrets.SIGNING_KEY_STORE_BASE64 }} 39 | run: | 40 | base64 -d <<< $ENCODED_KEYSTORE_STRING > ./keystore.jks 41 | echo "RELEASE_STORE_FILE=$(realpath ./keystore.jks)" >> $GITHUB_ENV 42 | 43 | - name: Build with Gradle 44 | env: 45 | SIGNING_KEY_STORE_PATH: ${{ env.RELEASE_STORE_FILE }} 46 | SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} 47 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 48 | SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} 49 | run: ./gradlew assembleFoss 50 | 51 | - name: Upload APK 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: release-artifacts 55 | paths: | 56 | app/build/outputs/apk/release/ 57 | app/build/outputs/apk/debug/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | metadator_keystore.jks 17 | *.apk 18 | /.idea/deploymentTargetSelector.xml 19 | /.idea/other.xml 20 | /buildSrc/.gradle 21 | /buildSrc/build 22 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # GitHub Copilot persisted chat sessions 5 | /copilot/chatSessions 6 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Metadator -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 40 | 41 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 19 | 20 | 22 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/studiobot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | google-services.json -------------------------------------------------------------------------------- /app/mediaplayer/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/mediaplayer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.android.kotlin) 4 | alias(libs.plugins.kotlin.ksp) 5 | alias(libs.plugins.compose.compiler) 6 | } 7 | 8 | android { 9 | namespace = "com.bobbyesp.mediaplayer" 10 | compileSdk = 35 11 | 12 | defaultConfig { 13 | minSdk = 24 14 | 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles("consumer-rules.pro") 17 | } 18 | 19 | buildTypes { 20 | release { 21 | isMinifyEnabled = false 22 | proguardFiles( 23 | getDefaultProguardFile("proguard-android-optimize.txt"), 24 | "proguard-rules.pro" 25 | ) 26 | } 27 | } 28 | libraryVariants.all { 29 | val variantName = name 30 | sourceSets { 31 | getByName("main") { 32 | java.srcDir(File("build/generated/ksp/$variantName/kotlin")) 33 | } 34 | } 35 | } 36 | compileOptions { 37 | sourceCompatibility = JavaVersion.VERSION_21 38 | targetCompatibility = JavaVersion.VERSION_21 39 | } 40 | kotlinOptions { 41 | jvmTarget = "21" 42 | } 43 | } 44 | 45 | ksp { 46 | arg("KOIN_CONFIG_CHECK", "true") 47 | } 48 | 49 | dependencies { 50 | implementation(libs.core.ktx) 51 | implementation(libs.core.appcompat) 52 | implementation(libs.androidx.legacy.support.v4) // Needed MediaSessionCompat.Token 53 | 54 | //DI (Dependency Injection - Koin) 55 | implementation(libs.bundles.koin) 56 | 57 | //Media3 58 | implementation(libs.bundles.media3) 59 | 60 | //Coil 61 | implementation(libs.coil) 62 | 63 | testImplementation(libs.junit) 64 | androidTestImplementation(libs.androidx.test.ext.junit) 65 | androidTestImplementation(libs.espresso.core) 66 | } -------------------------------------------------------------------------------- /app/mediaplayer/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/mediaplayer/consumer-rules.pro -------------------------------------------------------------------------------- /app/mediaplayer/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/mediaplayer/src/androidTest/java/com/bobbyesp/mediaplayer/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.bobbyesp.mediaplayer.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/ext/MediaMetadata.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.ext 2 | 3 | import androidx.media3.common.MediaItem 4 | import androidx.media3.common.MediaMetadata 5 | 6 | fun MediaMetadata.toMediaItem(): MediaItem { 7 | return MediaItem.Builder() 8 | .setMediaMetadata(this) 9 | .build() 10 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/ConnectionState.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service 2 | 3 | import android.util.Log 4 | import androidx.annotation.OptIn 5 | import androidx.media3.common.util.UnstableApi 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.SharingStarted 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.flow.onEach 12 | import kotlinx.coroutines.flow.stateIn 13 | import kotlinx.coroutines.flow.update 14 | 15 | @OptIn(UnstableApi::class) 16 | sealed class ConnectionState { 17 | data object Disconnected : ConnectionState() 18 | data class Connected(val serviceHandler: MediaServiceHandler) : ConnectionState() 19 | } 20 | 21 | @OptIn(UnstableApi::class) 22 | class ConnectionHandler { 23 | private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) 24 | val connectionState: StateFlow = _connectionState 25 | .onEach { newState -> 26 | Log.d("ConnectionHandler", "Connection state changed: $newState") 27 | } 28 | .stateIn( 29 | CoroutineScope(Dispatchers.Default), 30 | started = SharingStarted.WhileSubscribed(5000), 31 | initialValue = ConnectionState.Disconnected 32 | ) 33 | 34 | fun connect(serviceHandler: MediaServiceHandler) { 35 | _connectionState.update { 36 | ConnectionState.Connected(serviceHandler) 37 | } 38 | } 39 | 40 | fun disconnect() { 41 | _connectionState.update { 42 | ConnectionState.Disconnected 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaSessionConstants.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service 2 | 3 | import android.os.Bundle 4 | import androidx.media3.session.SessionCommand 5 | 6 | object MediaSessionConstants { 7 | const val ACTION_TOGGLE_LIBRARY = "TOGGLE_LIBRARY" 8 | const val ACTION_TOGGLE_LIKE = "TOGGLE_LIKE" 9 | const val ACTION_TOGGLE_SHUFFLE = "TOGGLE_SHUFFLE" 10 | const val ACTION_TOGGLE_REPEAT_MODE = "TOGGLE_REPEAT_MODE" 11 | val CommandToggleLibrary = SessionCommand(ACTION_TOGGLE_LIBRARY, Bundle.EMPTY) 12 | val CommandToggleLike = SessionCommand(ACTION_TOGGLE_LIKE, Bundle.EMPTY) 13 | val CommandToggleShuffle = SessionCommand(ACTION_TOGGLE_SHUFFLE, Bundle.EMPTY) 14 | val CommandToggleRepeatMode = SessionCommand(ACTION_TOGGLE_REPEAT_MODE, Bundle.EMPTY) 15 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service.notifications 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import androidx.core.graphics.drawable.toBitmap 7 | import androidx.media3.common.Player 8 | import androidx.media3.common.util.UnstableApi 9 | import androidx.media3.ui.PlayerNotificationManager 10 | import coil.ImageLoader 11 | 12 | @UnstableApi 13 | class MediaNotificationAdapter( 14 | private val context: Context, 15 | private val pendingIntent: PendingIntent? 16 | ) : PlayerNotificationManager.MediaDescriptionAdapter { 17 | 18 | override fun getCurrentContentTitle(player: Player): CharSequence { 19 | return player.mediaMetadata.albumTitle 20 | ?: context.getString(androidx.media3.ui.R.string.exo_track_unknown) 21 | } 22 | 23 | override fun createCurrentContentIntent(player: Player): PendingIntent? { 24 | return pendingIntent 25 | } 26 | 27 | override fun getCurrentContentText(player: Player): CharSequence { 28 | return player.mediaMetadata.displayTitle 29 | ?: context.getString(androidx.media3.ui.R.string.exo_track_unknown) 30 | } 31 | 32 | override fun getCurrentLargeIcon( 33 | player: Player, 34 | callback: PlayerNotificationManager.BitmapCallback 35 | ): Bitmap? { 36 | val loader = ImageLoader(context) 37 | val request = coil.request.ImageRequest.Builder(context) 38 | .data(player.mediaMetadata.artworkData) 39 | .target { drawable -> 40 | callback.onBitmap(drawable.toBitmap()) 41 | } 42 | .build() 43 | loader.enqueue(request) 44 | 45 | return null 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandler.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service.notifications.customLayout 2 | 3 | interface MediaSessionLayoutHandler { 4 | fun updateNotificationLayout() 5 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service.queue 2 | 3 | import androidx.media3.common.MediaItem 4 | import androidx.media3.common.MediaMetadata 5 | 6 | object EmptyQueue : Queue { 7 | override val preloadItem: MediaMetadata? 8 | get() = null 9 | 10 | override suspend fun getInitialData(): Queue.Data = Queue.Data.empty() 11 | override fun hasNextPage(): Boolean = false 12 | override suspend fun nextPage(): List = emptyList() 13 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service.queue 2 | 3 | import androidx.media3.common.MediaItem 4 | import androidx.media3.common.MediaMetadata 5 | 6 | interface Queue { 7 | val preloadItem: MediaMetadata? 8 | suspend fun getInitialData(): Data 9 | fun hasNextPage(): Boolean 10 | suspend fun nextPage(): List 11 | 12 | data class Data( 13 | val title: String?, 14 | val items: List, 15 | val mediaItemIndex: Int, 16 | val position: Long = 0L, 17 | ) { 18 | companion object { 19 | fun empty() = Data(null, emptyList(), -1, 0L) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer.service.queue 2 | 3 | import androidx.media3.common.MediaItem 4 | import androidx.media3.common.MediaMetadata 5 | 6 | data class SongsQueue( 7 | val title: String? = null, 8 | val items: List, 9 | val startIndex: Int = 0, 10 | val position: Long = 0L, 11 | ) : Queue { 12 | override val preloadItem: MediaMetadata? = null 13 | override suspend fun getInitialData(): Queue.Data = 14 | Queue.Data(title, items, startIndex, position) 15 | 16 | override fun hasNextPage(): Boolean = false 17 | override suspend fun nextPage() = throw UnsupportedOperationException() 18 | } 19 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/drawable/metadator_logo_player.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/drawable/repeat.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/drawable/repeat_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/drawable/repeat_one_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/drawable/shuffle.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/drawable/shuffle_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Modo aleatorio desactivado 4 | Modo aleatorio activado 5 | Repetición desactivada 6 | Repetir la canción actual 7 | Repetir todo 8 | -------------------------------------------------------------------------------- /app/mediaplayer/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Shuffle off 4 | Shuffle on 5 | Repeat mode off 6 | Repeat current song 7 | Repeat all mode 8 | -------------------------------------------------------------------------------- /app/mediaplayer/src/test/java/com/bobbyesp/mediaplayer/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.mediaplayer 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bobbyesp/metadator/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.bobbyesp.metadator", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/foss/kotlin/FirebaseSetup.kt: -------------------------------------------------------------------------------- 1 | import com.bobbyesp.metadator.App 2 | import com.bobbyesp.metadator.MainActivity 3 | 4 | /** 5 | * Initialize Firebase services. 6 | * EMPTY Because this is part of the FOSS flavour of the app. 7 | */ 8 | fun App.initializeFirebase() {} 9 | 10 | /** 11 | * Setup Crashlytics collection to the app. 12 | * EMPTY Because this is part of the FOSS flavour of the app. 13 | */ 14 | fun MainActivity.setCrashlyticsCollection() {} -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/App.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator 2 | 3 | import android.app.Application 4 | import android.content.pm.PackageInfo 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import com.bobbyesp.crashhandler.CrashHandler.setupCrashHandler 8 | import com.bobbyesp.crashhandler.ReportInfo 9 | import com.bobbyesp.metadator.core.di.appCoroutinesScope 10 | import com.bobbyesp.metadator.core.di.appSystemManagers 11 | import com.bobbyesp.metadator.core.di.coreFunctionalitiesModule 12 | import com.bobbyesp.metadator.mediastore.di.mediaStoreViewModelsModule 13 | import com.bobbyesp.metadator.mediaplayer.di.mediaplayerViewModels 14 | import com.bobbyesp.metadator.tageditor.di.tagEditorViewModelsModule 15 | import com.bobbyesp.metadator.features.spotify.di.spotifyMainModule 16 | import com.bobbyesp.metadator.features.spotify.di.spotifyServicesModule 17 | import mediaplayerInternalsModule 18 | import org.koin.android.ext.koin.androidContext 19 | import org.koin.android.ext.koin.androidLogger 20 | import org.koin.core.context.GlobalContext.startKoin 21 | import kotlin.properties.Delegates 22 | 23 | class App : Application() { 24 | override fun onCreate() { 25 | startKoin { 26 | androidLogger() 27 | androidContext(this@App) 28 | modules(appSystemManagers, appCoroutinesScope, coreFunctionalitiesModule) 29 | modules(mediaplayerInternalsModule) 30 | modules(mediaStoreViewModelsModule, tagEditorViewModelsModule, mediaplayerViewModels) 31 | modules(spotifyMainModule, spotifyServicesModule) 32 | } 33 | packageInfo = packageManager.run { 34 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) getPackageInfo( 35 | packageName, PackageManager.PackageInfoFlags.of(0) 36 | ) else 37 | getPackageInfo(packageName, 0) 38 | } 39 | isPlayStoreBuild = BuildConfig.FLAVOR == "playstore" 40 | super.onCreate() 41 | 42 | if (!isPlayStoreBuild) setupCrashHandler( 43 | reportInfo = ReportInfo( 44 | androidVersion = true, 45 | deviceInfo = true, 46 | supportedABIs = true 47 | ) 48 | ) 49 | } 50 | 51 | companion object { 52 | lateinit var packageInfo: PackageInfo 53 | var isPlayStoreBuild by Delegates.notNull() 54 | 55 | val appVersion: String get() = packageInfo.versionName.toString() 56 | 57 | const val APP_PACKAGE_NAME = "com.bobbyesp.metadator" 58 | const val PREFERENCES_NAME = "${APP_PACKAGE_NAME}_preferences" 59 | const val APP_FILE_PROVIDER = "$APP_PACKAGE_NAME.fileprovider" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/data/local/DarkThemePreference.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.data.local 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.ui.res.stringResource 7 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.DARK_THEME_VALUE 8 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.HIGH_CONTRAST 9 | import com.bobbyesp.utilities.R 10 | 11 | @Stable 12 | data class DarkThemePreference( 13 | val darkThemeValue: DarkThemeValue = DarkThemeValue.valueOf(DARK_THEME_VALUE.defaultValue), 14 | val isHighContrastModeEnabled: Boolean = HIGH_CONTRAST.defaultValue 15 | ) { 16 | companion object { 17 | enum class DarkThemeValue { 18 | FOLLOW_SYSTEM, 19 | ON, 20 | OFF 21 | } 22 | } 23 | 24 | @Composable 25 | fun isDarkTheme(): Boolean { 26 | return if (darkThemeValue == DarkThemeValue.FOLLOW_SYSTEM) 27 | isSystemInDarkTheme() 28 | else darkThemeValue == DarkThemeValue.ON 29 | } 30 | 31 | @Composable 32 | fun getDarkThemeDescription(): String { 33 | return when (darkThemeValue) { 34 | DarkThemeValue.FOLLOW_SYSTEM -> stringResource(R.string.follow_system) 35 | DarkThemeValue.ON -> stringResource(R.string.on) 36 | else -> stringResource(R.string.off) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferencesController.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.data.local.preferences 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface AppPreferencesController { 6 | val TAG get() = "AppPreferencesController" 7 | 8 | val userPreferencesFlow: Flow 9 | suspend fun getUserPreferences(): UserPreferences 10 | 11 | suspend fun saveSetting(key: PreferencesKey, value: T) 12 | fun getSettingFlow(key: PreferencesKey, defaultValue: T?): Flow 13 | suspend fun getSetting(key: PreferencesKey, defaultValue: T?): T 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/PreferencesKey.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.data.local.preferences 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.booleanPreferencesKey 5 | import androidx.datastore.preferences.core.intPreferencesKey 6 | import androidx.datastore.preferences.core.stringPreferencesKey 7 | import com.bobbyesp.metadator.core.data.local.DarkThemePreference.Companion.DarkThemeValue 8 | import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType 9 | import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize 10 | import com.bobbyesp.utilities.ui.DEFAULT_SEED_COLOR 11 | import com.materialkolor.PaletteStyle 12 | 13 | sealed class PreferencesKey(val key: Preferences.Key, val defaultValue: T) { 14 | // --> Core 15 | data object COMPLETED_ONBOARDING : 16 | PreferencesKey(booleanPreferencesKey("completed_onboarding"), false) 17 | 18 | // --> UI 19 | data object SONGS_LAYOUT : 20 | PreferencesKey(stringPreferencesKey("songs_layout"), LayoutType.Grid.name) 21 | 22 | data object REDUCE_SHADOWS : 23 | PreferencesKey(booleanPreferencesKey("reduce_shadows"), false) 24 | 25 | data object MARQUEE_TEXT_ENABLED : 26 | PreferencesKey(booleanPreferencesKey("marquee_text_enabled"), true) 27 | 28 | data object SONG_CARD_SIZE : 29 | PreferencesKey(stringPreferencesKey("song_card_size"), CompactCardSize.LARGE.name) 30 | 31 | // --> Theming 32 | data object DARK_THEME_VALUE : PreferencesKey( 33 | stringPreferencesKey("dark_theme_value"), 34 | DarkThemeValue.FOLLOW_SYSTEM.name 35 | ) 36 | 37 | data object HIGH_CONTRAST : 38 | PreferencesKey(booleanPreferencesKey("high_contrast"), false) 39 | 40 | data object USE_DYNAMIC_COLORING : 41 | PreferencesKey(booleanPreferencesKey("dynamic_coloring"), true) 42 | 43 | data object THEME_COLOR : 44 | PreferencesKey(intPreferencesKey("theme_color"), DEFAULT_SEED_COLOR) 45 | 46 | data object PALETTE_STYLE : 47 | PreferencesKey(stringPreferencesKey("palette_style"), PaletteStyle.Vibrant.name) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/UserPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.data.local.preferences 2 | 3 | import androidx.compose.runtime.Stable 4 | import com.bobbyesp.metadator.core.data.local.DarkThemePreference 5 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.MARQUEE_TEXT_ENABLED 6 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.PALETTE_STYLE 7 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.REDUCE_SHADOWS 8 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.SONGS_LAYOUT 9 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.SONG_CARD_SIZE 10 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.THEME_COLOR 11 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.USE_DYNAMIC_COLORING 12 | import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType 13 | import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize 14 | import com.materialkolor.PaletteStyle 15 | 16 | @Stable 17 | data class UserPreferences( 18 | val songsLayout: LayoutType, 19 | val songCardSize: CompactCardSize, 20 | val reduceShadows: Boolean, 21 | val marqueeTextEnabled: Boolean, 22 | val darkThemePreference: DarkThemePreference, 23 | val useDynamicColoring: Boolean, 24 | val themeColor: Int, 25 | val paletteStyle: PaletteStyle 26 | ) { 27 | companion object { 28 | fun emptyUserPreferences(): UserPreferences = 29 | UserPreferences( 30 | songsLayout = LayoutType.valueOf(SONGS_LAYOUT.defaultValue), 31 | reduceShadows = REDUCE_SHADOWS.defaultValue, 32 | marqueeTextEnabled = MARQUEE_TEXT_ENABLED.defaultValue, 33 | songCardSize = CompactCardSize.valueOf(SONG_CARD_SIZE.defaultValue), 34 | darkThemePreference = DarkThemePreference(), 35 | useDynamicColoring = USE_DYNAMIC_COLORING.defaultValue, 36 | themeColor = THEME_COLOR.defaultValue, 37 | paletteStyle = PaletteStyle.valueOf(PALETTE_STYLE.defaultValue) 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/datastore/DataStorePreferences.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.data.local.preferences.datastore 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.runtime.rememberCoroutineScope 7 | import androidx.datastore.core.DataStore 8 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 9 | import androidx.datastore.preferences.core.Preferences 10 | import androidx.datastore.preferences.core.emptyPreferences 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 13 | import com.bobbyesp.metadator.App.Companion.PREFERENCES_NAME 14 | import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey 15 | import com.bobbyesp.metadator.core.presentation.common.LocalAppPreferencesController 16 | import kotlinx.coroutines.CoroutineScope 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.SupervisorJob 19 | import kotlinx.coroutines.flow.distinctUntilChanged 20 | import kotlinx.coroutines.launch 21 | 22 | val Context.dataStore: DataStore by preferencesDataStore( 23 | name = PREFERENCES_NAME, 24 | corruptionHandler = ReplaceFileCorruptionHandler( 25 | produceNewData = { emptyPreferences() } 26 | ), 27 | //migrations = emptyList(), 28 | scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 29 | ) 30 | 31 | @Composable 32 | fun rememberPreferenceState( 33 | key: PreferencesKey, 34 | defaultValue: T = key.defaultValue 35 | ): Pair, (T) -> Unit> { 36 | val appPreferences = LocalAppPreferencesController.current 37 | val coroutineScope = rememberCoroutineScope() 38 | 39 | val preferenceFlow = 40 | remember { appPreferences.getSettingFlow(key, defaultValue).distinctUntilChanged() } 41 | val valueState = preferenceFlow.collectAsStateWithLifecycle(initialValue = defaultValue) 42 | 43 | val updatePreference: (T) -> Unit = { newValue -> 44 | if (valueState.value != newValue) { 45 | coroutineScope.launch(Dispatchers.IO) { 46 | appPreferences.saveSetting(key, newValue) 47 | } 48 | } 49 | } 50 | 51 | return valueState to updatePreference 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/di/CoresModule.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.di 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import coil.ImageLoader 6 | import coil.disk.DiskCache 7 | import coil.memory.MemoryCache 8 | import com.bobbyesp.metadator.core.data.local.preferences.AppPreferences 9 | import com.bobbyesp.metadator.core.data.local.preferences.datastore.dataStore 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.SupervisorJob 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.core.qualifier.named 15 | import org.koin.dsl.module 16 | 17 | val appCoroutinesScope = module { 18 | single( 19 | qualifier = named("AppMainSupervisedScope") 20 | ) { CoroutineScope(SupervisorJob()) } 21 | } 22 | 23 | val coreFunctionalitiesModule = module { 24 | single> { 25 | androidContext().dataStore 26 | } 27 | single { 28 | AppPreferences( 29 | dataStore = get(), 30 | scope = get(qualifier = named("AppMainSupervisedScope")) 31 | ) 32 | } 33 | 34 | single { 35 | val context = androidContext() 36 | ImageLoader.Builder(context) 37 | .memoryCache { 38 | MemoryCache.Builder(context) 39 | .maxSizePercent(0.4) 40 | .build() 41 | } 42 | .diskCache { 43 | DiskCache.Builder() 44 | .directory(context.cacheDir.resolve("image_cache")) 45 | .maxSizeBytes(7 * 1024 * 1024) 46 | .build() 47 | } 48 | .respectCacheHeaders(false) 49 | .allowHardware(true) 50 | .crossfade(true) 51 | .crossfade(300) 52 | .bitmapFactoryMaxParallelism(12) 53 | .dispatcher(Dispatchers.IO) 54 | .build() 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/di/SystemManagersModule.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.di 2 | 3 | import android.content.ClipboardManager 4 | import android.content.Context.CLIPBOARD_SERVICE 5 | import android.content.Context.CONNECTIVITY_SERVICE 6 | import android.net.ConnectivityManager 7 | import org.koin.android.ext.koin.androidApplication 8 | import org.koin.android.ext.koin.androidContext 9 | import org.koin.dsl.module 10 | 11 | val appSystemManagers = module { 12 | single { androidApplication().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager } 13 | single { androidContext().getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/domain/model/ParcelableSong.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.domain.model 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import androidx.compose.runtime.Immutable 6 | import com.bobbyesp.utilities.mediastore.model.UriSerializer 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.serialization.Serializable 9 | 10 | @Parcelize 11 | @Immutable 12 | @Serializable 13 | data class ParcelableSong( 14 | val name: String, 15 | val mainArtist: String, 16 | val localPath: String, 17 | @Serializable(with = UriSerializer::class) val artworkPath: Uri? = null, 18 | val filename: String 19 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/ext/KClass.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.ext 2 | 3 | import kotlin.reflect.KClass 4 | 5 | fun KClass<*>.qualifiedName(): String = this.qualifiedName.toString() -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/ext/List.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.ext 2 | 3 | import com.adamratzman.spotify.models.SimpleArtist 4 | 5 | fun List.formatArtists(useAmpersands: Boolean = false): String { 6 | val artistNames = when (firstOrNull()) { 7 | is String -> this as List 8 | is SimpleArtist -> (this as List).mapNotNull { it.name } 9 | else -> return "" 10 | } 11 | 12 | return if (useAmpersands) { 13 | when (artistNames.size) { 14 | 0 -> "" 15 | 1 -> artistNames.first() 16 | 2 -> artistNames.joinToString(" & ") 17 | else -> artistNames.subList(0, artistNames.size - 1) 18 | .joinToString(", ") + " & " + artistNames.last() 19 | } 20 | } else { 21 | artistNames.joinToString(", ") 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/ext/MediaItem.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.ext 2 | 3 | import androidx.media3.common.MediaItem 4 | import com.bobbyesp.utilities.mediastore.model.Song 5 | 6 | fun MediaItem.toSong(): Song { 7 | val mediaMetadata = 8 | this.mediaMetadata 9 | return Song( 10 | id = mediaId.hashCode().toLong(), 11 | title = (mediaMetadata.displayTitle ?: "").toString(), 12 | artist = (mediaMetadata.artist ?: "").toString(), 13 | album = (mediaMetadata.albumTitle ?: "").toString(), 14 | artworkPath = mediaMetadata.artworkUri, 15 | duration = 0.0, 16 | path = this.localConfiguration?.uri.toString(), 17 | fileName = (mediaMetadata.title ?: "").toString() 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/ext/Player.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.ext 2 | 3 | import androidx.media3.common.C 4 | import androidx.media3.common.Player 5 | import androidx.media3.common.Player.REPEAT_MODE_OFF 6 | import androidx.media3.common.Timeline 7 | 8 | fun Player.getQueueWindows(): List { 9 | val timeline = currentTimeline 10 | if (timeline.isEmpty) { 11 | return emptyList() 12 | } 13 | val queue = ArrayDeque() 14 | val queueSize = timeline.windowCount 15 | 16 | val currentMediaItemIndex: Int = currentMediaItemIndex 17 | queue.add(timeline.getWindow(currentMediaItemIndex, Timeline.Window())) 18 | 19 | var firstMediaItemIndex = currentMediaItemIndex 20 | var lastMediaItemIndex = currentMediaItemIndex 21 | val shuffleModeEnabled = shuffleModeEnabled 22 | while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET) && queue.size < queueSize) { 23 | if (lastMediaItemIndex != C.INDEX_UNSET) { 24 | lastMediaItemIndex = 25 | timeline.getNextWindowIndex(lastMediaItemIndex, REPEAT_MODE_OFF, shuffleModeEnabled) 26 | if (lastMediaItemIndex != C.INDEX_UNSET) { 27 | queue.add(timeline.getWindow(lastMediaItemIndex, Timeline.Window())) 28 | } 29 | } 30 | if (firstMediaItemIndex != C.INDEX_UNSET && queue.size < queueSize) { 31 | firstMediaItemIndex = timeline.getPreviousWindowIndex( 32 | firstMediaItemIndex, 33 | REPEAT_MODE_OFF, 34 | shuffleModeEnabled 35 | ) 36 | if (firstMediaItemIndex != C.INDEX_UNSET) { 37 | queue.addFirst(timeline.getWindow(firstMediaItemIndex, Timeline.Window())) 38 | } 39 | } 40 | } 41 | return queue.toList() 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/ext/ReleaseDate.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.ext 2 | 3 | import com.adamratzman.spotify.models.ReleaseDate 4 | 5 | fun ReleaseDate.format(precision: String?, separator: String = "/"): String { 6 | return when (precision) { 7 | "year" -> { 8 | "$year" 9 | } 10 | 11 | "month" -> { 12 | "$month $separator $year" 13 | } 14 | 15 | "day" -> { 16 | "$day $separator $month $separator $year" 17 | } 18 | 19 | else -> { 20 | "Unknown" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/ext/Song.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.ext 2 | 3 | import com.bobbyesp.metadator.core.domain.model.ParcelableSong 4 | import com.bobbyesp.utilities.mediastore.model.Song 5 | 6 | fun Song.toParcelableSong(): ParcelableSong { 7 | return ParcelableSong( 8 | name = this.title, 9 | mainArtist = this.artist, 10 | localPath = this.path, 11 | artworkPath = this.artworkPath, 12 | filename = this.fileName 13 | ) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/presentation/SettingsRouting.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.presentation 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.compose.navigation 5 | import com.bobbyesp.metadator.core.presentation.common.Route 6 | import com.bobbyesp.metadator.core.presentation.pages.settings.SettingsPage 7 | import com.bobbyesp.metadator.core.presentation.pages.settings.modules.GeneralSettingsPage 8 | import com.bobbyesp.ui.motion.animatedComposable 9 | 10 | fun NavGraphBuilder.settingsRouting( 11 | onNavigateBack: () -> Unit 12 | ) { 13 | navigation( 14 | startDestination = Route.SettingsNavigator.Settings, 15 | ) { 16 | animatedComposable { 17 | SettingsPage( 18 | onBackPressed = onNavigateBack 19 | ) 20 | } 21 | 22 | animatedComposable { 23 | GeneralSettingsPage() 24 | } 25 | 26 | animatedComposable { 27 | 28 | } 29 | 30 | animatedComposable { 31 | 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/presentation/common/DestinationInfo.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.presentation.common 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.rounded.Home 6 | import androidx.compose.material.icons.rounded.PlayArrow 7 | import androidx.compose.runtime.Immutable 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import com.bobbyesp.metadator.R 10 | 11 | @Immutable 12 | enum class DestinationInfo( 13 | val icon: ImageVector, 14 | @StringRes val title: Int, 15 | ) { 16 | HOME( 17 | icon = Icons.Rounded.Home, 18 | title = R.string.home 19 | ), 20 | MEDIAPLAYER( 21 | icon = Icons.Rounded.PlayArrow, 22 | title = R.string.mediaplayer 23 | ); 24 | 25 | companion object { 26 | fun fromRoute(route: Route): DestinationInfo? { 27 | return when (route) { 28 | is Route.MetadatorNavigator -> HOME 29 | is Route.MediaplayerNavigator -> MEDIAPLAYER 30 | else -> null 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/presentation/common/Route.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.presentation.common 2 | 3 | import com.bobbyesp.metadator.core.domain.model.ParcelableSong 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed interface Route { 8 | 9 | @Serializable 10 | data object OnboardingNavigator : Route { 11 | @Serializable 12 | data object Welcome : Route 13 | 14 | @Serializable 15 | data object Permissions : Route 16 | } 17 | 18 | @Serializable 19 | data object MetadatorNavigator : Route { 20 | @Serializable 21 | data object Home : Route { 22 | 23 | @Serializable 24 | data object VisualSettings : Route 25 | } 26 | } 27 | 28 | @Serializable 29 | data object MediaplayerNavigator : Route { 30 | @Serializable 31 | data object Mediaplayer : Route 32 | } 33 | 34 | @Serializable 35 | data object UtilitiesNavigator : Route { 36 | @Serializable 37 | data class TagEditor(val selectedSong: ParcelableSong) : Route 38 | } 39 | 40 | @Serializable 41 | data object SettingsNavigator : Route { 42 | @Serializable 43 | data object Settings : Route { 44 | @Serializable 45 | data object General : Route 46 | 47 | @Serializable 48 | data object Appearance : Route 49 | 50 | @Serializable 51 | data object About : Route 52 | } 53 | } 54 | } 55 | 56 | val mainNavigators = listOf( 57 | Route.MetadatorNavigator, 58 | Route.MediaplayerNavigator 59 | ) 60 | 61 | fun Any.qualifiedName(): String = this::class.qualifiedName.toString() -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Shapes.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.presentation.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val AppShapes = Shapes( 8 | extraSmall = RoundedCornerShape(2.dp), 9 | small = RoundedCornerShape(4.dp), 10 | medium = RoundedCornerShape(8.dp), 11 | large = RoundedCornerShape(16.dp), 12 | extraLarge = RoundedCornerShape(32.dp) 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.presentation.theme 2 | 3 | import android.os.Build 4 | import androidx.compose.material3.LocalTextStyle 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.ProvideTextStyle 7 | import androidx.compose.material3.dynamicDarkColorScheme 8 | import androidx.compose.material3.dynamicLightColorScheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.text.style.LineBreak 13 | import androidx.compose.ui.text.style.TextDirection 14 | import com.bobbyesp.metadator.core.presentation.common.LocalDynamicColoringSwitch 15 | import com.bobbyesp.metadator.core.presentation.common.LocalDynamicThemeState 16 | import com.materialkolor.DynamicMaterialTheme 17 | 18 | fun isDynamicColoringSupported(): Boolean { 19 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 20 | } 21 | 22 | val MetadatorLogoForeground = Color(0xFFFFFFF0) 23 | val MetadatorLogoBackground = Color(0xFF313638) 24 | 25 | @Composable 26 | fun MetadatorTheme( 27 | content: @Composable () -> Unit 28 | ) { 29 | val themeState = LocalDynamicThemeState.current 30 | val dynamicColoring = LocalDynamicColoringSwitch.current 31 | val context = LocalContext.current 32 | val canUseDynamicColor = dynamicColoring && isDynamicColoringSupported() 33 | 34 | val dynamicColorScheme = if (canUseDynamicColor) { 35 | if (themeState.isDark) { 36 | dynamicDarkColorScheme(context).let { 37 | if (themeState.isAmoled) it.copy( 38 | surface = Color.Black, 39 | background = Color.Black 40 | ) else it 41 | } 42 | } else { 43 | dynamicLightColorScheme(context) 44 | } 45 | } else null 46 | 47 | ProvideTextStyle( 48 | value = LocalTextStyle.current.copy( 49 | lineBreak = LineBreak.Paragraph, 50 | textDirection = TextDirection.Content 51 | ) 52 | ) { 53 | if (dynamicColorScheme != null) { 54 | MaterialTheme(colorScheme = dynamicColorScheme, shapes = AppShapes, content = content) 55 | } else { 56 | DynamicMaterialTheme( 57 | state = themeState, 58 | animate = true, 59 | shapes = AppShapes, 60 | content = content 61 | ) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.presentation.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/util/Debugging.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.util 2 | 3 | import com.bobbyesp.metadator.BuildConfig 4 | 5 | //execute the code inside if it is a debug release 6 | fun executeIfDebugging(debugOnlyOperation: () -> Unit) { 7 | if (BuildConfig.DEBUG) debugOnlyOperation() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/util/Navigation.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.util 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.navigation.NavHostController 5 | 6 | /** 7 | * Determines whether the navigation controller can navigate back. 8 | * 9 | * This property checks if the current back stack entry's lifecycle state is RESUMED. 10 | * If the current entry is in the RESUMED state, it indicates that it's currently 11 | * visible and interacting with the user, and therefore it's safe to navigate back from it. 12 | * If the current entry is not in the RESUMED state (e.g., it's in CREATED, STARTED, or DESTROYED), 13 | * navigating back might lead to unexpected behavior or UI inconsistencies. 14 | * 15 | * @return `true` if the navigation controller can go back, `false` otherwise. 16 | */ 17 | val NavHostController.canGoBack: Boolean 18 | get() = this.currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED 19 | 20 | /** 21 | * Navigates back to the previous destination in the navigation stack, if possible. 22 | * 23 | * This function checks if the navigation controller can go back using [NavHostController.canGoBack]. 24 | * If it can, it pops the current destination off the stack using [NavHostController.popBackStack], 25 | * effectively navigating to the previous destination. If there's no previous destination to go back to, 26 | * this function does nothing. 27 | * 28 | * @receiver The [NavHostController] instance that manages the navigation stack. 29 | */ 30 | fun NavHostController.navigateBack() { 31 | if (canGoBack) { 32 | popBackStack() 33 | } 34 | } 35 | 36 | /** 37 | * Extension function for NavHostController to navigate to a destination while cleaning up the back stack. 38 | * 39 | * @param T The type of the destination, which must be a subclass of Any. 40 | * @param destination The destination to navigate to. 41 | */ 42 | fun NavHostController.cleanNavigate(destination: T) = navigate(destination) { 43 | // Pop up to the start destination of the graph, saving the state 44 | popUpTo(graph.startDestinationId) { 45 | saveState = true 46 | } 47 | // Launch the destination as a single top instance 48 | launchSingleTop = true 49 | // Restore the state if possible 50 | restoreState = true 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/util/Permissions.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.util 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | 6 | fun getNeededStoragePermissions(): Array { 7 | return when { 8 | Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> arrayOf( 9 | Manifest.permission.READ_EXTERNAL_STORAGE, 10 | Manifest.permission.WRITE_EXTERNAL_STORAGE 11 | ) 12 | 13 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> arrayOf( 14 | Manifest.permission.READ_MEDIA_AUDIO 15 | ) 16 | 17 | else -> arrayOf( 18 | Manifest.permission.READ_EXTERNAL_STORAGE 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/core/util/getAppVersionName.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.core.util 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import com.bobbyesp.metadator.R 7 | 8 | fun Context.getAppVersionName(): String { 9 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 10 | packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)).versionName 11 | } else { 12 | packageManager.getPackageInfo(packageName, 0).versionName 13 | } ?: this.getString(R.string.unknown) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/search/SpotifySearchServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.features.spotify.data.remote.search 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import com.adamratzman.spotify.endpoints.pub.SearchApi 6 | import com.adamratzman.spotify.models.SearchFilter 7 | import com.adamratzman.spotify.models.SpotifySearchResult 8 | import com.adamratzman.spotify.models.Track 9 | import com.bobbyesp.metadator.features.spotify.domain.pagination.TracksPagingSource 10 | import com.bobbyesp.metadator.features.spotify.domain.services.SpotifyService 11 | import com.bobbyesp.metadator.features.spotify.domain.services.search.SpotifySearchService 12 | import org.koin.core.component.KoinComponent 13 | 14 | class SpotifySearchServiceImpl( 15 | private val spotifyService: SpotifyService 16 | ) : SpotifySearchService, KoinComponent { 17 | 18 | override suspend fun search( 19 | query: String, 20 | vararg searchTypes: SearchApi.SearchType, 21 | filters: List 22 | ): SpotifySearchResult { 23 | val api = spotifyService.getSpotifyApi() 24 | return api.search.search(query = query, searchTypes = searchTypes, filters = filters) 25 | } 26 | 27 | override suspend fun searchPaginatedTracks( 28 | query: String, 29 | filters: List 30 | ): Pager { 31 | val api = spotifyService.getSpotifyApi() 32 | return Pager( 33 | config = PagingConfig( 34 | pageSize = 20, 35 | enablePlaceholders = false, 36 | initialLoadSize = 40, 37 | ), 38 | pagingSourceFactory = { 39 | TracksPagingSource( 40 | spotifyApi = api, 41 | query = query, 42 | ) 43 | } 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/features/spotify/di/SpotifyModule.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.features.spotify.di 2 | 3 | import com.bobbyesp.metadator.BuildConfig 4 | import com.bobbyesp.metadator.features.spotify.data.remote.SpotifyServiceImpl 5 | import com.bobbyesp.metadator.features.spotify.data.remote.search.SpotifySearchServiceImpl 6 | import com.bobbyesp.metadator.features.spotify.domain.services.SpotifyService 7 | import com.bobbyesp.metadator.features.spotify.domain.services.search.SpotifySearchService 8 | import org.koin.core.qualifier.named 9 | import org.koin.dsl.module 10 | 11 | val spotifyMainModule = module { 12 | single(named("client_id")) { BuildConfig.CLIENT_ID } 13 | single(named("client_secret")) { BuildConfig.CLIENT_SECRET } 14 | single { SpotifyServiceImpl() } 15 | } 16 | 17 | val spotifyServicesModule = module { 18 | single { SpotifySearchServiceImpl(get()) } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/pagination/TracksPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.features.spotify.domain.pagination 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.adamratzman.spotify.SpotifyAppApi 6 | import com.adamratzman.spotify.models.SearchFilter 7 | import com.adamratzman.spotify.models.Track 8 | 9 | class TracksPagingSource( 10 | private var spotifyApi: SpotifyAppApi, 11 | private var query: String, 12 | private val filters: List = emptyList(), 13 | ) : PagingSource() { 14 | 15 | override suspend fun load(params: LoadParams): LoadResult { 16 | val offset = params.key ?: 0 17 | 18 | return try { 19 | val response = spotifyApi.search.searchTrack( 20 | query = query, 21 | limit = params.loadSize, 22 | offset = offset, 23 | market = null, 24 | filters = filters, 25 | ) 26 | 27 | if (response.isNotEmpty()) { 28 | val tracks = response.items 29 | 30 | LoadResult.Page( 31 | data = tracks, 32 | prevKey = if (offset > 0) offset - params.loadSize else null, 33 | nextKey = if (tracks.isNotEmpty()) offset + params.loadSize else null 34 | ) 35 | } else { 36 | LoadResult.Error(IllegalStateException("No tracks found")) 37 | } 38 | } catch (exception: Exception) { 39 | LoadResult.Error(exception) 40 | } 41 | } 42 | 43 | override fun getRefreshKey(state: PagingState): Int? { 44 | return state.anchorPosition 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/repositories/SearchRepository.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.features.spotify.domain.repositories 2 | 3 | import androidx.paging.Pager 4 | import com.adamratzman.spotify.models.Album 5 | import com.adamratzman.spotify.models.Artist 6 | import com.adamratzman.spotify.models.Playlist 7 | import com.adamratzman.spotify.models.Track 8 | 9 | interface SearchRepository { 10 | suspend fun searchTracks(query: String): Result> 11 | suspend fun searchAlbums(query: String): Result> 12 | suspend fun searchPlaylists(query: String): Result> 13 | suspend fun searchArtists(query: String): Result> 14 | suspend fun searchPaginatedTracks(query: String): Pager 15 | suspend fun searchPaginatedAlbums(query: String): Pager 16 | suspend fun searchPaginatedPlaylists(query: String): Pager 17 | suspend fun searchPaginatedArtists(query: String): Pager 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/SpotifyService.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.features.spotify.domain.services 2 | 3 | import com.adamratzman.spotify.SpotifyAppApi 4 | import com.adamratzman.spotify.models.Token 5 | 6 | interface SpotifyService { 7 | suspend fun getSpotifyApi(): SpotifyAppApi 8 | suspend fun getSpotifyToken(): Token 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/search/SpotifySearchService.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.features.spotify.domain.services.search 2 | 3 | import androidx.paging.Pager 4 | import com.adamratzman.spotify.endpoints.pub.SearchApi 5 | import com.adamratzman.spotify.models.SearchFilter 6 | import com.adamratzman.spotify.models.SpotifySearchResult 7 | import com.adamratzman.spotify.models.Track 8 | 9 | interface SpotifySearchService { 10 | suspend fun search( 11 | query: String, 12 | vararg searchTypes: SearchApi.SearchType, 13 | filters: List 14 | ): SpotifySearchResult 15 | 16 | suspend fun searchPaginatedTracks( 17 | query: String, 18 | filters: List, 19 | ): Pager 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/MediaplayerRouting.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.compose.navigation 5 | import com.bobbyesp.metadator.core.presentation.common.Route 6 | import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerPage 7 | import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel 8 | import com.bobbyesp.ui.motion.animatedComposable 9 | 10 | fun NavGraphBuilder.mediaplayerRouting( 11 | mediaplayerViewModel: MediaplayerViewModel, 12 | onNavigateBack: () -> Unit 13 | ) { 14 | navigation( 15 | startDestination = Route.MediaplayerNavigator.Mediaplayer, 16 | ) { 17 | animatedComposable { 18 | MediaplayerPage(mediaplayerViewModel) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/data/local/MediaplayerServiceConnection.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.data.local 2 | 3 | import android.content.ComponentName 4 | import android.content.ServiceConnection 5 | import android.os.IBinder 6 | import androidx.annotation.OptIn 7 | import androidx.media3.common.util.UnstableApi 8 | import com.bobbyesp.mediaplayer.service.ConnectionHandler 9 | import com.bobbyesp.mediaplayer.service.MediaplayerService 10 | import com.bobbyesp.utilities.Logging 11 | 12 | @OptIn(UnstableApi::class) 13 | class MediaplayerServiceConnection( 14 | private val connectionHandler: ConnectionHandler 15 | ) : ServiceConnection { 16 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 17 | Logging.i( 18 | "The Music Service is connected. Updating the connection handler." 19 | ) 20 | val binder = service as MediaplayerService.MusicBinder 21 | connectionHandler.connect(binder.service.mediaServiceHandler) 22 | } 23 | 24 | override fun onServiceDisconnected(name: ComponentName?) { 25 | Logging.i( 26 | "The Music Service has been disconnected. Detaching the connection handler." 27 | ) 28 | connectionHandler.disconnect() 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/di/MediaplayerViewModelsModule.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.di 2 | 3 | import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel 4 | import org.koin.core.module.dsl.viewModel 5 | import org.koin.dsl.module 6 | 7 | val mediaplayerViewModels = module { 8 | viewModel { MediaplayerViewModel(get(), get(), get(), get()) } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/RepeatStateIcon.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.components.others 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.rounded.Repeat 5 | import androidx.compose.material.icons.rounded.RepeatOne 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.alpha 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.media3.common.Player.REPEAT_MODE_ALL 12 | import androidx.media3.common.Player.REPEAT_MODE_OFF 13 | import androidx.media3.common.Player.REPEAT_MODE_ONE 14 | import com.bobbyesp.mediaplayer.R 15 | 16 | @Composable 17 | fun RepeatStateIcon( 18 | modifier: Modifier = Modifier, 19 | repeatMode: Int 20 | ) { 21 | when (repeatMode) { 22 | REPEAT_MODE_OFF -> { 23 | Icon( 24 | imageVector = Icons.Rounded.Repeat, 25 | contentDescription = stringResource(id = R.string.repeat_mode_off), 26 | modifier = modifier.alpha(0.5f) 27 | ) 28 | } 29 | 30 | REPEAT_MODE_ONE -> { 31 | Icon( 32 | imageVector = Icons.Rounded.RepeatOne, 33 | contentDescription = stringResource(id = R.string.repeat_mode_one), 34 | modifier = modifier 35 | ) 36 | } 37 | 38 | REPEAT_MODE_ALL -> { 39 | Icon( 40 | imageVector = Icons.Rounded.Repeat, 41 | contentDescription = stringResource(id = R.string.repeat_mode_all), 42 | modifier = modifier 43 | ) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/ShuffleStateIcon.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.components.others 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.rounded.Shuffle 5 | import androidx.compose.material.icons.rounded.ShuffleOn 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import com.bobbyesp.mediaplayer.R 11 | 12 | @Composable 13 | fun ShuffleStateIcon( 14 | modifier: Modifier = Modifier, 15 | isShuffleEnabled: Boolean 16 | ) { 17 | when (isShuffleEnabled) { 18 | true -> { 19 | Icon( 20 | imageVector = Icons.Rounded.ShuffleOn, 21 | contentDescription = stringResource(id = R.string.action_shuffle_on), 22 | modifier = modifier 23 | ) 24 | } 25 | 26 | false -> { 27 | Icon( 28 | imageVector = Icons.Rounded.Shuffle, 29 | contentDescription = stringResource(id = R.string.action_shuffle_off), 30 | modifier = modifier 31 | ) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerConstants.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player 2 | 3 | import androidx.compose.animation.ContentTransform 4 | import androidx.compose.animation.SizeTransform 5 | import androidx.compose.animation.core.AnimationSpec 6 | import androidx.compose.animation.core.EaseInOutSine 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.ui.unit.Dp 9 | import androidx.compose.ui.unit.dp 10 | import com.bobbyesp.ui.motion.materialSharedAxisXIn 11 | import com.bobbyesp.ui.motion.materialSharedAxisXOut 12 | 13 | val CollapsedPlayerHeight = 84.dp 14 | val SeekToButtonSize = 48.dp 15 | val PlayerCommandsButtonSize = 48.dp 16 | 17 | val PlayerAnimationSpec: AnimationSpec = tween( 18 | durationMillis = 750, 19 | delayMillis = 0, 20 | easing = EaseInOutSine 21 | ) 22 | 23 | val AnimatedTextContentTransformation = ContentTransform( 24 | materialSharedAxisXIn(initialOffsetX = { it / 10 }), 25 | materialSharedAxisXOut(targetOffsetX = { -it / 10 }), 26 | sizeTransform = SizeTransform(clip = false) 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheet.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.ui.Modifier 7 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 8 | import com.bobbyesp.mediaplayer.service.ConnectionState 9 | import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel 10 | import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views.MediaplayerCollapsedContent 11 | import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views.MediaplayerExpandedContent 12 | import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheet 13 | import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheetState 14 | import kotlinx.coroutines.launch 15 | 16 | @Composable 17 | fun MediaplayerSheet( 18 | modifier: Modifier = Modifier, state: DraggableBottomSheetState, viewModel: MediaplayerViewModel 19 | ) { 20 | val playingSong = 21 | viewModel.songBeingPlayed.collectAsStateWithLifecycle().value?.mediaMetadata ?: return 22 | val connectionState = 23 | viewModel.connectionHandler.connectionState.collectAsStateWithLifecycle().value 24 | 25 | LaunchedEffect(connectionState, Unit) { 26 | if (connectionState is ConnectionState.Connected && state.isDismissed) { 27 | launch { 28 | state.collapseSoft() 29 | } 30 | } 31 | } 32 | 33 | DraggableBottomSheet(modifier = modifier, state = state, collapsedContent = { 34 | MediaplayerCollapsedContent( 35 | viewModel = viewModel, nowPlaying = playingSong 36 | ) 37 | }, backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh, onDismiss = { 38 | viewModel.dismissPlayer() 39 | }) { 40 | MediaplayerExpandedContent( 41 | viewModel = viewModel, 42 | sheetState = state, 43 | ) 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheetView.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player 2 | 3 | enum class MediaplayerSheetView { 4 | FULL_PLAYER, 5 | QUEUE 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerOptions.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.navigationBarsPadding 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.automirrored.rounded.QueueMusic 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.IconButton 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import com.bobbyesp.metadator.R 22 | 23 | @Composable 24 | fun PlayerOptions( 25 | modifier: Modifier = Modifier, 26 | onOpenQueue: () -> Unit = {} 27 | ) { 28 | Box( 29 | modifier = modifier 30 | .clip( 31 | RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) 32 | ) 33 | .background(MaterialTheme.colorScheme.surfaceContainerHigh), 34 | contentAlignment = Alignment.Center 35 | ) { 36 | Row( 37 | modifier = Modifier 38 | .navigationBarsPadding() 39 | .padding(vertical = 8.dp), 40 | verticalAlignment = Alignment.CenterVertically, 41 | horizontalArrangement = Arrangement.Center 42 | ) { 43 | IconButton(onClick = onOpenQueue) { 44 | Icon( 45 | imageVector = Icons.AutoMirrored.Rounded.QueueMusic, 46 | contentDescription = stringResource(id = R.string.music_queue) 47 | ) 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/PlayerQueue.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.unit.dp 13 | import androidx.media3.common.MediaMetadata 14 | import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage 15 | 16 | @Composable 17 | fun PlayerQueue( 18 | modifier: Modifier = Modifier, 19 | imageModifier: Modifier, 20 | nowPlaying: MediaMetadata?, 21 | queue: List, 22 | onPlay: (MediaMetadata) -> Unit = {}, 23 | onBackPressed: () -> Unit = {} 24 | ) { 25 | BackHandler { 26 | onBackPressed() 27 | } 28 | 29 | Column { 30 | Box( 31 | modifier = modifier 32 | .fillMaxWidth() 33 | .padding(24.dp) 34 | ) { 35 | AsyncImage( 36 | imageModel = nowPlaying?.artworkUri, 37 | modifier = imageModifier 38 | .clip(MaterialTheme.shapes.small) 39 | ) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediastore/di/MediaStoreViewModelsModule.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediastore.di 2 | 3 | import com.bobbyesp.metadator.mediastore.presentation.MediaStorePageViewModel 4 | import org.koin.core.module.dsl.viewModel 5 | import org.koin.dsl.module 6 | 7 | val mediaStoreViewModelsModule = module { 8 | viewModel { MediaStorePageViewModel(get()) } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/CompactCardSize.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediastore.domain.enums 2 | 3 | import androidx.compose.foundation.shape.CornerBasedShape 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.dp 8 | 9 | enum class CompactCardSize(val value: Dp) { 10 | SMALL(96.dp), 11 | MEDIUM(120.dp), 12 | LARGE(144.dp), 13 | EXTRA_LARGE(168.dp); 14 | 15 | companion object { 16 | 17 | fun Int.toCompactCardSize(): CompactCardSize = 18 | CompactCardSize.entries.first { it.ordinal == this } 19 | 20 | @Composable 21 | fun CompactCardSize.toShape(): CornerBasedShape = when (this) { 22 | SMALL -> MaterialTheme.shapes.small 23 | MEDIUM -> MaterialTheme.shapes.medium 24 | LARGE -> MaterialTheme.shapes.large 25 | EXTRA_LARGE -> MaterialTheme.shapes.extraLarge 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/LayoutType.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediastore.domain.enums 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.rounded.List 5 | import androidx.compose.material.icons.rounded.GridView 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | 8 | enum class LayoutType(val icon: ImageVector) { 9 | Grid(icon = Icons.Rounded.GridView), 10 | List(icon = Icons.AutoMirrored.Rounded.List); 11 | 12 | companion object { 13 | fun Int.toListType(): LayoutType = LayoutType.entries.first { it.ordinal == this } 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePageViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.mediastore.presentation 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.bobbyesp.utilities.mediastore.MediaStoreReceiver.Advanced.observeSongs 7 | import com.bobbyesp.utilities.mediastore.model.Song 8 | import com.bobbyesp.utilities.states.ResourceState 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.flow.collectLatest 13 | import kotlinx.coroutines.flow.update 14 | import kotlinx.coroutines.launch 15 | 16 | class MediaStorePageViewModel( 17 | context: Context 18 | ) : ViewModel() { 19 | private val _songs: MutableStateFlow>> = 20 | MutableStateFlow(ResourceState.Loading()) 21 | val songs = _songs.asStateFlow() 22 | 23 | private val mediaStoreSongsFlow = 24 | context.contentResolver.observeSongs() 25 | 26 | private fun songsCollection() { 27 | viewModelScope.launch(Dispatchers.IO) { 28 | mediaStoreSongsFlow.collectLatest { songs -> 29 | _songs.update { ResourceState.Success(songs) } 30 | } 31 | } 32 | } 33 | 34 | private fun reloadMediaStore() { 35 | _songs.update { ResourceState.Loading() } 36 | songsCollection() 37 | } 38 | 39 | fun onEvent(event: Events) { 40 | when (event) { 41 | is Events.StartObservingMediaStore -> songsCollection() 42 | 43 | is Events.ReloadMediaStore -> reloadMediaStore() 44 | } 45 | } 46 | 47 | companion object { 48 | interface Events { 49 | data object StartObservingMediaStore : Events 50 | data object ReloadMediaStore : Events 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/onboarding/OnboardingRouting.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.onboarding 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | import androidx.navigation.NavGraphBuilder 7 | import androidx.navigation.compose.navigation 8 | import com.bobbyesp.metadator.core.presentation.common.Route 9 | import com.bobbyesp.metadator.core.util.getNeededStoragePermissions 10 | import com.bobbyesp.metadator.onboarding.presentation.pages.OnboardingPermissionsPage 11 | import com.bobbyesp.metadator.onboarding.presentation.pages.OnboardingWelcomePage 12 | import com.bobbyesp.ui.motion.animatedComposable 13 | import com.bobbyesp.utilities.ui.permission.PermissionType.Companion.toPermissionType 14 | 15 | fun NavGraphBuilder.onboardingRouting( 16 | onNavigate: (Route) -> Unit, 17 | onCompletedOnboarding: () -> Unit 18 | ) { 19 | navigation( 20 | startDestination = Route.OnboardingNavigator.Welcome::class, 21 | ) { 22 | animatedComposable { 23 | OnboardingWelcomePage( 24 | onGetStarted = { 25 | onNavigate(Route.OnboardingNavigator.Permissions) 26 | } 27 | ) 28 | } 29 | 30 | animatedComposable { 31 | 32 | val neededPermissions by remember { mutableStateOf(getNeededStoragePermissions().map { it.toPermissionType() }) } 33 | 34 | OnboardingPermissionsPage( 35 | neededPermissions = neededPermissions, 36 | onNextClick = { 37 | onCompletedOnboarding() 38 | } 39 | ) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/onboarding/domain/PermissionItem.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.onboarding.domain 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | import com.bobbyesp.utilities.ui.permission.PermissionType 6 | 7 | @Stable 8 | data class PermissionItem( 9 | val permission: PermissionType, 10 | val icon: ImageVector, 11 | val isGranted: Boolean, 12 | val onClick: () -> Unit 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorViewModelsModule.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.tageditor.di 2 | 3 | import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.MetadataEditorViewModel 4 | import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.core.module.dsl.viewModel 7 | import org.koin.dsl.module 8 | 9 | val tagEditorViewModelsModule = module { 10 | viewModel { 11 | MetadataEditorViewModel( 12 | context = androidContext(), 13 | stateHandle = get() 14 | ) 15 | } 16 | viewModel { 17 | MetadataBottomSheetViewModel( 18 | searchService = get() 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MediaStoreInfoDialog.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material3.AlertDialog 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TextButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import com.bobbyesp.metadator.R 11 | 12 | @Composable 13 | fun MediaStoreInfoDialog( 14 | modifier: Modifier = Modifier, 15 | onDismissRequest: () -> Unit, 16 | ) { 17 | AlertDialog( 18 | modifier = modifier, 19 | onDismissRequest = onDismissRequest, 20 | text = { 21 | Column { 22 | } 23 | }, 24 | confirmButton = {}, 25 | dismissButton = { 26 | TextButton( 27 | onClick = onDismissRequest 28 | ) { 29 | Text(stringResource(id = R.string.dismiss)) 30 | } 31 | } 32 | ) 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/SongSyncNeededDialog.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Lyrics 6 | import androidx.compose.material3.AlertDialog 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.LocalUriHandler 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.buildAnnotatedString 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import com.bobbyesp.utilities.R 18 | 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | @Composable 21 | fun SongSyncNeededDialog( 22 | modifier: Modifier = Modifier, 23 | onDismissRequest: () -> Unit, 24 | ) { 25 | val uriLauncher = LocalUriHandler.current 26 | AlertDialog( 27 | icon = { 28 | Icon( 29 | imageVector = Icons.Filled.Lyrics, 30 | contentDescription = "SongSync app needed" 31 | ) 32 | }, modifier = modifier, onDismissRequest = onDismissRequest, 33 | title = { 34 | Text(text = stringResource(id = R.string.song_sync_needed)) 35 | }, text = { 36 | Text( 37 | text = buildAnnotatedString { 38 | append(stringResource(id = R.string.song_sync_needed_desc)) 39 | append(" \n") 40 | append(stringResource(id = R.string.song_sync_not_installed)) 41 | }, 42 | ) 43 | }, confirmButton = { 44 | TextButton( 45 | onClick = { 46 | uriLauncher.openUri("https://github.com/Lambada10/SongSync/releases/latest") 47 | } 48 | ) { 49 | Text(stringResource(id = R.string.download)) 50 | } 51 | }, dismissButton = { 52 | TextButton( 53 | onClick = onDismissRequest 54 | ) { 55 | Text(stringResource(id = R.string.dismiss)) 56 | } 57 | } 58 | ) 59 | } 60 | 61 | @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) 62 | @Composable 63 | fun SongSyncNeededDialogPreview() { 64 | SongSyncNeededDialog(onDismissRequest = { }) 65 | } -------------------------------------------------------------------------------- /app/src/main/play_store_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/play_store_512.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/metadator_logo_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/metadator_logo_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/playstore/kotlin/FirebaseSetup.kt: -------------------------------------------------------------------------------- 1 | import com.bobbyesp.metadator.App 2 | import com.google.firebase.Firebase 3 | import com.google.firebase.crashlytics.crashlytics 4 | import com.google.firebase.initialize 5 | 6 | fun App.initializeFirebase() { 7 | Firebase.initialize(this) 8 | } 9 | 10 | /** 11 | * Extension function for MainActivity to enable Crashlytics collection. 12 | * 13 | * This function sets the Crashlytics collection to be enabled, allowing Firebase Crashlytics 14 | * to collect crash reports for the application. 15 | */ 16 | fun setCrashlyticsCollection() { 17 | Firebase.crashlytics.setCrashlyticsCollectionEnabled(true) 18 | } -------------------------------------------------------------------------------- /app/src/test/java/com/bobbyesp/metadator/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.metadator 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /app/ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.android.kotlin) 4 | alias(libs.plugins.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "com.bobbyesp.ui" 9 | compileSdk = 35 10 | 11 | defaultConfig { 12 | minSdk = 24 13 | 14 | vectorDrawables { 15 | useSupportLibrary = true 16 | } 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles("consumer-rules.pro") 20 | } 21 | 22 | buildTypes { 23 | release { 24 | isMinifyEnabled = false 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), 27 | "proguard-rules.pro" 28 | ) 29 | packaging { 30 | resources.excludes.add("META-INF/*.kotlin_module") 31 | } 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_21 36 | targetCompatibility = JavaVersion.VERSION_21 37 | } 38 | kotlinOptions { 39 | jvmTarget = "21" 40 | } 41 | buildFeatures { 42 | compose = true 43 | } 44 | composeCompiler { 45 | reportsDestination = layout.buildDirectory.dir("compose_compiler") 46 | } 47 | packaging { 48 | resources { 49 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 50 | } 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(libs.core.ktx) 57 | implementation(libs.bundles.compose) 58 | implementation(libs.compose.tooling.preview) 59 | implementation(libs.materialKolor) 60 | implementation(libs.scrollbar) 61 | 62 | //Compose testing and tooling libraries 63 | androidTestImplementation(platform(libs.compose.bom)) 64 | androidTestImplementation(libs.compose.test.junit4) 65 | debugImplementation(libs.compose.tooling) 66 | debugImplementation(libs.compose.test.manifest) 67 | } -------------------------------------------------------------------------------- /app/ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/ui/consumer-rules.pro -------------------------------------------------------------------------------- /app/ui/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/ui/src/androidTest/java/com/bobbyesp/ui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.bobbyesp.ui.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/common/pages/IdlePage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.common.pages 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun IdlePage() { 12 | Box( 13 | modifier = Modifier.fillMaxSize(), 14 | contentAlignment = Alignment.Center 15 | ) { 16 | Text(text = "Idle Page") 17 | } 18 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.common.pages 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.safeDrawingPadding 9 | import androidx.compose.material3.LinearProgressIndicator 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun LoadingPage( 20 | modifier: Modifier = Modifier, 21 | text: String 22 | ) { 23 | Box( 24 | modifier = modifier 25 | .fillMaxSize() 26 | .safeDrawingPadding(), 27 | contentAlignment = Alignment.Center 28 | ) { 29 | Column( 30 | modifier = Modifier.fillMaxWidth(), 31 | verticalArrangement = Arrangement.spacedBy(8.dp), 32 | horizontalAlignment = Alignment.CenterHorizontally 33 | ) { 34 | Text( 35 | text = text, 36 | style = MaterialTheme.typography.bodyMedium, 37 | fontWeight = FontWeight.SemiBold 38 | ) 39 | LinearProgressIndicator( 40 | modifier = Modifier.fillMaxWidth(0.7f) 41 | ) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/animatable/rememberAnimatable.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.animatable 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.AnimationVector1D 5 | import androidx.compose.animation.core.Spring 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.saveable.rememberSaveable 8 | import com.bobbyesp.ui.util.AnimatableSaver 9 | 10 | @Composable 11 | fun rememberAnimatable( 12 | initialValue: Float, 13 | visibilityThreshold: Float = Spring.DefaultDisplacementThreshold 14 | ): Animatable { 15 | return rememberSaveable( 16 | saver = AnimatableSaver 17 | ) { 18 | Animatable(initialValue, visibilityThreshold) 19 | } 20 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetAnchor.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.bottomsheet.draggable 2 | 3 | enum class DraggableBottomSheetAnchor { 4 | DISMISSED, 5 | COLLAPSED, 6 | EXPANDED 7 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/button/BackButton.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.button 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 5 | import androidx.compose.material.icons.filled.Close 6 | import androidx.compose.material3.FilledTonalIconButton 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import com.bobbyesp.ui.R 13 | 14 | @Composable 15 | fun BackButton(onClick: () -> Unit) { 16 | IconButton(modifier = Modifier, onClick = onClick) { 17 | Icon( 18 | imageVector = Icons.AutoMirrored.Filled.ArrowBack, 19 | contentDescription = stringResource(R.string.back), 20 | ) 21 | } 22 | } 23 | 24 | @Composable 25 | fun CloseButton(onClick: () -> Unit) { 26 | IconButton(modifier = Modifier, onClick = onClick) { 27 | Icon( 28 | imageVector = Icons.Default.Close, 29 | contentDescription = stringResource(R.string.back), 30 | ) 31 | } 32 | } 33 | 34 | @Composable 35 | fun DynamicButton( 36 | modifier: Modifier = Modifier, 37 | icon: @Composable () -> Unit, 38 | icon2: @Composable () -> Unit, 39 | isIcon1: Boolean, 40 | onClick: () -> Unit 41 | ) { 42 | FilledTonalIconButton(modifier = modifier, onClick = onClick) { 43 | if (isIcon1) { 44 | icon() 45 | } else { 46 | icon2() 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/button/FilledButtons.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.button 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Button 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.FilledTonalButton 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.vector.ImageVector 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun FilledButtonWithIcon( 17 | modifier: Modifier = Modifier, 18 | onClick: () -> Unit, 19 | icon: ImageVector, 20 | enabled: Boolean = true, 21 | text: String, 22 | contentDescription: String? = null 23 | ) { 24 | Button( 25 | modifier = modifier, 26 | onClick = onClick, 27 | enabled = enabled, 28 | contentPadding = ButtonDefaults.ButtonWithIconContentPadding 29 | ) { 30 | Icon( 31 | modifier = Modifier.size(18.dp), 32 | imageVector = icon, 33 | contentDescription = contentDescription 34 | ) 35 | Text( 36 | modifier = Modifier.padding(start = 6.dp), 37 | text = text 38 | ) 39 | } 40 | } 41 | 42 | @Composable 43 | fun FilledTonalButtonWithIcon( 44 | modifier: Modifier = Modifier, 45 | onClick: () -> Unit, 46 | icon: ImageVector, 47 | text: String, 48 | contentDescription: String? = null 49 | ) { 50 | FilledTonalButton( 51 | modifier = modifier, 52 | onClick = onClick, 53 | contentPadding = ButtonDefaults.ButtonWithIconContentPadding 54 | ) { 55 | Icon( 56 | modifier = Modifier.size(18.dp), 57 | imageVector = icon, 58 | contentDescription = contentDescription 59 | ) 60 | Text( 61 | modifier = Modifier.padding(start = 8.dp), 62 | text = text 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/button/OutlinedButtons.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.button 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.ButtonDefaults 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.OutlinedButton 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | fun OutlinedButtonWithIcon( 18 | modifier: Modifier = Modifier, 19 | onClick: () -> Unit, 20 | icon: ImageVector, 21 | text: String, 22 | contentColor: Color = MaterialTheme.colorScheme.primary, 23 | contentDescription: String? = null 24 | ) { 25 | OutlinedButton( 26 | modifier = modifier, 27 | onClick = onClick, 28 | contentPadding = ButtonDefaults.ButtonWithIconContentPadding, 29 | colors = ButtonDefaults.outlinedButtonColors(contentColor = contentColor) 30 | ) { 31 | Icon( 32 | modifier = Modifier.size(18.dp), 33 | imageVector = icon, 34 | contentDescription = contentDescription 35 | ) 36 | Text( 37 | modifier = Modifier.padding(start = 8.dp), 38 | text = text 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/chip/SingleChoiceChip.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.chip 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.outlined.Check 9 | import androidx.compose.material3.FilterChip 10 | import androidx.compose.material3.FilterChipDefaults 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.vector.ImageVector 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun SingleChoiceChip( 20 | modifier: Modifier = Modifier, 21 | selected: Boolean, 22 | onClick: () -> Unit, 23 | label: String, 24 | leadingIcon: ImageVector = Icons.Outlined.Check 25 | ) { 26 | FilterChip( 27 | modifier = modifier.padding(horizontal = 4.dp), 28 | selected = selected, 29 | onClick = onClick, 30 | label = { 31 | Text(text = label) 32 | }, 33 | leadingIcon = { 34 | Row { 35 | AnimatedVisibility(visible = selected, modifier = Modifier) { 36 | Icon( 37 | imageVector = leadingIcon, 38 | contentDescription = null, 39 | modifier = Modifier.size(FilterChipDefaults.IconSize) 40 | ) 41 | } 42 | } 43 | }, 44 | ) 45 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownItemContainer.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.dropdown 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.sizeIn 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun DropdownItemContainer( 15 | modifier: Modifier = Modifier, 16 | contentPadding: androidx.compose.ui.unit.Dp = 0.dp, 17 | content: @Composable RowScope.() -> Unit 18 | ) { 19 | Row( 20 | modifier = modifier 21 | .fillMaxWidth() 22 | // Preferred min and max width used during the intrinsic measurement. 23 | .sizeIn( 24 | minWidth = DropdownMenuItemDefaultMinWidth, 25 | maxWidth = DropdownMenuItemDefaultMaxWidth, 26 | minHeight = ListItemContainerHeight 27 | ) 28 | .padding(contentPadding) 29 | .padding(horizontal = DropdownMenuItemHorizontalPadding), 30 | verticalAlignment = Alignment.CenterVertically 31 | ) { 32 | content() 33 | } 34 | } 35 | 36 | private val DropdownMenuItemHorizontalPadding = 12.dp 37 | private val DropdownMenuItemDefaultMinWidth = 112.dp 38 | private val DropdownMenuItemDefaultMaxWidth = 280.dp 39 | private val ListItemContainerHeight = 48.0.dp -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/M3ElevationTokens.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.dropdown 2 | 3 | /** 4 | * The tonal elevation tokens. 5 | * 6 | * @see androidx.compose.material3.tokens.ElevationTokens 7 | */ 8 | internal object ElevationTokens { 9 | const val Level0 = 0 10 | const val Level1 = 1 11 | const val Level2 = 3 12 | const val Level3 = 6 13 | const val Level4 = 8 14 | const val Level5 = 12 15 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/image/ProfilePictureGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.image 2 | 3 | import android.content.res.Configuration.UI_MODE_NIGHT_YES 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.foundation.shape.CornerBasedShape 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | 23 | @Composable 24 | fun ProfilePicture( 25 | modifier: Modifier = Modifier, 26 | size: Int = 40, 27 | name: String, 28 | shape: CornerBasedShape = CircleShape, 29 | surfaceColor: Color = MaterialTheme.colorScheme.primary, 30 | onClick: () -> Unit = {} 31 | ) { 32 | val firstLetter = name.first().toString() 33 | Surface( 34 | modifier = modifier.size(size.dp), 35 | shape = shape, 36 | onClick = onClick, 37 | color = surfaceColor, 38 | ) { 39 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 40 | Text( 41 | fontSize = (size / 1.5).sp, 42 | text = firstLetter, 43 | fontWeight = FontWeight.SemiBold, 44 | textAlign = TextAlign.Center, 45 | ) 46 | } 47 | } 48 | } 49 | 50 | @Preview 51 | @Preview(uiMode = UI_MODE_NIGHT_YES) 52 | @Composable 53 | private fun ProfilePicturePreview() { 54 | ProfilePicture( 55 | name = "Bobby", 56 | onClick = {}, 57 | shape = RoundedCornerShape(1f), 58 | modifier = Modifier, 59 | size = 40, 60 | ) 61 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/others/AdditionalInformation.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.others 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.Info 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.text.AnnotatedString 15 | import androidx.compose.ui.text.font.FontFamily 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import com.bobbyesp.ui.R 19 | 20 | @Composable 21 | fun AdditionalInformation( 22 | modifier: Modifier = Modifier, 23 | text: AnnotatedString 24 | ) { 25 | Column( 26 | modifier = modifier, 27 | verticalArrangement = Arrangement.spacedBy(8.dp) 28 | ) { 29 | Icon( 30 | imageVector = Icons.Rounded.Info, 31 | tint = MaterialTheme.colorScheme.onSurfaceVariant, 32 | contentDescription = stringResource(id = R.string.additional_information), 33 | ) 34 | 35 | Text( 36 | text = text, 37 | color = MaterialTheme.colorScheme.onSurfaceVariant, 38 | ) 39 | } 40 | } 41 | 42 | @Composable 43 | fun AdditionalInformation( 44 | modifier: Modifier = Modifier, 45 | text: String, 46 | fontFamily: FontFamily = FontFamily.Default 47 | ) { 48 | Column( 49 | modifier = modifier, 50 | verticalArrangement = Arrangement.spacedBy(8.dp) 51 | ) { 52 | Icon( 53 | modifier = Modifier.size(32.dp), 54 | imageVector = Icons.Rounded.Info, 55 | tint = MaterialTheme.colorScheme.onSurfaceVariant, 56 | contentDescription = stringResource(id = R.string.additional_information), 57 | ) 58 | 59 | Text( 60 | text = text, 61 | style = MaterialTheme.typography.bodyMedium, 62 | color = MaterialTheme.colorScheme.onSurfaceVariant, 63 | fontFamily = fontFamily 64 | ) 65 | } 66 | } 67 | 68 | @Preview 69 | @Composable 70 | private fun Preview() { 71 | MaterialTheme { 72 | AdditionalInformation( 73 | text = "This is a preview text preview text preview text preview text preview text preview text " + 74 | "preview text preview text" 75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/others/LoadingPlaceholder.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.others 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun LoadingPlaceholder( 14 | modifier: Modifier = Modifier, 15 | progress: Float? = null, 16 | colorful: Boolean, 17 | ) { 18 | 19 | val color = 20 | if (colorful) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface 21 | val onColor = 22 | if (colorful) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface 23 | val elevation = if (colorful) 0.dp else 8.dp 24 | 25 | Surface( 26 | tonalElevation = elevation, 27 | color = color, 28 | modifier = modifier 29 | ) { 30 | if (progress == null) { 31 | CircularProgressIndicator( 32 | modifier = Modifier 33 | .fillMaxSize() 34 | .padding(8.dp), 35 | color = onColor, 36 | ) 37 | } else { 38 | CircularProgressIndicator( 39 | progress = { progress }, 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .padding(8.dp), 43 | color = onColor, 44 | ) 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/others/MetadataTag.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.others 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.alpha 11 | import androidx.compose.ui.platform.LocalClipboardManager 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.text.AnnotatedString 14 | import androidx.compose.ui.text.style.TextAlign 15 | import androidx.compose.ui.unit.sp 16 | import com.bobbyesp.ui.R 17 | 18 | @Composable 19 | fun MetadataTag( 20 | modifier: Modifier = Modifier, 21 | typeOfMetadata: String, 22 | metadata: String = "Unknown", 23 | ) { 24 | val context = LocalContext.current 25 | val clipboardManager = LocalClipboardManager.current 26 | 27 | Column( 28 | modifier = modifier.clickable { 29 | clipboardManager.setText(AnnotatedString(metadata)) 30 | Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() 31 | } 32 | ) { 33 | Text( 34 | text = typeOfMetadata, 35 | modifier = Modifier.alpha(alpha = 0.8f), 36 | style = MaterialTheme.typography.labelSmall, 37 | textAlign = TextAlign.Start 38 | ) 39 | Text( 40 | modifier = Modifier, 41 | text = metadata, 42 | style = MaterialTheme.typography.titleLarge.copy(fontSize = 16.sp), 43 | textAlign = TextAlign.Start 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOption.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.bobbyesp.ui.components.preferences 3 | 4 | sealed class SettingOption( 5 | val title: String, 6 | val onSelection: () -> Unit, 7 | ) 8 | -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOptionsRow.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.preferences 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.ShapeDefaults 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.unit.dp 18 | 19 | @Composable 20 | fun SettingOptionsRow( 21 | title: String, 22 | options: List, 23 | modifier: Modifier = Modifier, 24 | optionContent: @Composable (T) -> Unit 25 | ) { 26 | Column( 27 | modifier = modifier 28 | .clip(ShapeDefaults.ExtraLarge) 29 | .background(color = MaterialTheme.colorScheme.surfaceContainer) 30 | .padding(vertical = 16.dp) 31 | ) { 32 | Text( 33 | text = title, 34 | style = MaterialTheme.typography.titleMedium, 35 | color = MaterialTheme.colorScheme.onSurface, 36 | modifier = Modifier.padding(start = 20.dp) 37 | ) 38 | 39 | Spacer(modifier = Modifier.height(8.dp)) 40 | 41 | LazyRow { 42 | item { 43 | Spacer(modifier = Modifier.width(8.dp)) 44 | } 45 | items( 46 | items = options, 47 | key = { it.title } 48 | ) { option -> 49 | optionContent(option) 50 | } 51 | item { 52 | Spacer(modifier = Modifier.width(8.dp)) 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOption.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.preferences 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | data class SettingSegmentOption( 6 | val icon: ImageVector, 7 | val contentDescription: String, 8 | val onClick: () -> Unit 9 | ) -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSlider.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.bobbyesp.ui.components.preferences 3 | 4 | import androidx.annotation.IntRange 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Slider 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun SettingSlider( 20 | modifier: Modifier = Modifier, 21 | title: String, 22 | value: Float, 23 | onValueChange: (Float) -> Unit, 24 | onValueChangeFinished: () -> Unit, 25 | valueToShow: String? = null, 26 | @IntRange steps: Int = 0, 27 | valueRange: ClosedFloatingPointRange = 0f..1f 28 | ) { 29 | Column( 30 | modifier = modifier 31 | ) { 32 | Row( 33 | modifier = Modifier.fillMaxWidth(), 34 | horizontalArrangement = Arrangement.SpaceBetween 35 | ) { 36 | Text( 37 | text = title, 38 | style = MaterialTheme.typography.titleMedium, 39 | color = MaterialTheme.colorScheme.onSurface 40 | ) 41 | 42 | Text( 43 | text = "${valueToShow ?: value.toInt()}", 44 | style = MaterialTheme.typography.titleMedium, 45 | color = MaterialTheme.colorScheme.onSurface 46 | ) 47 | } 48 | 49 | Spacer(modifier = Modifier.height(4.dp)) 50 | 51 | Slider( 52 | value = value, 53 | onValueChange = onValueChange, 54 | onValueChangeFinished = onValueChangeFinished, 55 | steps = steps, 56 | valueRange = valueRange, 57 | modifier = Modifier.fillMaxWidth() 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSwitch.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.preferences 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.ShapeDefaults 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.SwitchDefaults 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.vector.ImageVector 21 | import androidx.compose.ui.unit.dp 22 | 23 | @Composable 24 | fun SettingSwitch( 25 | title: String, 26 | supportingText: String, 27 | icon: ImageVector, 28 | isChecked: Boolean, 29 | onCheckedChange: (Boolean) -> Unit, 30 | modifier: Modifier = Modifier 31 | ) { 32 | Row( 33 | modifier = modifier 34 | .clip(ShapeDefaults.Large) 35 | .clickable( 36 | interactionSource = remember { MutableInteractionSource() }, 37 | indication = null 38 | ) { 39 | onCheckedChange(!isChecked) 40 | }, 41 | verticalAlignment = Alignment.CenterVertically 42 | ) { 43 | Icon( 44 | imageVector = icon, 45 | tint = MaterialTheme.colorScheme.onSurface, 46 | contentDescription = null 47 | ) 48 | 49 | Column( 50 | modifier = Modifier 51 | .weight(1f) 52 | .padding(horizontal = 16.dp) 53 | ) { 54 | Text( 55 | text = title, 56 | style = MaterialTheme.typography.titleMedium, 57 | color = MaterialTheme.colorScheme.onSurface 58 | ) 59 | 60 | Text( 61 | text = supportingText, 62 | style = MaterialTheme.typography.bodyMedium, 63 | color = MaterialTheme.colorScheme.onSurfaceVariant 64 | ) 65 | } 66 | 67 | Switch( 68 | checked = isChecked, 69 | onCheckedChange = onCheckedChange, 70 | colors = SwitchDefaults.colors( 71 | uncheckedBorderColor = Color.Transparent, 72 | ) 73 | ) 74 | } 75 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/state/LoadingState.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.material3.LinearProgressIndicator 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | fun LoadingState(text: String, modifier: Modifier = Modifier) { 18 | Column( 19 | modifier = modifier.fillMaxWidth(), 20 | verticalArrangement = Arrangement.spacedBy( 21 | 8.dp, 22 | Alignment.CenterVertically 23 | ), 24 | horizontalAlignment = Alignment.CenterHorizontally 25 | ) { 26 | Text( 27 | text = text, 28 | style = MaterialTheme.typography.bodyMedium, 29 | textAlign = TextAlign.Center, 30 | fontWeight = FontWeight.Bold, 31 | modifier = Modifier.fillMaxWidth() 32 | ) 33 | LinearProgressIndicator( 34 | modifier = Modifier.fillMaxWidth(0.8f) 35 | ) 36 | } 37 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/tags/RoundedTag.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.tags 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Shape 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.text.style.TextOverflow 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun RoundedTag( 19 | modifier: Modifier = Modifier, 20 | text: String, 21 | shape: Shape = MaterialTheme.shapes.medium 22 | ) { 23 | Surface( 24 | modifier = modifier, 25 | color = MaterialTheme.colorScheme.secondaryContainer, 26 | shape = shape 27 | ) { 28 | Row( 29 | modifier = Modifier, 30 | verticalAlignment = Alignment.CenterVertically, 31 | horizontalArrangement = Arrangement.Center, 32 | ) { 33 | Text( 34 | text = text, 35 | style = MaterialTheme.typography.bodySmall, 36 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), 37 | overflow = TextOverflow.Ellipsis, 38 | fontWeight = FontWeight.Bold, 39 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) 40 | ) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/text/AnimatedCounter.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.text 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.slideInVertically 5 | import androidx.compose.animation.slideOutVertically 6 | import androidx.compose.animation.togetherWith 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.SideEffect 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableIntStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.TextStyle 18 | 19 | @Composable 20 | fun AnimatedCounter( 21 | count: Int, 22 | modifier: Modifier = Modifier, 23 | style: TextStyle = MaterialTheme.typography.bodySmall 24 | ) { 25 | var oldCount by remember { 26 | mutableIntStateOf(count) 27 | } 28 | SideEffect { 29 | oldCount = count 30 | } 31 | Row(modifier = modifier) { 32 | val countString = count.toString() 33 | val oldCountString = oldCount.toString() 34 | countString.indices.forEach { i -> 35 | val oldChar = oldCountString.getOrNull(i) 36 | val newChar = countString[i] 37 | val char = if (oldChar == newChar) { 38 | oldCountString[i] 39 | } else { 40 | countString[i] 41 | } 42 | AnimatedContent( 43 | targetState = char, 44 | transitionSpec = { 45 | slideInVertically { it } togetherWith slideOutVertically { -it } 46 | }, label = "Animated Counter" 47 | ) { character -> 48 | Text( 49 | text = character.toString(), 50 | style = style, 51 | softWrap = false 52 | ) 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/text/CategoryTitles.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.text 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun ExtraLargeCategoryTitle( 15 | modifier: Modifier = Modifier, 16 | text: String, 17 | color: Color = MaterialTheme.colorScheme.primary, 18 | ) { 19 | Text( 20 | text = text, 21 | modifier = modifier 22 | .fillMaxWidth() 23 | .padding(start = 4.dp, top = 16.dp, bottom = 8.dp), 24 | color = color, 25 | style = MaterialTheme.typography.titleMedium, 26 | fontWeight = FontWeight.Bold 27 | ) 28 | } 29 | 30 | @Composable 31 | fun LargeCategoryTitle( 32 | modifier: Modifier = Modifier, 33 | text: String, 34 | color: Color = MaterialTheme.colorScheme.primary, 35 | ) { 36 | Text( 37 | text = text, 38 | modifier = modifier 39 | .fillMaxWidth() 40 | .padding(start = 4.dp, top = 16.dp, bottom = 8.dp), 41 | color = color, 42 | style = MaterialTheme.typography.labelLarge 43 | ) 44 | } 45 | 46 | @Composable 47 | fun MediumCategoryTitle( 48 | modifier: Modifier = Modifier, 49 | text: String, 50 | color: Color = MaterialTheme.colorScheme.primary, 51 | ) { 52 | Text( 53 | text = text, 54 | modifier = modifier 55 | .fillMaxWidth() 56 | .padding(start = 4.dp, top = 16.dp, bottom = 8.dp), 57 | color = color, 58 | style = MaterialTheme.typography.labelMedium 59 | ) 60 | } 61 | 62 | @Composable 63 | fun SmallCategoryTitle( 64 | modifier: Modifier = Modifier, 65 | text: String, 66 | color: Color = MaterialTheme.colorScheme.primary, 67 | ) { 68 | Text( 69 | text = text, 70 | modifier = modifier 71 | .fillMaxWidth() 72 | .padding(start = 4.dp, top = 16.dp, bottom = 8.dp), 73 | color = color, 74 | style = MaterialTheme.typography.labelSmall 75 | ) 76 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/text/DotWithText.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.text 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun DotWithText(text: String) { 20 | Row(verticalAlignment = Alignment.CenterVertically) { 21 | Box( 22 | modifier = Modifier 23 | .size(8.dp) 24 | .clip(CircleShape) 25 | .background(MaterialTheme.colorScheme.primary) 26 | ) 27 | Text( 28 | text = text, 29 | fontWeight = FontWeight.Bold, 30 | modifier = Modifier.padding(start = 8.dp) 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/text/ExpandableText.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.text 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.material3.LocalTextStyle 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.text.TextStyle 15 | import androidx.compose.ui.text.font.FontFamily 16 | import androidx.compose.ui.text.font.FontStyle 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.text.style.TextDecoration 20 | import androidx.compose.ui.text.style.TextOverflow 21 | import androidx.compose.ui.unit.TextUnit 22 | 23 | @Composable 24 | fun ExpandableText( 25 | text: String, 26 | modifier: Modifier = Modifier, 27 | color: Color = Color.Unspecified, 28 | fontSize: TextUnit = TextUnit.Unspecified, 29 | fontStyle: FontStyle? = null, 30 | fontWeight: FontWeight? = null, 31 | fontFamily: FontFamily? = null, 32 | letterSpacing: TextUnit = TextUnit.Unspecified, 33 | textDecoration: TextDecoration? = null, 34 | textAlign: TextAlign? = null, 35 | lineHeight: TextUnit = TextUnit.Unspecified, 36 | maxLines: Int = 2, 37 | overflow: TextOverflow = TextOverflow.Clip, 38 | softWrap: Boolean = true, 39 | style: TextStyle = LocalTextStyle.current.plus(TextStyle()), 40 | ) { 41 | var expanded by remember { mutableStateOf(false) } 42 | var canBeExpanded by remember { mutableStateOf(false) } 43 | 44 | Text( 45 | modifier = if (canBeExpanded) modifier 46 | .clickable { expanded = !expanded } 47 | .animateContentSize() else modifier, 48 | text = text, 49 | color = color, 50 | fontSize = fontSize, 51 | fontStyle = fontStyle, 52 | fontWeight = fontWeight, 53 | fontFamily = fontFamily, 54 | letterSpacing = letterSpacing, 55 | textDecoration = textDecoration, 56 | textAlign = textAlign, 57 | lineHeight = lineHeight, 58 | overflow = overflow, 59 | softWrap = softWrap, 60 | style = style, 61 | maxLines = if (expanded) Int.MAX_VALUE else maxLines, 62 | onTextLayout = { 63 | if (!canBeExpanded) { 64 | canBeExpanded = it.hasVisualOverflow 65 | } 66 | } 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/text/TextSizedComponents.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.text 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.text.style.TextOverflow 11 | import androidx.compose.ui.unit.TextUnit 12 | import androidx.compose.ui.unit.sp 13 | 14 | @Composable 15 | fun MediumText( 16 | text: String, 17 | modifier: Modifier = Modifier, 18 | color: Color = Color.Unspecified, 19 | fontWeight: FontWeight = FontWeight.Bold, 20 | lineHeight: TextUnit = TextUnit.Unspecified, 21 | maxLines: Int = 1, 22 | fontSize: TextUnit = 16.sp, 23 | textAlign: TextAlign? = null, 24 | ) { 25 | Text( 26 | text, 27 | textAlign = textAlign, 28 | color = color, 29 | fontSize = fontSize, 30 | fontWeight = fontWeight, 31 | maxLines = maxLines, 32 | lineHeight = lineHeight, 33 | overflow = TextOverflow.Ellipsis, 34 | modifier = modifier 35 | ) 36 | } 37 | 38 | @Composable 39 | fun Subtext( 40 | text: String, 41 | modifier: Modifier = Modifier, 42 | fontSize: TextUnit = 12.sp, 43 | maxLines: Int = 2, 44 | textAlign: TextAlign? = null, 45 | ) { 46 | Text( 47 | text, 48 | textAlign = textAlign, 49 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), 50 | fontSize = fontSize, 51 | lineHeight = 18.sp, 52 | maxLines = maxLines, 53 | overflow = TextOverflow.Ellipsis, 54 | modifier = modifier 55 | ) 56 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/components/text/bottomSheet/BottomSheetTexts.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.components.text.bottomSheet 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.AnnotatedString 9 | import androidx.compose.ui.text.font.FontWeight 10 | import androidx.compose.ui.text.style.TextAlign 11 | import androidx.compose.ui.unit.sp 12 | 13 | @Composable 14 | fun BottomSheetHeader( 15 | modifier: Modifier = Modifier, 16 | text: String 17 | ) { 18 | Text( 19 | text, 20 | fontSize = 24.sp, 21 | color = MaterialTheme.colorScheme.onSurface, 22 | textAlign = TextAlign.Center, 23 | fontWeight = FontWeight.Medium, 24 | modifier = modifier.fillMaxWidth() 25 | ) 26 | } 27 | 28 | @Composable 29 | fun BottomSheetSubtitle( 30 | modifier: Modifier = Modifier, 31 | text: AnnotatedString 32 | ) { 33 | Text( 34 | text, 35 | fontSize = 16.sp, 36 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), 37 | textAlign = TextAlign.Center, 38 | modifier = modifier.fillMaxWidth() 39 | ) 40 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/ext/Color.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.ext 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | fun Color.applyAlpha(enabled: Boolean, alpha: Float = 0.62f): Color { 6 | return if (enabled) this else this.copy(alpha = alpha) 7 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/ext/CornerBasedShape.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.ext 2 | 3 | import androidx.compose.foundation.shape.CornerBasedShape 4 | import androidx.compose.foundation.shape.CornerSize 5 | import androidx.compose.ui.unit.dp 6 | 7 | fun CornerBasedShape.top() = copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)) 8 | fun CornerBasedShape.bottom() = copy(topStart = CornerSize(0.dp), topEnd = CornerSize(0.dp)) -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/motion/AnimationSpecs.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.motion 2 | 3 | import android.graphics.Path 4 | import android.view.animation.PathInterpolator 5 | import androidx.compose.animation.BoundsTransform 6 | import androidx.compose.animation.ExperimentalSharedTransitionApi 7 | import androidx.compose.animation.core.CubicBezierEasing 8 | import androidx.compose.animation.core.Easing 9 | import androidx.compose.animation.core.tween 10 | import com.bobbyesp.ui.motion.MotionConstants.DURATION 11 | import com.bobbyesp.ui.motion.MotionConstants.DURATION_ENTER 12 | import com.bobbyesp.ui.motion.MotionConstants.DURATION_EXIT 13 | import com.bobbyesp.ui.motion.MotionConstants.DURATION_EXIT_SHORT 14 | 15 | fun PathInterpolator.toEasing(): Easing { 16 | return Easing { f -> this.getInterpolation(f) } 17 | } 18 | 19 | private val path = Path().apply { 20 | moveTo(0f, 0f) 21 | cubicTo(0.05F, 0F, 0.133333F, 0.06F, 0.166666F, 0.4F) 22 | cubicTo(0.208333F, 0.82F, 0.25F, 1F, 1F, 1F) 23 | } 24 | 25 | val EmphasizedPathInterpolator = PathInterpolator(path) 26 | val EmphasizedEasing = EmphasizedPathInterpolator.toEasing() 27 | 28 | val EmphasizeEasingVariant = CubicBezierEasing(.2f, 0f, 0f, 1f) 29 | val EmphasizedDecelerate = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1f) 30 | val EmphasizedAccelerate = CubicBezierEasing(0.3f, 0f, 1f, 1f) 31 | val EmphasizedDecelerateEasing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1f) 32 | val EmphasizedAccelerateEasing = CubicBezierEasing(0.3f, 0f, 0.8f, 0.15f) 33 | 34 | val StandardDecelerate = CubicBezierEasing(.0f, .0f, 0f, 1f) 35 | val MotionEasingStandard = CubicBezierEasing(0.4F, 0.0F, 0.2F, 1F) 36 | 37 | val tweenSpec = tween(durationMillis = DURATION_ENTER, easing = EmphasizedEasing) 38 | 39 | fun tweenEnter( 40 | delayMillis: Int = DURATION_EXIT, 41 | durationMillis: Int = DURATION_ENTER 42 | ) = 43 | tween( 44 | delayMillis = delayMillis, 45 | durationMillis = durationMillis, 46 | easing = EmphasizedDecelerateEasing 47 | ) 48 | 49 | fun tweenExit( 50 | durationMillis: Int = DURATION_EXIT_SHORT, 51 | ) = tween( 52 | durationMillis = durationMillis, 53 | easing = EmphasizedAccelerateEasing 54 | ) 55 | 56 | @OptIn(ExperimentalSharedTransitionApi::class) 57 | val DefaultBoundsTransform = BoundsTransform { _, _ -> 58 | tween(easing = EmphasizedEasing, durationMillis = DURATION) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/motion/MotionConstants.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.motion 2 | 3 | /* 4 | * Copyright 2021 SOUP 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | import androidx.compose.ui.unit.Dp 21 | import androidx.compose.ui.unit.dp 22 | 23 | object MotionConstants { 24 | const val DefaultMotionDuration: Int = 300 25 | const val DefaultFadeInDuration: Int = 150 26 | const val DefaultFadeOutDuration: Int = 75 27 | val DefaultSlideDistance: Dp = 30.dp 28 | 29 | const val DURATION = 600 30 | const val DURATION_ENTER = 400 31 | const val DURATION_ENTER_SHORT = 300 32 | const val DURATION_EXIT = 200 33 | const val DURATION_EXIT_SHORT = 100 34 | 35 | const val InitialOffset = 0.10f 36 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/util/FadingEdge.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.util 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.draw.drawWithContent 5 | import androidx.compose.ui.graphics.BlendMode 6 | import androidx.compose.ui.graphics.Brush 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.graphics.graphicsLayer 9 | import androidx.compose.ui.unit.Dp 10 | 11 | fun Modifier.fadingEdge( 12 | left: Dp? = null, 13 | top: Dp? = null, 14 | right: Dp? = null, 15 | bottom: Dp? = null, 16 | ): Modifier = this 17 | .graphicsLayer(alpha = 0.99f) 18 | .drawWithContent { 19 | drawContent() 20 | top?.let { 21 | drawRect( 22 | brush = Brush.verticalGradient( 23 | colors = listOf(Color.Transparent, Color.Black), 24 | startY = 0f, 25 | endY = it.toPx() 26 | ), 27 | blendMode = BlendMode.DstIn 28 | ) 29 | } 30 | bottom?.let { 31 | drawRect( 32 | brush = Brush.verticalGradient( 33 | colors = listOf(Color.Black, Color.Transparent), 34 | startY = size.height - it.toPx(), 35 | endY = size.height 36 | ), 37 | blendMode = BlendMode.DstIn 38 | ) 39 | } 40 | left?.let { 41 | drawRect( 42 | brush = Brush.horizontalGradient( 43 | colors = listOf(Color.Black, Color.Transparent), 44 | startX = 0f, 45 | endX = it.toPx() 46 | ), 47 | blendMode = BlendMode.DstIn 48 | ) 49 | } 50 | right?.let { 51 | drawRect( 52 | brush = Brush.horizontalGradient( 53 | colors = listOf(Color.Transparent, Color.Black), 54 | startX = size.width - it.toPx(), 55 | endX = size.width 56 | ), 57 | blendMode = BlendMode.DstIn 58 | ) 59 | } 60 | } 61 | 62 | fun Modifier.fadingEdge( 63 | horizontal: Dp? = null, 64 | vertical: Dp? = null, 65 | ) = fadingEdge( 66 | left = horizontal, 67 | right = horizontal, 68 | top = vertical, 69 | bottom = vertical 70 | ) -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/util/RememberSaveableWithInitialValue.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.key 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.saveable.Saver 8 | import androidx.compose.runtime.saveable.autoSaver 9 | import androidx.compose.runtime.saveable.rememberSaveable 10 | 11 | /** 12 | * Allows to create a saveable with an initial value where updating the initial value will lead to updating the 13 | * state *even if* the composable gets restored from saveable later on. 14 | * 15 | * rememberVolatileSaveable should be used in a composable, when you want to initialize a mutable state 16 | * (e.g. for holding the value of a textinput field) with an initial value AND need the user input to survive 17 | * configuration changes AND want to allow changes to the initial value while being on a later screen 18 | * (i.e. while this composable is not active). 19 | */ 20 | @Composable 21 | fun rememberVolatileSaveable( 22 | initialValue: T?, 23 | saver: Saver = autoSaver() 24 | ): MutableState { 25 | return key(initialValue) { 26 | rememberSaveable(stateSaver = saver) { 27 | mutableStateOf(initialValue) 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Allows to create a saveable with an initial value where updating the initial value will lead to updating the 34 | * state *even if* the composable gets restored from saveable later on. 35 | * 36 | * rememberVolatileSaveable should be used in a composable, when you want to initialize a mutable state 37 | * (e.g. for holding the value of a textinput field) with an initial value AND need the user input to survive 38 | * configuration changes AND want to allow changes to the initial value while being on a later screen 39 | * (i.e. while this composable is not active). 40 | */ 41 | @JvmName("rememberSaveableWithVolatileInitialValueNotNull") 42 | @Composable 43 | fun rememberVolatileSaveable( 44 | initialValue: T, 45 | saver: Saver = autoSaver() 46 | ): MutableState { 47 | return key(initialValue) { 48 | rememberSaveable(stateSaver = saver) { 49 | mutableStateOf(initialValue) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/ui/src/main/java/com/bobbyesp/ui/util/Savers.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui.util 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.AnimationVector1D 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.SaverScope 7 | import androidx.compose.ui.text.input.TextFieldValue 8 | 9 | object TextFieldValueSaver : Saver { 10 | override fun restore(value: String): TextFieldValue { 11 | return TextFieldValue(value) 12 | } 13 | 14 | override fun SaverScope.save(value: TextFieldValue): String { 15 | return value.text 16 | } 17 | } 18 | 19 | object AnimatableSaver : Saver, Float> { 20 | override fun restore(value: Float): Animatable? { 21 | return Animatable(value) 22 | } 23 | 24 | override fun SaverScope.save(value: Animatable): Float? { 25 | return value.value 26 | } 27 | } -------------------------------------------------------------------------------- /app/ui/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Reintentar 4 | Atrás 5 | Deshacer 6 | Refrescado correctamente 7 | Refrescando 8 | Soltar para refrescar 9 | Tirar para refrescar 10 | Copiado al portapapeles 11 | Error 12 | Ha ocurrido un error desconocido! 13 | Abrir ajustes 14 | Línea %1$s 15 | Información adicional 16 | -------------------------------------------------------------------------------- /app/ui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Retry 4 | Back 5 | Undo 6 | Successfully refreshed 7 | Refreshing 8 | Release to refresh 9 | Pull to refresh 10 | Copied to clipboard 11 | Error 12 | An unknown error has occurred! 13 | Open settings 14 | Line %1$s 15 | Additional information 16 | -------------------------------------------------------------------------------- /app/ui/src/test/java/com/bobbyesp/ui/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.ui 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /app/utilities/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/utilities/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.android.kotlin) 4 | alias(libs.plugins.kotlin.serialization) 5 | alias(libs.plugins.kotlin.parcelize) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | android { 10 | namespace = "com.bobbyesp.utilities" 11 | compileSdk = 35 12 | 13 | defaultConfig { 14 | minSdk = 24 15 | 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles("consumer-rules.pro") 18 | } 19 | 20 | buildTypes { 21 | release { 22 | isMinifyEnabled = false 23 | proguardFiles( 24 | getDefaultProguardFile("proguard-android-optimize.txt"), 25 | "proguard-rules.pro" 26 | ) 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_21 31 | targetCompatibility = JavaVersion.VERSION_21 32 | } 33 | buildFeatures { 34 | compose = true 35 | buildConfig = true 36 | } 37 | composeCompiler { 38 | reportsDestination = layout.buildDirectory.dir("compose_compiler") 39 | } 40 | kotlinOptions { 41 | jvmTarget = "21" 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation(libs.core.ktx) 47 | implementation(libs.kotlinx.datetime) 48 | implementation(libs.kotlinx.serialization.json) 49 | implementation(libs.bundles.compose) 50 | implementation(libs.paging.compose) 51 | implementation(libs.paging.runtime) 52 | implementation(libs.coil) 53 | implementation(libs.kotlinx.collections.immutable) 54 | implementation(libs.bundles.coroutines) 55 | implementation(libs.bundles.accompanist) 56 | 57 | androidTestImplementation(platform(libs.compose.bom)) 58 | androidTestImplementation(libs.compose.test.junit4) 59 | implementation(libs.compose.tooling.preview) 60 | debugImplementation(libs.compose.tooling) 61 | debugImplementation(libs.compose.test.manifest) 62 | } -------------------------------------------------------------------------------- /app/utilities/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/app/utilities/consumer-rules.pro -------------------------------------------------------------------------------- /app/utilities/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/utilities/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/Logging.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.os.Build 6 | import android.util.Log 7 | import java.io.File 8 | 9 | object Logging { 10 | private val callingClass = Throwable().stackTrace[1].className 11 | val isDebug = BuildConfig.DEBUG 12 | 13 | fun i(message: String) { 14 | Log.i(callingClass, message) 15 | } 16 | 17 | fun d(message: String) { 18 | Log.d(callingClass, message) 19 | } 20 | 21 | fun e(message: String) { 22 | Log.e(callingClass, message) 23 | } 24 | 25 | fun e(throwable: Throwable) { 26 | Log.e(callingClass, throwable.message ?: "No message", throwable) 27 | } 28 | 29 | fun e(message: String, throwable: Throwable) { 30 | Log.e(callingClass, message, throwable) 31 | } 32 | 33 | fun w(message: String) { 34 | Log.w(callingClass, message) 35 | } 36 | 37 | fun v(message: String) { 38 | Log.v(callingClass, message) 39 | } 40 | 41 | fun wtf(message: String) { 42 | Log.wtf(callingClass, message) 43 | } 44 | 45 | fun getVersionReport(packageInfo: PackageInfo): String { 46 | val versionName = packageInfo.versionName 47 | 48 | @Suppress("DEPRECATION") 49 | val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 50 | packageInfo.longVersionCode 51 | } else { 52 | packageInfo.versionCode.toLong() 53 | } 54 | val release = if (Build.VERSION.SDK_INT >= 30) { 55 | Build.VERSION.RELEASE_OR_CODENAME 56 | } else { 57 | Build.VERSION.RELEASE 58 | } 59 | 60 | val appName = packageInfo.applicationInfo?.name 61 | return StringBuilder() 62 | .append("App version: $appName $versionName ($versionCode)\n") 63 | .append("Android version: Android $release (API ${Build.VERSION.SDK_INT})\n") 64 | .append("Device: ${Build.MANUFACTURER} ${Build.MODEL}\n") 65 | .append("Supported ABIs: ${Build.SUPPORTED_ABIS.contentToString()}\n") 66 | .toString() 67 | } 68 | 69 | fun createLogFile(context: Context, errorReport: String): String { 70 | val date = Time.getZuluTimeSnapshot() 71 | val fileName = "log_$date.txt" 72 | val logFile = File(context.filesDir, fileName) 73 | if (!logFile.exists()) { 74 | logFile.createNewFile() 75 | } 76 | logFile.appendText(errorReport) 77 | return logFile.absolutePath 78 | } 79 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/Packages.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | 7 | object Packages { 8 | fun isPackageInstalled(context: Context, packageName: String): Boolean { 9 | return try { 10 | context.packageManager.getPackageInfo(packageName, 0) 11 | true 12 | } catch (e: PackageManager.NameNotFoundException) { 13 | false 14 | } 15 | } 16 | 17 | fun Intent.launchOrAction(context: Context, action: () -> Unit) { 18 | if (resolveActivity(context.packageManager) != null) { 19 | context.startActivity(this) 20 | } else { 21 | action() 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.TimeZone 5 | import kotlinx.datetime.toLocalDateTime 6 | import java.util.Locale 7 | 8 | object Time { 9 | fun getZuluTimeSnapshot(): String { 10 | val instant = Clock.System.now() 11 | return instant.toLocalDateTime(TimeZone.currentSystemDefault()).toString() 12 | } 13 | 14 | fun formatDuration(duration: Long): String { 15 | val minutes: Long = duration / 60000 16 | val seconds: Long = (duration % 60000) / 1000 17 | return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) 18 | } 19 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ext/Array.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ext 2 | 3 | fun Array?.joinToStringOrNull(separator: String = ", "): String? { 4 | return this?.joinToString(separator = separator) 5 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ext/Int.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ext 2 | 3 | fun Int.bigQuantityFormatter(): String { 4 | return when (this) { 5 | in 0..999 -> this.toString() 6 | in 1000..999999 -> "${this / 1000} K" 7 | in 1000000..999999999 -> "${this / 1000000} M" 8 | else -> "${this / 1000000000} B" 9 | } 10 | } 11 | 12 | /** 13 | * Extension function to convert an integer representing seconds into a formatted string of minutes and seconds. 14 | * 15 | * @receiver Int The number of seconds to be converted. 16 | * @return String The formatted string in "MM:SS" format. 17 | */ 18 | fun Int.toMinutes(): String { 19 | val minutes = this / 60 20 | val seconds = this % 60 21 | return "%02d:%02d".format(minutes, seconds) 22 | } 23 | 24 | /** 25 | * Extension function to convert an integer representing milliseconds into a formatted string of minutes and seconds. 26 | * 27 | * @receiver Int The number of milliseconds to be converted. 28 | * @return String The formatted string in "MM:SS" format. 29 | */ 30 | fun Int.fromMillisToMinutes(): String { 31 | val totalSeconds = this / 1000 32 | val minutes = totalSeconds / 60 33 | val seconds = totalSeconds % 60 34 | return "%02d:%02d".format(minutes, seconds) 35 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ext/Long.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ext 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.res.stringResource 5 | import com.bobbyesp.utilities.R 6 | 7 | private const val GIGA_BYTES = 1024f * 1024f * 1024f 8 | private const val MEGA_BYTES = 1024f * 1024f 9 | 10 | @Composable 11 | fun Long.toFileSizeText() = this.toFloat().run { 12 | if (this > GIGA_BYTES) 13 | stringResource(R.string.filesize_gb).format(this / GIGA_BYTES) 14 | else stringResource(R.string.filesize_mb).format(this / MEGA_BYTES) 15 | } 16 | -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ext/String.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ext 2 | 3 | import androidx.core.text.isDigitsOnly 4 | 5 | fun String?.formatForField(separator: String = ","): Array { 6 | return this?.split(separator)?.map { it.trim() }?.toTypedArray() ?: arrayOf(this ?: "") 7 | } 8 | 9 | fun String.isNumberInRange(start: Int, end: Int): Boolean { 10 | return this.isNotEmpty() && this.isDigitsOnly() && this.length < 10 && this.toInt() >= start && this.toInt() <= end 11 | } 12 | 13 | fun String?.isNeitherNullNorBlank(): Boolean { 14 | return this != null && this.isNotBlank() 15 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/advanced/AdvancedContentResolverQuery.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.mediastore.advanced 2 | 3 | import android.content.ContentResolver 4 | import android.database.Cursor 5 | import android.net.Uri 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.provider.MediaStore 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.withContext 11 | 12 | //Thanks to https://proandroiddev.com/kotlin-flow-contentresolver-and-mediastore-the-key-to-effortless-media-access-in-android-fad56db16fdd 13 | /** 14 | * An advanced of [ContentResolver.query] 15 | * @see ContentResolver.query 16 | * @param order valid column to use for orderBy. 17 | */ 18 | suspend fun ContentResolver.advancedQuery( 19 | uri: Uri, 20 | projection: Array? = null, 21 | selection: String, 22 | args: Array? = null, 23 | order: String = MediaStore.MediaColumns._ID, 24 | ascending: Boolean = true, 25 | offset: Int = 0, 26 | limit: Int = Int.MAX_VALUE 27 | ): Cursor? { 28 | return withContext(Dispatchers.Default) { 29 | // use only above android 10 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 31 | // compose the args 32 | val args2 = Bundle().apply { 33 | // Limit & Offset 34 | putInt(ContentResolver.QUERY_ARG_LIMIT, limit) 35 | putInt(ContentResolver.QUERY_ARG_OFFSET, offset) 36 | 37 | // order 38 | putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(order)) 39 | putInt( 40 | ContentResolver.QUERY_ARG_SORT_DIRECTION, 41 | if (ascending) ContentResolver.QUERY_SORT_DIRECTION_ASCENDING else ContentResolver.QUERY_SORT_DIRECTION_DESCENDING 42 | ) 43 | // Selection and groupBy 44 | if (args != null) putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, args) 45 | // add selection. 46 | // TODO: Consider adding group by. 47 | // currently I experienced errors in android 10 for groupBy and arg groupBy is supported 48 | // above android 10. 49 | putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection) 50 | } 51 | query(uri, projection, args2, null) 52 | } 53 | // below android 0 54 | else { 55 | //language=SQL 56 | val order2 = 57 | order + (if (ascending) " ASC" else " DESC") + " LIMIT $limit OFFSET $offset" 58 | // compose the selection. 59 | query(uri, projection, selection, args, order2) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/advanced/ContentResolverObserver.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.mediastore.advanced 2 | 3 | import android.content.ContentResolver 4 | import android.database.ContentObserver 5 | import android.net.Uri 6 | import kotlinx.coroutines.channels.awaitClose 7 | import kotlinx.coroutines.flow.callbackFlow 8 | 9 | /** 10 | * Register an observer class that gets callbacks when data identified by a given content URI 11 | * changes. 12 | */ 13 | fun ContentResolver.observe(uri: Uri) = callbackFlow { 14 | val observer = object : ContentObserver(null) { 15 | override fun onChange(selfChange: Boolean) { 16 | trySend(selfChange) 17 | } 18 | } 19 | registerContentObserver(uri, true, observer) 20 | // trigger first. 21 | trySend(false) 22 | awaitClose { 23 | unregisterContentObserver(observer) 24 | } 25 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/model/Song.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.mediastore.model 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import androidx.compose.runtime.Stable 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | import kotlinx.serialization.KSerializer 9 | import kotlinx.serialization.Serializable 10 | import kotlinx.serialization.Serializer 11 | import kotlinx.serialization.descriptors.PrimitiveKind 12 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 13 | import kotlinx.serialization.descriptors.SerialDescriptor 14 | import kotlinx.serialization.encoding.Decoder 15 | import kotlinx.serialization.encoding.Encoder 16 | 17 | @Serializable 18 | @Parcelize 19 | @Stable 20 | data class Song( 21 | val id: Long, 22 | val title: String, 23 | val artist: String, 24 | val album: String, 25 | @Serializable(with = UriSerializer::class) val artworkPath: Uri? = null, 26 | val duration: Double, 27 | val path: String, 28 | val fileName: String 29 | ) : Parcelable { 30 | companion object { 31 | val empty = Song(-1, "", "", "", null, 0.0, "", "") 32 | } 33 | } 34 | 35 | @OptIn(ExperimentalSerializationApi::class) 36 | @Serializer(forClass = Uri::class) 37 | object UriSerializer : KSerializer { 38 | override val descriptor: SerialDescriptor = 39 | PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) 40 | 41 | override fun serialize(encoder: Encoder, value: Uri) { 42 | encoder.encodeString(value.toString()) 43 | } 44 | 45 | override fun deserialize(decoder: Decoder): Uri { 46 | return Uri.parse(decoder.decodeString()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/states/ResourceState.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.states 2 | 3 | /** 4 | * A sealed class representing the state of a resource. 5 | * 6 | * @param T The type of data held by this state. 7 | * @property data The data associated with the state, if any. 8 | * @property message The message associated with the state, if any. 9 | */ 10 | sealed class ResourceState( 11 | val data: T? = null, 12 | val message: String? = null 13 | ) { 14 | /** 15 | * Represents a loading state with optional partial data. 16 | * 17 | * @param T The type of data held by this state. 18 | * @property partialData The partial data associated with the loading state, if any. 19 | */ 20 | data class Loading(val partialData: T? = null) : ResourceState(partialData) 21 | 22 | /** 23 | * Represents a successful state with required data. 24 | * 25 | * @param T The type of data held by this state. 26 | * @property result The result data associated with the successful state. 27 | */ 28 | data class Success(val result: T) : ResourceState(result) 29 | 30 | /** 31 | * Represents an error state with a message and optional data. 32 | * 33 | * @param T The type of data held by this state. 34 | * @property errorMessage The error message associated with the error state. 35 | * @property errorData The data associated with the error state, if any. 36 | */ 37 | data class Error( 38 | val errorMessage: String, 39 | val errorData: T? = null 40 | ) : ResourceState(errorData, errorMessage) 41 | 42 | /** 43 | * Returns a string representation of the resource state. 44 | * 45 | * @return A string describing the current state. 46 | */ 47 | override fun toString(): String { 48 | return when (this) { 49 | is Loading -> "Loading(data=$data)" 50 | is Success -> "Success(data=$data)" 51 | is Error -> "Error(message=$message, data=$data)" 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/states/ScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.states 2 | 3 | /** 4 | * A sealed interface representing the state of a screen. 5 | * 6 | * @param T The type of data held by this state. 7 | */ 8 | sealed interface ScreenState { 9 | /** 10 | * Represents a loading state. 11 | */ 12 | object Loading : ScreenState 13 | 14 | /** 15 | * Represents a successful state with data. 16 | * 17 | * @param T The type of data held by this state. 18 | * @property data The data associated with the successful state. 19 | */ 20 | data class Success(val data: T?) : ScreenState 21 | 22 | /** 23 | * Represents an error state with an exception. 24 | * 25 | * @property exception The exception associated with the error state. 26 | */ 27 | data class Error(val exception: Throwable) : ScreenState 28 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ui/Assets.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ui 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.res.vectorResource 7 | 8 | @Composable 9 | fun localAsset(@DrawableRes id: Int) = ImageVector.vectorResource(id = id) -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ui/Colors.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ui 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | const val DEFAULT_SEED_COLOR = 0xFF415f76.toInt() 6 | 7 | fun Color.applyOpacity(enabled: Boolean): Color { 8 | return if (enabled) this else this.copy(alpha = 0.62f) 9 | } 10 | 11 | fun Color.applyAlpha(alpha: Float): Color = this.copy(alpha = alpha) -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ui/LazyGridStateSaver.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ui 2 | 3 | import androidx.compose.foundation.lazy.grid.LazyGridState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.DisposableEffect 6 | import androidx.compose.runtime.saveable.rememberSaveable 7 | 8 | /** 9 | * Static field, contains all scroll values 10 | */ 11 | val SaveMap = mutableMapOf() 12 | 13 | data class KeyParams( 14 | val params: String = "", 15 | val index: Int, 16 | val scrollOffset: Int 17 | ) 18 | 19 | /** 20 | * Save scroll state on all time. 21 | * @param key value for comparing screen 22 | * @param params arguments for find different between equals screen 23 | * @param initialFirstVisibleItemIndex see [LazyGridState.firstVisibleItemIndex] 24 | * @param initialFirstVisibleItemScrollOffset see [LazyGridState.firstVisibleItemScrollOffset] 25 | */ 26 | @Composable 27 | fun rememberForeverLazyGridState( 28 | key: String, 29 | params: String = "", 30 | initialFirstVisibleItemIndex: Int = 0, 31 | initialFirstVisibleItemScrollOffset: Int = 0 32 | ): LazyGridState { 33 | val scrollState = rememberSaveable(saver = LazyGridState.Saver) { 34 | var savedValue = SaveMap[key] 35 | if (savedValue?.params != params) savedValue = null 36 | val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex 37 | val savedOffset = savedValue?.scrollOffset ?: initialFirstVisibleItemScrollOffset 38 | LazyGridState( 39 | savedIndex, 40 | savedOffset 41 | ) 42 | } 43 | DisposableEffect(Unit) { 44 | onDispose { 45 | val lastIndex = scrollState.firstVisibleItemIndex 46 | val lastOffset = scrollState.firstVisibleItemScrollOffset 47 | SaveMap[key] = KeyParams(params, lastIndex, lastOffset) 48 | } 49 | } 50 | return scrollState 51 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ui/PagingStateHandler.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ui 2 | 3 | import androidx.compose.foundation.lazy.LazyListScope 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.paging.LoadState 7 | import androidx.paging.compose.LazyPagingItems 8 | import com.bobbyesp.utilities.BuildConfig 9 | 10 | fun LazyListScope.pagingStateHandler( 11 | items: LazyPagingItems?, 12 | itemCount: Int = 7, 13 | loadingContent: @Composable () -> Unit 14 | ) { 15 | items?.apply { 16 | when { 17 | loadState.refresh is LoadState.Loading -> { 18 | items(itemCount) { 19 | // Render a loading indicator while refreshing 20 | loadingContent() 21 | } 22 | } 23 | 24 | loadState.append is LoadState.Loading -> { 25 | items(itemCount) { 26 | // Render a loading indicator at the end while loading more items 27 | loadingContent() 28 | } 29 | } 30 | 31 | loadState.refresh is LoadState.Error -> { 32 | val errorMessage = 33 | (loadState.refresh as LoadState.Error).error.message 34 | item { 35 | // Render an error message if refreshing encounters an error 36 | if (errorMessage != null) { 37 | if (BuildConfig.DEBUG) Text(errorMessage) 38 | } 39 | } 40 | } 41 | 42 | loadState.append is LoadState.Error -> { 43 | val errorMessage = 44 | (loadState.append as LoadState.Error).error.message 45 | item { 46 | // Render an error message if loading more items encounters an error 47 | if (errorMessage != null) { 48 | if (BuildConfig.DEBUG) Text(errorMessage) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/utilities/src/main/java/com/bobbyesp/utilities/ui/permission/PermissionRequestHandler.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.utilities.ui.permission 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 5 | import com.google.accompanist.permissions.PermissionState 6 | import com.google.accompanist.permissions.PermissionStatus 7 | 8 | @ExperimentalPermissionsApi 9 | @Composable 10 | fun PermissionRequestHandler( 11 | permissionState: PermissionState, 12 | deniedContent: @Composable (Boolean) -> Unit, 13 | content: @Composable () -> Unit 14 | ) { 15 | when (permissionState.status) { 16 | is PermissionStatus.Granted -> { 17 | content() 18 | } 19 | 20 | is PermissionStatus.Denied -> { 21 | deniedContent( 22 | (permissionState.status as PermissionStatus.Denied).shouldShowRationale 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/utilities/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Seguir al sistema 4 | Encendido 5 | Apagado 6 | Conceder 7 | Denegar 8 | Título 9 | Artista 10 | Permiso no concedido 11 | Para acceder a esta funcionalidad de la aplicación se requiere de algún permiso. Te gustaría aceptarlo? 12 | Si no le concedes a la aplicación los permisos necesarios no podrás acceder a esta funcionalidad. 13 | Los permisos necesarios son: 14 | Aceptar permisos 15 | Leer almacenamiento externo 16 | Escribir almacenamiento externo 17 | Internet 18 | Acceder al estado de la red 19 | Acceder al estado del WiFi 20 | Cambiar el estado del WiFi 21 | Leer archivos de audio 22 | Administrar almacenamiento externo 23 | SongSync necesario 24 | SongSync es un simple pero poderoso descargador de letras que permite a los usuarios obtener las letras desde múltiples fuentes. \nMetadator usa su puente integrado para recibir las letras. 25 | Parece que la aplicación no está instalada en tu dispositivo. Por favor, descárgala para acceder a esta funcionalidad. 26 | Descargar 27 | Álbum 28 | Permite a la aplicación leer contenido de tu almacenamiento 29 | Permite a la aplicación escribir en tu almacenamiento 30 | Permite a la aplicación leer los archivos de sonido 31 | Este permiso no contiene una descripción 32 | -------------------------------------------------------------------------------- /app/utilities/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Follow system 4 | On 5 | Off 6 | Grant 7 | Dismiss 8 | %.2f MB 9 | %.2f GB 10 | Title 11 | Artist 12 | 13 | Permission not granted 14 | To access this feature of the app is required to grant some necessary permissions. Would you like to accept them? 15 | If you do not grant the required permissions you won\'t be able to access this app functionality. 16 | The permissions needed are: 17 | Accept permissions 18 | 19 | Read external storage 20 | Write external storage 21 | Internet 22 | Access network state 23 | Access WiFi state 24 | Change WiFi state 25 | Read audio files 26 | Manage external storage 27 | 28 | SongSync needed 29 | SongSync is a simple yet powerful lyrics downloader that lets users fetch lyrics from multiple sources. \nMetadator leverages its integrated bridge to retrieve the lyrics. 30 | It seems the app isn\'t installed on your device. Please download it to access this feature 31 | Download 32 | Album 33 | Allows the app to read the contents of your storage 34 | Allows the app to write to your storage 35 | Allows the app to read audio files 36 | This permission does not contain a description 37 | -------------------------------------------------------------------------------- /assets/app_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/assets/app_logo.png -------------------------------------------------------------------------------- /assets/feature_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/assets/feature_header.png -------------------------------------------------------------------------------- /assets/mockups/Mockup1_FINAL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/assets/mockups/Mockup1_FINAL.png -------------------------------------------------------------------------------- /assets/mockups/Mockup2_FINAL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/assets/mockups/Mockup2_FINAL.png -------------------------------------------------------------------------------- /assets/mockups/Mockup3_FINAL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/assets/mockups/Mockup3_FINAL.png -------------------------------------------------------------------------------- /crashhandler/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /crashhandler/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.android.kotlin) 4 | alias(libs.plugins.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "com.bobbyesp.crashhandler" 9 | compileSdk = 35 10 | 11 | defaultConfig { 12 | minSdk = 24 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_21 29 | targetCompatibility = JavaVersion.VERSION_21 30 | } 31 | buildFeatures { 32 | compose = true 33 | } 34 | composeCompiler { 35 | reportsDestination = layout.buildDirectory.dir("compose_compiler") 36 | } 37 | kotlinOptions { 38 | jvmTarget = "21" 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(libs.core.ktx) 44 | implementation(libs.bundles.compose) 45 | implementation(libs.compose.tooling.preview) 46 | 47 | //Compose testing and tooling libraries 48 | androidTestImplementation(platform(libs.compose.bom)) 49 | androidTestImplementation(libs.compose.test.junit4) 50 | debugImplementation(libs.compose.tooling) 51 | debugImplementation(libs.compose.test.manifest) 52 | } -------------------------------------------------------------------------------- /crashhandler/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/crashhandler/consumer-rules.pro -------------------------------------------------------------------------------- /crashhandler/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 -------------------------------------------------------------------------------- /crashhandler/src/androidTest/java/com/bobbyesp/crashhandler/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.bobbyesp.crashhandler.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /crashhandler/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crashhandler/src/main/java/com/bobbyesp/crashhandler/CrashHandlerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.saveable.rememberSaveable 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.platform.LocalClipboardManager 13 | import androidx.compose.ui.text.AnnotatedString 14 | import androidx.core.view.ViewCompat 15 | import androidx.core.view.WindowCompat 16 | import com.bobbyesp.crashhandler.ui.CrashReportPage 17 | import com.bobbyesp.crashhandler.ui.theme.CrashHandlerTheme 18 | import java.io.File 19 | 20 | class CrashHandlerActivity : ComponentActivity() { 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | enableEdgeToEdge() 24 | val versionReport: String = intent.getStringExtra("version_report").toString() 25 | val logfilePath: String = intent.getStringExtra("logfile_path").toString() 26 | 27 | setContent { 28 | CrashHandlerTheme { 29 | val clipboardManager = LocalClipboardManager.current 30 | var log by rememberSaveable(key = "log") { 31 | mutableStateOf("") 32 | } 33 | 34 | LaunchedEffect(true) { 35 | val logFile = File(logfilePath) 36 | log = logFile.readText() 37 | } 38 | 39 | CrashReportPage( 40 | versionReport = versionReport, 41 | errorMessage = log 42 | ) { 43 | clipboardManager.setText( 44 | AnnotatedString(versionReport).plus( 45 | AnnotatedString( 46 | "\n" 47 | ) 48 | ).plus(AnnotatedString(log)) 49 | ) 50 | this.finishAffinity() 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /crashhandler/src/main/java/com/bobbyesp/crashhandler/ReportInfo.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler 2 | 3 | data class ReportInfo( 4 | val androidVersion: Boolean = true, 5 | val deviceInfo: Boolean = true, 6 | val supportedABIs: Boolean = true, 7 | ) 8 | -------------------------------------------------------------------------------- /crashhandler/src/main/java/com/bobbyesp/crashhandler/ui/UiUtils.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler.ui 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object UiUtils { 6 | fun Color.applyAlpha(alpha: Float): Color { 7 | return this.copy(alpha = alpha) 8 | } 9 | } -------------------------------------------------------------------------------- /crashhandler/src/main/java/com/bobbyesp/crashhandler/ui/components/FilledButtonWithIcon.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler.ui.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Button 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.vector.ImageVector 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun FilledButtonWithIcon( 16 | modifier: Modifier = Modifier, 17 | onClick: () -> Unit, 18 | icon: ImageVector, 19 | enabled: Boolean = true, 20 | text: String, 21 | contentDescription: String? = null 22 | ) { 23 | Button( 24 | modifier = modifier, 25 | onClick = onClick, 26 | enabled = enabled, 27 | contentPadding = ButtonDefaults.ButtonWithIconContentPadding 28 | ) 29 | { 30 | Icon( 31 | modifier = Modifier.size(18.dp), 32 | imageVector = icon, 33 | contentDescription = contentDescription 34 | ) 35 | Text( 36 | modifier = Modifier.padding(start = 6.dp), 37 | text = text 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /crashhandler/src/main/java/com/bobbyesp/crashhandler/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.material3.lightColorScheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.platform.LocalContext 11 | 12 | @Composable 13 | fun CrashHandlerTheme( 14 | darkTheme: Boolean = isSystemInDarkTheme(), 15 | content: @Composable () -> Unit 16 | ) { 17 | val context = LocalContext.current 18 | 19 | //if is android higher than 12, use dynamic color scheme, else use static color scheme based on dark theme 20 | if (android.os.Build.VERSION.SDK_INT >= 31) { 21 | MaterialTheme( 22 | colorScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme( 23 | context 24 | ) 25 | ) { 26 | content() 27 | } 28 | } else { 29 | MaterialTheme( 30 | colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme() 31 | ) { 32 | content() 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /crashhandler/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copy and exit 4 | Report on GitHub 5 | An unknown error has occurred! 6 | Device info 7 | This shows us information about your device for helping you to solve the problem 8 | -------------------------------------------------------------------------------- /crashhandler/src/test/java/com/bobbyesp/crashhandler/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.bobbyesp.crashhandler 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaultConfig.ndk.debugSymbolLevel='FULL' 25 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobbyESP/Metadator/993e0d5d461e99b89ac8b227ce6d7bea89d47107/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 26 14:31:21 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | maven("https://maven.google.com/") 13 | maven("https://jitpack.io") 14 | } 15 | } 16 | dependencyResolutionManagement { 17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 18 | repositories { 19 | google() 20 | mavenCentral() 21 | maven("https://jitpack.io") 22 | } 23 | } 24 | 25 | rootProject.name = "Metadator" 26 | include(":app") 27 | include(":app:utilities") 28 | include(":app:ui") 29 | include(":crashhandler") 30 | include(":app:mediaplayer") 31 | --------------------------------------------------------------------------------