├── .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 |
5 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/studiobot.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------