├── .github ├── FUNDING.yml └── workflows │ └── beta.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── shub39 │ │ │ └── rush │ │ │ ├── MainActivity.kt │ │ │ ├── RushApp.kt │ │ │ ├── RushApplication.kt │ │ │ ├── core │ │ │ ├── data │ │ │ │ ├── DatastoreFactory.android.kt │ │ │ │ └── PaletteGenerator.android.kt │ │ │ └── presentation │ │ │ │ ├── RushTheme.android.kt │ │ │ │ └── util.android.kt │ │ │ ├── di │ │ │ ├── RushModules.android.kt │ │ │ └── util.kt │ │ │ ├── lyrics │ │ │ ├── data │ │ │ │ ├── backup │ │ │ │ │ ├── ExportImpl.android.kt │ │ │ │ │ └── RestoreImpl.android.kt │ │ │ │ ├── database │ │ │ │ │ └── DatabaseFactory.android.kt │ │ │ │ └── listener │ │ │ │ │ ├── MediaListenerImpl.android.kt │ │ │ │ │ └── NotificationListener.kt │ │ │ └── presentation │ │ │ │ ├── LyricsGraph.kt │ │ │ │ ├── SettingsGraph.kt │ │ │ │ ├── lyrics │ │ │ │ └── LyricsPage.kt │ │ │ │ ├── setting │ │ │ │ ├── AboutAppPage.kt │ │ │ │ ├── AboutLibrariesPage.kt │ │ │ │ ├── BackupPage.kt │ │ │ │ ├── BatchDownloader.kt │ │ │ │ ├── LookAndFeelPage.kt │ │ │ │ ├── SettingRootPage.kt │ │ │ │ ├── SettingsPageAction.kt │ │ │ │ ├── component │ │ │ │ │ └── DownloaderCard.kt │ │ │ │ └── util.kt │ │ │ │ ├── share │ │ │ │ ├── SharePage.kt │ │ │ │ ├── SharePageAction.kt │ │ │ │ ├── component │ │ │ │ │ ├── ChatCard.kt │ │ │ │ │ ├── CoupletShareCard.kt │ │ │ │ │ ├── HypnoticShareCard.kt │ │ │ │ │ ├── ListSelect.kt │ │ │ │ │ ├── MessyCard.kt │ │ │ │ │ ├── QuoteShareCard.kt │ │ │ │ │ ├── RushedShareCard.kt │ │ │ │ │ ├── SpotifyShareCard.kt │ │ │ │ │ └── VerticalShareCard.kt │ │ │ │ └── util.kt │ │ │ │ └── viewmodels │ │ │ │ ├── SettingsVM.kt │ │ │ │ └── ShareVM.kt │ │ │ └── onboarding │ │ │ └── OnBoardingDialog.kt │ └── res │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.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 │ │ ├── values-night │ │ └── splash.xml │ │ ├── values │ │ ├── splash.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── file_paths.xml │ ├── commonMain │ ├── composeResources │ │ ├── drawable │ │ │ ├── genius.xml │ │ │ └── rush_transparent.png │ │ ├── font │ │ │ ├── dm_sans.ttf │ │ │ ├── figtree.ttf │ │ │ ├── inter.ttf │ │ │ ├── jost.ttf │ │ │ ├── manrope.ttf │ │ │ ├── montserrat.ttf │ │ │ ├── open_sans.ttf │ │ │ ├── outfit.ttf │ │ │ ├── poppins_regular.ttf │ │ │ └── quicksand.ttf │ │ ├── values-ar │ │ │ └── strings.xml │ │ ├── values-de │ │ │ └── strings.xml │ │ ├── values-es │ │ │ └── strings.xml │ │ ├── values-et │ │ │ └── strings.xml │ │ ├── values-fa │ │ │ └── strings.xml │ │ ├── values-fr │ │ │ └── strings.xml │ │ ├── values-he │ │ │ └── strings.xml │ │ ├── values-id │ │ │ └── strings.xml │ │ ├── values-it │ │ │ └── strings.xml │ │ ├── values-ja │ │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ │ └── strings.xml │ │ ├── values-ro-rRO │ │ │ └── strings.xml │ │ ├── values-ru │ │ │ └── strings.xml │ │ ├── values-tr │ │ │ └── strings.xml │ │ ├── values-uk │ │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ └── com │ │ └── shub39 │ │ └── rush │ │ ├── core │ │ ├── data │ │ │ ├── DatastoreFactory.kt │ │ │ ├── HttpClientExt.kt │ │ │ ├── HttpClientFactory.kt │ │ │ ├── LyricsPagePreferencesImpl.kt │ │ │ ├── OtherPreferencesImpl.kt │ │ │ ├── PaletteGenerator.kt │ │ │ └── SharePagePreferencesImpl.kt │ │ ├── domain │ │ │ ├── Error.kt │ │ │ ├── LyricsPagePreferences.kt │ │ │ ├── OtherPreferences.kt │ │ │ ├── Result.kt │ │ │ ├── Route.kt │ │ │ ├── SharePagePreferences.kt │ │ │ ├── SourceError.kt │ │ │ ├── data_classes │ │ │ │ ├── ExtractedColors.kt │ │ │ │ ├── SongDetails.kt │ │ │ │ └── Theme.kt │ │ │ └── enums │ │ │ │ ├── AppTheme.kt │ │ │ │ ├── CardColors.kt │ │ │ │ ├── CardFit.kt │ │ │ │ ├── CardTheme.kt │ │ │ │ ├── CornerRadius.kt │ │ │ │ ├── Fonts.kt │ │ │ │ ├── SortOrder.kt │ │ │ │ └── Sources.kt │ │ └── presentation │ │ │ ├── ArtFromUrl.kt │ │ │ ├── ColorPickerDialog.kt │ │ │ ├── Empty.kt │ │ │ ├── PageFill.kt │ │ │ ├── RushDialog.kt │ │ │ ├── RushTheme.kt │ │ │ ├── SettingsSlider.kt │ │ │ ├── Typography.kt │ │ │ ├── errorStringRes.kt │ │ │ ├── scrollbar.kt │ │ │ └── util.kt │ │ ├── di │ │ ├── RushModules.kt │ │ └── initKoin.kt │ │ └── lyrics │ │ ├── data │ │ ├── backup │ │ │ ├── ExportImpl.kt │ │ │ └── RestoreImpl.kt │ │ ├── database │ │ │ ├── DatabaseFactory.kt │ │ │ ├── SongDao.kt │ │ │ ├── SongDatabase.kt │ │ │ └── SongEntity.kt │ │ ├── listener │ │ │ └── MediaListenerImpl.kt │ │ ├── mappers │ │ │ └── Mappers.kt │ │ ├── network │ │ │ ├── GeniusApi.kt │ │ │ ├── GeniusScraper.kt │ │ │ ├── LrcLibApi.kt │ │ │ ├── Tokens.kt │ │ │ └── dto │ │ │ │ ├── genius │ │ │ │ ├── GeniusSearchDto.kt │ │ │ │ └── GeniusSongDto.kt │ │ │ │ └── lrclib │ │ │ │ └── LrcGetDto.kt │ │ └── repository │ │ │ └── RushRepository.kt │ │ ├── domain │ │ ├── AudioFile.kt │ │ ├── LrcLibSong.kt │ │ ├── Lyric.kt │ │ ├── MediaInterface.kt │ │ ├── SearchResult.kt │ │ ├── Song.kt │ │ ├── SongRepo.kt │ │ ├── SongUi.kt │ │ └── backup │ │ │ ├── ExportRepo.kt │ │ │ ├── ExportSchema.kt │ │ │ ├── ExportState.kt │ │ │ ├── RestoreRepo.kt │ │ │ ├── RestoreResult.kt │ │ │ └── SongSchema.kt │ │ └── presentation │ │ ├── lyrics │ │ ├── LyricsCustomisationPage.kt │ │ ├── LyricsPageAction.kt │ │ ├── LyricsPageState.kt │ │ ├── component │ │ │ ├── ActionsRow.kt │ │ │ ├── ErrorCard.kt │ │ │ ├── LoadingCard.kt │ │ │ ├── LrcCorrectDialog.kt │ │ │ ├── PlainLyrics.kt │ │ │ └── SyncedLyrics.kt │ │ └── util.kt │ │ ├── saved │ │ ├── SavedPage.kt │ │ ├── SavedPageAction.kt │ │ ├── SavedPageState.kt │ │ └── component │ │ │ ├── GroupedCard.kt │ │ │ └── SongCard.kt │ │ ├── search_sheet │ │ ├── SearchResultCard.kt │ │ ├── SearchSheet.kt │ │ ├── SearchSheetAction.kt │ │ └── SearchSheetState.kt │ │ ├── setting │ │ └── SettingsPageState.kt │ │ ├── share │ │ └── SharePageState.kt │ │ └── viewmodels │ │ ├── LyricsVM.kt │ │ ├── SavedVM.kt │ │ ├── SearchSheetVM.kt │ │ └── StateLayer.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── shub39 │ │ └── rush │ │ ├── RushApp.kt │ │ ├── core │ │ ├── data │ │ │ ├── DatastoreFactory.desktop.kt │ │ │ └── PaletteGenerator.desktop.kt │ │ └── presentation │ │ │ ├── RushTheme.desktop.kt │ │ │ └── util.desktop.kt │ │ ├── di │ │ └── RushModules.desktop.kt │ │ ├── lyrics │ │ ├── data │ │ │ ├── backup │ │ │ │ ├── ExportImpl.desktop.kt │ │ │ │ └── RestoreImpl.desktop.kt │ │ │ ├── database │ │ │ │ └── DatabaseFactory.desktop.kt │ │ │ └── listener │ │ │ │ └── MediaListenerImpl.desktop.kt │ │ └── presentation │ │ │ ├── LyricsGraph.kt │ │ │ ├── LyricsPage.kt │ │ │ └── SettingsGraph.kt │ │ └── main.kt │ └── test │ └── java │ └── com │ └── shub39 │ └── rush │ └── ApiTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── ar-DZ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── as │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── en-US │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ ├── icon200x200.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ └── 7.png │ ├── short_description.txt │ └── title.txt │ ├── es-ES │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── et │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── fa-IR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── hi-IN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── id-ID │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── it-IT │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── iw-IL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ja-JP │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pt-rBR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ro-rRO │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ru-RU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── tr-TR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── uk-UA │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── zh-rCN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── zh-rTW │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [shub39] 2 | buy_me_a_coffee: shub39 3 | custom: ["https://www.paypal.me/shub39"] -------------------------------------------------------------------------------- /.github/workflows/beta.yml: -------------------------------------------------------------------------------- 1 | name: beta 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: { } 8 | 9 | jobs: 10 | android: 11 | name: Build Beta APK 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up JDK 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: 'zulu' 24 | java-version: '17' 25 | cache: 'gradle' 26 | 27 | - name: Setup Gradle 28 | uses: gradle/actions/setup-gradle@v4 29 | 30 | - name: Decode Keystore 31 | env: 32 | KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} 33 | run: echo "$KEYSTORE_FILE" | base64 --decode > $GITHUB_WORKSPACE/keystore.jks 34 | 35 | - name: Grant execute permissions to Gradle wrapper 36 | run: chmod +x gradlew 37 | 38 | - name: Build Beta APK 39 | run: | 40 | ./gradlew assembleBeta \ 41 | -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/keystore.jks \ 42 | -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \ 43 | -Pandroid.injected.signing.key.alias=key0 \ 44 | -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} 45 | 46 | - name: Upload Beta APK 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: beta-apk 50 | path: app/build/outputs/apk/beta/app-beta.apk 51 | 52 | - name: Upload APK to Discord 53 | shell: bash 54 | env: 55 | WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 56 | VERSION: $( echo ${{ github.event.head_commit.id }} | cut -c1-7 ) 57 | COMMIT: $( sed -E "s/(.*) <.*@.*>/\\1/g;t" <<< "${{ github.event.head_commit.message }}" | jq -Rsa . | tail -c +2 | head -c -2 ) 58 | run: | 59 | message=$(echo "**${{ env.VERSION }}**\n${{ env.COMMIT }}\n[Download APK from GitHub Actions](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})") 60 | curl -H "Content-Type: application/json" -X POST -d "{\"content\": \"$message\"}" ${{ env.WEBHOOK }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | app/beta 3 | app/play 4 | app/release 5 | .gradle 6 | /local.properties 7 | .idea 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | .cxx 13 | local.properties 14 | .kotlin -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Rush]() 2 | 3 | # Rush 4 | ### Search, save and share lyrics like Spotify! 5 | 6 | > []() 7 | > []() 8 | > []() 9 | 10 | > ### Stats and Socials 11 | > []() 12 | > []() 13 | > [](https://discord.gg/https://discord.gg/nxA2hgtEKf) 14 | > [](https://x.com/RushedLyrics) 15 | 16 | > ### Get On 17 | > [Get it on Google Play](https://play.google.com/store/apps/details?id=com.shub39.rush.play) 18 | > [Get it on F-Droid](https://f-droid.org/packages/com.shub39.rush/) 19 | > [](https://apt.izzysoft.de/packages/com.shub39.rush/latest) 20 | > [](https://www.openapk.net/dharmik/com.shub39.rush/) 21 | > [](https://www.androidfreeware.net/download-rush-apk.html) 22 | > ### Or Download latest from [Releases](https://github.com/shub39/Rush/releases) 23 | 24 | ## Screenshots 📱 25 | 26 | | ![1](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png) | ![2](fastlane/metadata/android/en-US/images/phoneScreenshots/2.png) | 27 | |:-------------------------------------------------------------------:|:-------------------------------------------------------------------:| 28 | | ![3](fastlane/metadata/android/en-US/images/phoneScreenshots/3.png) | ![4](fastlane/metadata/android/en-US/images/phoneScreenshots/4.png) | 29 | | ![5](fastlane/metadata/android/en-US/images/phoneScreenshots/5.png) | ![6](fastlane/metadata/android/en-US/images/phoneScreenshots/6.png) | 30 | 31 | ## Features ✨ 32 | >- [x] Search Lyrics 33 | >- [x] Download Lyrics 34 | >- [x] Share Lyrics 35 | >- [x] Customisations 36 | >- [x] Auto-fill current playing song in search 37 | >- [x] Synced Lyrics 38 | >- [x] Batch download lyrics 39 | >- [x] Import and Export saved lyrics 40 | 41 | Checkout planned changes in [RoadMap](https://github.com/shub39/Rush/discussions/113) 42 | 43 | ## Motivation 💭 44 | Spotify removed its feature to see and share lyrics from its free tier just to bring it back again. 45 | So, I made this app to get and store lyrics for my favorite songs from Genius and share them like Spotify, 46 | all in Material 3 look. As an audiophile, This has now become my way to listen to complete albums with lyrics without 47 | dealing with genius's "UI". 48 | 49 | ## Translations 🔠 50 | Translations are done via weblate, you can contribute there! 51 | [Translation status](https://hosted.weblate.org/engage/rush/) 52 | [Translation status](https://hosted.weblate.org/engage/rush/) 53 | 54 | ## References and Inspiration 💡 55 | 56 | >- [Fastlyrics](https://github.com/TecCheck/FastLyrics) 57 | >- [SongSync](https://github.com/Lambada10/SongSync) 58 | >- [LrcLib](https://lrclib.net/) 59 | >- Spotify Lyrics UI 60 | 61 | ## Stargazers over time ✨ 62 | [![Stargazers over time](https://starchart.cc/shub39/Rush.svg?background=%23282828&axis=%23f2dfd3&line=%23ffb780)](https://starchart.cc/shub39/Rush) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -keep class com.shub39.rush.** { *; } -------------------------------------------------------------------------------- /app/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush 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.core.splashscreen.SplashScreen.Companion.installSplashScreen 8 | import org.koin.compose.KoinContext 9 | 10 | class MainActivity : ComponentActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | installSplashScreen() 14 | super.onCreate(savedInstanceState) 15 | 16 | enableEdgeToEdge() 17 | setContent { 18 | KoinContext { 19 | RushApp() 20 | } 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/RushApplication.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush 2 | 3 | import android.app.Application 4 | import com.shub39.rush.di.initKoin 5 | import org.koin.android.ext.koin.androidContext 6 | 7 | class RushApplication: Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | 12 | // Check if androidMain process 13 | if (packageName == getProcessName()) { 14 | initKoin { 15 | androidContext(this@RushApplication) 16 | } 17 | } 18 | 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/core/data/DatastoreFactory.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.core.data 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | 7 | actual class DatastoreFactory(private val context: Context) { 8 | actual fun getLyricsPagePreferencesDataStore() : DataStore = createDataStore ( 9 | producePath = { context.filesDir.resolve(LYRICS_DATASTORE).absolutePath } 10 | ) 11 | 12 | actual fun getOtherPreferencesDataStore(): DataStore = createDataStore ( 13 | producePath = { context.filesDir.resolve(OTHER_DATASTORE).absolutePath } 14 | ) 15 | 16 | actual fun getSharePagePreferencesDataStore(): DataStore = createDataStore( 17 | producePath = { context.filesDir.resolve(SHARE_DATASTORE).absolutePath } 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/core/data/PaletteGenerator.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.core.data 2 | 3 | import android.content.Context 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.asImageBitmap 6 | import androidx.compose.ui.graphics.toArgb 7 | import coil3.ImageLoader 8 | import coil3.request.ImageRequest 9 | import coil3.request.SuccessResult 10 | import coil3.request.allowConversionToBitmap 11 | import coil3.request.allowHardware 12 | import coil3.toBitmap 13 | import com.kmpalette.palette.graphics.Palette 14 | import com.shub39.rush.core.domain.data_classes.ExtractedColors 15 | 16 | actual class PaletteGenerator( 17 | private val context: Context, 18 | private val imageLoader: ImageLoader 19 | ) { 20 | actual suspend fun generatePaletteFromUrl(url: String): ExtractedColors { 21 | val request = ImageRequest.Builder(context) 22 | .data(url) 23 | .allowConversionToBitmap(true) 24 | .allowHardware(false) 25 | .build() 26 | val result = (imageLoader.execute(request) as? SuccessResult)?.image?.toBitmap()?.asImageBitmap() 27 | 28 | return result?.let { bitmap -> 29 | val colors = Palette.from(bitmap).generate() 30 | 31 | ExtractedColors( 32 | cardBackgroundDominant = 33 | Color( 34 | colors.vibrantSwatch?.rgb ?: colors.lightVibrantSwatch?.rgb 35 | ?: colors.darkVibrantSwatch?.rgb ?: colors.dominantSwatch?.rgb 36 | ?: Color.DarkGray.toArgb() 37 | ), 38 | cardContentDominant = 39 | Color( 40 | colors.vibrantSwatch?.bodyTextColor 41 | ?: colors.lightVibrantSwatch?.bodyTextColor 42 | ?: colors.darkVibrantSwatch?.bodyTextColor 43 | ?: colors.dominantSwatch?.bodyTextColor 44 | ?: Color.White.toArgb() 45 | ), 46 | cardBackgroundMuted = 47 | Color( 48 | colors.mutedSwatch?.rgb ?: colors.darkMutedSwatch?.rgb 49 | ?: colors.lightMutedSwatch?.rgb ?: Color.DarkGray.toArgb() 50 | ), 51 | cardContentMuted = 52 | Color( 53 | colors.mutedSwatch?.bodyTextColor 54 | ?: colors.darkMutedSwatch?.bodyTextColor 55 | ?: colors.lightMutedSwatch?.bodyTextColor 56 | ?: Color.White.toArgb() 57 | ) 58 | ) 59 | } ?: ExtractedColors() 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/core/presentation/RushTheme.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.core.presentation 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.res.colorResource 8 | import com.materialkolor.DynamicMaterialTheme 9 | import com.shub39.rush.core.domain.data_classes.Theme 10 | import com.shub39.rush.core.domain.enums.AppTheme 11 | 12 | @Composable 13 | actual fun RushTheme( 14 | state: Theme, 15 | content: @Composable () -> Unit 16 | ) { 17 | DynamicMaterialTheme( 18 | seedColor = if (state.materialTheme && Build.VERSION.SDK_INT > Build.VERSION_CODES.S) { 19 | colorResource(android.R.color.system_accent1_200) 20 | } else { 21 | Color(state.seedColor) 22 | }, 23 | useDarkTheme = when (state.appTheme) { 24 | AppTheme.SYSTEM -> isSystemInDarkTheme() 25 | AppTheme.LIGHT -> false 26 | AppTheme.DARK -> true 27 | }, 28 | withAmoled = state.withAmoled, 29 | style = state.style, 30 | typography = provideTypography( 31 | font = state.fonts.font 32 | ), 33 | content = content 34 | ) 35 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/core/presentation/util.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.core.presentation 2 | 3 | import android.app.Activity 4 | import android.content.ClipData 5 | import android.content.Context 6 | import android.content.ContextWrapper 7 | import android.os.Build 8 | import android.view.WindowManager 9 | import androidx.activity.ComponentActivity 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.DisposableEffect 12 | import androidx.compose.ui.platform.ClipEntry 13 | import androidx.compose.ui.platform.Clipboard 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.core.view.WindowCompat 16 | import androidx.core.view.WindowInsetsCompat 17 | import androidx.core.view.WindowInsetsControllerCompat 18 | 19 | actual fun hypnoticAvailable() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 20 | 21 | @Composable 22 | actual fun KeepScreenOn() { 23 | val context = LocalContext.current 24 | 25 | DisposableEffect(Unit) { 26 | (context as? ComponentActivity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 27 | onDispose { 28 | (context as? ComponentActivity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 29 | } 30 | } 31 | } 32 | 33 | actual suspend fun Clipboard.copyToClipboard(text: String) { 34 | setClipEntry( 35 | ClipEntry( 36 | ClipData.newPlainText("lyrics", text) 37 | ) 38 | ) 39 | } 40 | 41 | fun Context.findActivity(): Activity? { 42 | var context = this 43 | while (context is ContextWrapper) { 44 | if (context is Activity) return context 45 | context = context.baseContext 46 | } 47 | return null 48 | } 49 | 50 | fun updateSystemBars(context: Context, show: Boolean) { 51 | val window = context.findActivity()?.window ?: return 52 | val insetsController = WindowCompat.getInsetsController(window, window.decorView) 53 | 54 | insetsController.apply { 55 | if (show) { 56 | show(WindowInsetsCompat.Type.systemBars()) 57 | systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT 58 | } else { 59 | hide(WindowInsetsCompat.Type.systemBars()) 60 | systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/di/RushModules.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.di 2 | 3 | import com.shub39.rush.core.data.DatastoreFactory 4 | import com.shub39.rush.core.data.PaletteGenerator 5 | import com.shub39.rush.lyrics.data.backup.ExportImpl 6 | import com.shub39.rush.lyrics.data.backup.RestoreImpl 7 | import com.shub39.rush.lyrics.data.database.DatabaseFactory 8 | import com.shub39.rush.lyrics.data.listener.MediaListenerImpl 9 | import com.shub39.rush.lyrics.domain.MediaInterface 10 | import com.shub39.rush.lyrics.domain.backup.ExportRepo 11 | import com.shub39.rush.lyrics.domain.backup.RestoreRepo 12 | import com.shub39.rush.lyrics.presentation.viewmodels.SettingsVM 13 | import com.shub39.rush.lyrics.presentation.viewmodels.ShareVM 14 | import org.koin.core.module.dsl.singleOf 15 | import org.koin.core.module.dsl.viewModelOf 16 | import org.koin.dsl.bind 17 | import org.koin.dsl.module 18 | 19 | actual val platformModule = module { 20 | singleOf(::DatabaseFactory) 21 | singleOf(::DatastoreFactory) 22 | singleOf(::ExportImpl).bind() 23 | singleOf(::RestoreImpl).bind() 24 | singleOf(::PaletteGenerator) 25 | singleOf(::MediaListenerImpl).bind() 26 | 27 | // android specific 28 | viewModelOf(::ShareVM) 29 | viewModelOf(::SettingsVM) 30 | singleOf(::provideImageLoader) 31 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/di/util.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.di 2 | 3 | import android.content.Context 4 | import coil3.ImageLoader 5 | import coil3.disk.DiskCache 6 | import coil3.request.CachePolicy 7 | import coil3.request.crossfade 8 | import okio.Path.Companion.toOkioPath 9 | 10 | fun provideImageLoader(context: Context): ImageLoader { 11 | return ImageLoader.Builder(context) 12 | .crossfade(true) 13 | .memoryCachePolicy(CachePolicy.ENABLED) 14 | .diskCachePolicy(CachePolicy.ENABLED) 15 | .diskCache { 16 | DiskCache.Builder() 17 | .directory(context.cacheDir.resolve("image_cache").toOkioPath()) 18 | .maxSizePercent(0.02) 19 | .build() 20 | } 21 | .build() 22 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/data/backup/ExportImpl.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.data.backup 2 | 3 | import android.os.Environment 4 | import com.shub39.rush.lyrics.data.mappers.toSongSchema 5 | import com.shub39.rush.lyrics.domain.SongRepo 6 | import com.shub39.rush.lyrics.domain.backup.ExportRepo 7 | import com.shub39.rush.lyrics.domain.backup.ExportSchema 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.async 10 | import kotlinx.coroutines.coroutineScope 11 | import kotlinx.coroutines.withContext 12 | import kotlinx.datetime.Clock 13 | import kotlinx.datetime.TimeZone 14 | import kotlinx.datetime.toLocalDateTime 15 | import kotlinx.serialization.json.Json 16 | import java.io.File 17 | 18 | actual class ExportImpl( 19 | private val songRepo: SongRepo 20 | ): ExportRepo { 21 | override suspend fun exportToJson() = coroutineScope { 22 | val songsData = async { 23 | withContext(Dispatchers.IO) { 24 | songRepo.getAllSongs().map { it.toSongSchema() } 25 | } 26 | } 27 | val exportFolder = File( 28 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), 29 | "Rush" 30 | ) 31 | 32 | if (!exportFolder.exists() || !exportFolder.isDirectory) exportFolder.mkdirs() 33 | 34 | val time = 35 | Clock.System.now().toLocalDateTime(TimeZone.Companion.UTC).toString().replace(":", "") 36 | .replace(" ", "") 37 | val file = File(exportFolder, "Rush-Export-$time.json") 38 | 39 | val songs = songsData.await() 40 | 41 | file.writeText( 42 | Json.Default.encodeToString( 43 | ExportSchema( 44 | schemaVersion = 3, 45 | songs = songs 46 | ) 47 | ) 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/data/backup/RestoreImpl.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.data.backup 2 | 3 | import android.content.Context 4 | import androidx.core.net.toUri 5 | import com.shub39.rush.lyrics.data.mappers.toSong 6 | import com.shub39.rush.lyrics.domain.SongRepo 7 | import com.shub39.rush.lyrics.domain.backup.ExportSchema 8 | import com.shub39.rush.lyrics.domain.backup.RestoreFailedException 9 | import com.shub39.rush.lyrics.domain.backup.RestoreRepo 10 | import com.shub39.rush.lyrics.domain.backup.RestoreResult 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.async 13 | import kotlinx.coroutines.awaitAll 14 | import kotlinx.coroutines.withContext 15 | import kotlinx.serialization.SerializationException 16 | import kotlinx.serialization.json.Json 17 | import kotlin.io.path.createTempFile 18 | import kotlin.io.path.outputStream 19 | import kotlin.io.path.readText 20 | 21 | actual class RestoreImpl( 22 | private val songRepo: SongRepo, 23 | private val context: Context 24 | ): RestoreRepo { 25 | override suspend fun restoreSongs(path: String): RestoreResult { 26 | return try { 27 | val file = createTempFile() 28 | 29 | context.contentResolver.openInputStream(path.toUri()).use { input -> 30 | file.outputStream().use { output -> 31 | input?.copyTo(output) 32 | } 33 | } 34 | 35 | val json = Json { 36 | ignoreUnknownKeys = true 37 | } 38 | 39 | val jsonDeserialized = json.decodeFromString(file.readText()) 40 | 41 | withContext(Dispatchers.IO) { 42 | awaitAll( 43 | async { 44 | val songs = jsonDeserialized.songs.map { it.toSong() } 45 | 46 | songs.forEach { 47 | songRepo.insertSong(it) 48 | } 49 | } 50 | ) 51 | } 52 | 53 | RestoreResult.Success 54 | } catch (e: IllegalArgumentException) { 55 | e.printStackTrace() 56 | RestoreResult.Failure(RestoreFailedException.InvalidFile) 57 | } catch (e: SerializationException) { 58 | e.printStackTrace() 59 | RestoreResult.Failure(RestoreFailedException.OldSchema) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/data/database/DatabaseFactory.android.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.data.database 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | 7 | actual class DatabaseFactory( 8 | private val context: Context 9 | ) { 10 | actual fun create(): RoomDatabase.Builder { 11 | val appContext = context.applicationContext 12 | val dbFile = appContext.getDatabasePath(SongDatabase.DB_NAME) 13 | 14 | return Room.databaseBuilder(appContext, dbFile.absolutePath) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/data/listener/NotificationListener.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.data.listener 2 | 3 | import android.content.Context 4 | import android.service.notification.NotificationListenerService 5 | import androidx.core.app.NotificationManagerCompat 6 | 7 | class NotificationListener : NotificationListenerService() { 8 | 9 | companion object { 10 | fun canAccessNotifications(context: Context): Boolean { 11 | return NotificationManagerCompat.getEnabledListenerPackages(context) 12 | .contains(context.packageName) 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/LyricsGraph.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation 2 | 3 | import androidx.compose.animation.fadeIn 4 | import androidx.compose.animation.fadeOut 5 | import androidx.compose.foundation.layout.widthIn 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.SideEffect 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | import androidx.navigation.compose.rememberNavController 14 | import com.shub39.rush.core.presentation.updateSystemBars 15 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsCustomisationsPage 16 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsPage 17 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsPageAction 18 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsPageState 19 | import com.shub39.rush.lyrics.presentation.share.SharePage 20 | import com.shub39.rush.lyrics.presentation.share.SharePageAction 21 | import com.shub39.rush.lyrics.presentation.share.SharePageState 22 | import kotlinx.serialization.Serializable 23 | 24 | private sealed interface LyricsRoutes { 25 | @Serializable 26 | data object LyricsPage : LyricsRoutes 27 | 28 | @Serializable 29 | data object LyricsCustomisations : LyricsRoutes 30 | 31 | @Serializable 32 | data object SharePage : LyricsRoutes 33 | } 34 | 35 | @Composable 36 | fun LyricsGraph( 37 | notificationAccess: Boolean, 38 | lyricsState: LyricsPageState, 39 | shareState: SharePageState, 40 | lyricsAction: (LyricsPageAction) -> Unit, 41 | shareAction: (SharePageAction) -> Unit 42 | ) { 43 | val context = LocalContext.current 44 | val navController = rememberNavController() 45 | 46 | NavHost( 47 | navController = navController, 48 | startDestination = LyricsRoutes.LyricsPage, 49 | enterTransition = { fadeIn() }, 50 | exitTransition = { fadeOut() }, 51 | popEnterTransition = { fadeIn() }, 52 | popExitTransition = { fadeOut() } 53 | ) { 54 | composable { 55 | SideEffect { 56 | if (lyricsState.fullscreen) { 57 | updateSystemBars(context, false) 58 | } 59 | } 60 | 61 | LyricsPage( 62 | onEdit = { 63 | navController.navigate(LyricsRoutes.LyricsCustomisations) { 64 | launchSingleTop = true 65 | } 66 | }, 67 | onShare = { 68 | navController.navigate(LyricsRoutes.SharePage) { 69 | launchSingleTop = true 70 | } 71 | }, 72 | action = lyricsAction, 73 | state = lyricsState, 74 | notificationAccess = notificationAccess 75 | ) 76 | } 77 | 78 | composable { 79 | SideEffect { 80 | if (lyricsState.fullscreen) { 81 | updateSystemBars(context, true) 82 | } 83 | } 84 | 85 | SharePage( 86 | onDismiss = { navController.navigateUp() }, 87 | state = shareState, 88 | action = shareAction 89 | ) 90 | } 91 | 92 | composable { 93 | SideEffect { 94 | if (lyricsState.fullscreen) { 95 | updateSystemBars(context, true) 96 | } 97 | } 98 | 99 | LyricsCustomisationsPage( 100 | state = lyricsState, 101 | onNavigateBack = { navController.navigateUp() }, 102 | action = lyricsAction, 103 | modifier = Modifier.widthIn(max = 700.dp) 104 | ) 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/SettingsGraph.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation 2 | 3 | import androidx.compose.animation.fadeIn 4 | import androidx.compose.animation.fadeOut 5 | import androidx.compose.animation.slideInHorizontally 6 | import androidx.compose.animation.slideOutHorizontally 7 | import androidx.compose.runtime.Composable 8 | import androidx.navigation.compose.NavHost 9 | import androidx.navigation.compose.composable 10 | import androidx.navigation.compose.rememberNavController 11 | import com.shub39.rush.lyrics.presentation.setting.AboutAppPage 12 | import com.shub39.rush.lyrics.presentation.setting.AboutLibrariesPage 13 | import com.shub39.rush.lyrics.presentation.setting.BackupPage 14 | import com.shub39.rush.lyrics.presentation.setting.BatchDownloader 15 | import com.shub39.rush.lyrics.presentation.setting.LookAndFeelPage 16 | import com.shub39.rush.lyrics.presentation.setting.SettingRootPage 17 | import com.shub39.rush.lyrics.presentation.setting.SettingsPageAction 18 | import com.shub39.rush.lyrics.presentation.setting.SettingsPageState 19 | import kotlinx.serialization.Serializable 20 | 21 | sealed interface SettingsRoutes { 22 | @Serializable 23 | data object SettingRootPage : SettingsRoutes 24 | 25 | @Serializable 26 | data object BatchDownloaderPage : SettingsRoutes 27 | 28 | @Serializable 29 | data object BackupPage : SettingsRoutes 30 | 31 | @Serializable 32 | data object AboutPage : SettingsRoutes 33 | 34 | @Serializable 35 | data object LookAndFeelPage : SettingsRoutes 36 | 37 | @Serializable 38 | data object AboutLibrariesPage : SettingsRoutes 39 | } 40 | 41 | @Composable 42 | fun SettingsGraph( 43 | notificationAccess: Boolean, 44 | state: SettingsPageState, 45 | action: (SettingsPageAction) -> Unit 46 | ) { 47 | val navController = rememberNavController() 48 | 49 | NavHost( 50 | navController = navController, 51 | startDestination = SettingsRoutes.SettingRootPage, 52 | enterTransition = { 53 | slideInHorizontally(initialOffsetX = { it }) + fadeIn() 54 | }, 55 | exitTransition = { 56 | slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() 57 | }, 58 | popEnterTransition = { 59 | slideInHorizontally(initialOffsetX = { -it }) + fadeIn() 60 | }, 61 | popExitTransition = { 62 | slideOutHorizontally(targetOffsetX = { it }) + fadeOut() 63 | } 64 | ) { 65 | composable { 66 | SettingRootPage( 67 | notificationAccess = notificationAccess, 68 | action = action, 69 | navigator = { navController.navigate(it) { launchSingleTop = true } } 70 | ) 71 | } 72 | 73 | composable { 74 | BatchDownloader( 75 | state = state, 76 | action = action 77 | ) 78 | } 79 | 80 | composable { 81 | BackupPage( 82 | state = state, 83 | action = action 84 | ) 85 | } 86 | 87 | composable { 88 | AboutAppPage( 89 | onNavigateToLibraries = { 90 | navController.navigate(SettingsRoutes.AboutLibrariesPage) { 91 | launchSingleTop = true 92 | } 93 | } 94 | ) 95 | } 96 | 97 | composable { 98 | LookAndFeelPage( 99 | state = state, 100 | action = action 101 | ) 102 | } 103 | 104 | composable { 105 | AboutLibrariesPage() 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/setting/AboutLibrariesPage.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.setting 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.widthIn 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Scaffold 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TopAppBar 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer 15 | import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults 16 | import com.mikepenz.aboutlibraries.ui.compose.libraryColors 17 | import com.shub39.rush.core.presentation.PageFill 18 | import org.jetbrains.compose.resources.stringResource 19 | import rush.app.generated.resources.Res 20 | import rush.app.generated.resources.about_libraries 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun AboutLibrariesPage() = PageFill { 25 | Scaffold( 26 | modifier = Modifier.widthIn(max = 500.dp), 27 | topBar = { 28 | TopAppBar( 29 | title = { Text(stringResource(Res.string.about_libraries)) } 30 | ) 31 | } 32 | ) { padding -> 33 | LibrariesContainer( 34 | modifier = Modifier 35 | .padding(padding) 36 | .fillMaxSize(), 37 | colors = LibraryDefaults.libraryColors( 38 | backgroundColor = MaterialTheme.colorScheme.background, 39 | contentColor = MaterialTheme.colorScheme.onBackground, 40 | badgeBackgroundColor = MaterialTheme.colorScheme.primary, 41 | badgeContentColor = MaterialTheme.colorScheme.onPrimary, 42 | dialogConfirmButtonColor = MaterialTheme.colorScheme.primary 43 | ) 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/setting/SettingsPageAction.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.setting 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import com.materialkolor.PaletteStyle 6 | import com.shub39.rush.core.domain.enums.AppTheme 7 | import com.shub39.rush.core.domain.enums.Fonts 8 | 9 | sealed interface SettingsPageAction { 10 | data class OnSeedColorChange(val color: Int): SettingsPageAction 11 | data class OnThemeSwitch(val appTheme: AppTheme): SettingsPageAction 12 | data class OnAmoledSwitch(val amoled: Boolean): SettingsPageAction 13 | data class OnPaletteChange(val style: PaletteStyle): SettingsPageAction 14 | data class OnMaterialThemeToggle(val pref: Boolean): SettingsPageAction 15 | data class OnProcessAudioFiles(val context: Context, val uri: Uri): SettingsPageAction 16 | data class OnFontChange(val fonts: Fonts): SettingsPageAction 17 | data object OnClearIndexes: SettingsPageAction 18 | data object OnBatchDownload: SettingsPageAction 19 | data object OnDeleteSongs: SettingsPageAction 20 | data object ResetBackup: SettingsPageAction 21 | data class OnRestoreSongs(val path: String): SettingsPageAction 22 | data object OnExportSongs: SettingsPageAction 23 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/setting/component/DownloaderCard.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.setting.component 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Warning 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.ListItem 9 | import androidx.compose.material3.ListItemColors 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.text.style.TextOverflow 16 | import androidx.compose.ui.unit.dp 17 | import com.shub39.rush.lyrics.domain.AudioFile 18 | import compose.icons.FontAwesomeIcons 19 | import compose.icons.fontawesomeicons.Solid 20 | import compose.icons.fontawesomeicons.solid.CheckCircle 21 | import compose.icons.fontawesomeicons.solid.SyncAlt 22 | 23 | @Composable 24 | fun DownloaderCard( 25 | audioFile: AudioFile, 26 | state: Boolean?, 27 | listItemColors: ListItemColors 28 | ) { 29 | ListItem( 30 | headlineContent = { 31 | Column { 32 | Text( 33 | text = audioFile.title, 34 | maxLines = 2, 35 | overflow = TextOverflow.Ellipsis 36 | ) 37 | 38 | Text( 39 | text = audioFile.artist, 40 | maxLines = 1, 41 | overflow = TextOverflow.Ellipsis 42 | ) 43 | } 44 | }, 45 | trailingContent = { 46 | when (state) { 47 | true -> Icon( 48 | imageVector = FontAwesomeIcons.Solid.CheckCircle, 49 | contentDescription = "Done", 50 | modifier = Modifier.size(20.dp) 51 | ) 52 | 53 | null -> Icon( 54 | imageVector = FontAwesomeIcons.Solid.SyncAlt, 55 | contentDescription = "Sync", 56 | modifier = Modifier.size(20.dp) 57 | ) 58 | 59 | else -> Icon( 60 | imageVector = Icons.Default.Warning, 61 | contentDescription = "Error", 62 | modifier = Modifier.size(20.dp) 63 | ) 64 | } 65 | }, 66 | colors = listItemColors, 67 | modifier = Modifier.clip(MaterialTheme.shapes.large) 68 | ) 69 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/setting/util.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.setting 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.material3.ListItemColors 5 | import androidx.compose.material3.ListItemDefaults 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | 10 | @Composable 11 | fun stateListColors( 12 | state: Boolean? 13 | ): ListItemColors { 14 | val cardContent by animateColorAsState( 15 | targetValue = when (state) { 16 | null -> MaterialTheme.colorScheme.primary 17 | true -> MaterialTheme.colorScheme.onSecondary 18 | else -> MaterialTheme.colorScheme.error 19 | }, label = "status" 20 | ) 21 | 22 | val cardBackground by animateColorAsState( 23 | targetValue = when (state) { 24 | null -> MaterialTheme.colorScheme.surface 25 | true -> MaterialTheme.colorScheme.onSecondaryContainer 26 | else -> MaterialTheme.colorScheme.errorContainer 27 | }, label = "status" 28 | ) 29 | 30 | return ListItemDefaults.colors( 31 | containerColor = cardBackground, 32 | headlineColor = cardContent, 33 | trailingIconColor = cardContent 34 | ) 35 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/share/SharePageAction.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.share 2 | 3 | import com.shub39.rush.core.domain.enums.CardColors 4 | import com.shub39.rush.core.domain.enums.CardFit 5 | import com.shub39.rush.core.domain.enums.CardTheme 6 | import com.shub39.rush.core.domain.enums.CornerRadius 7 | import com.shub39.rush.core.domain.enums.Fonts 8 | 9 | sealed interface SharePageAction { 10 | data class OnUpdateCardTheme(val theme: CardTheme) : SharePageAction 11 | data class OnUpdateCardColor(val color: CardColors) : SharePageAction 12 | data class OnUpdateCardFit(val fit: CardFit) : SharePageAction 13 | data class OnUpdateCardRoundness(val roundness: CornerRadius) : SharePageAction 14 | data class OnUpdateCardContent(val color: Int): SharePageAction 15 | data class OnUpdateCardBackground(val color: Int): SharePageAction 16 | data class OnUpdateCardFont(val font: Fonts) : SharePageAction 17 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/share/component/HypnoticShareCard.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.share.component 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.CardColors 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.clip 10 | import androidx.compose.ui.graphics.Brush 11 | import androidx.compose.ui.graphics.Color 12 | import com.materialkolor.ktx.darken 13 | import com.materialkolor.ktx.lighten 14 | import com.mikepenz.hypnoticcanvas.shaderBackground 15 | import com.mikepenz.hypnoticcanvas.shaders.MeshGradient 16 | import com.shub39.rush.core.domain.data_classes.SongDetails 17 | import com.shub39.rush.core.domain.enums.CardFit 18 | import com.shub39.rush.core.presentation.generateGradientColors 19 | 20 | @Composable 21 | fun HypnoticShareCard( 22 | modifier: Modifier, 23 | song: SongDetails, 24 | sortedLines: Map, 25 | cardColors: CardColors, 26 | cardCorners: RoundedCornerShape, 27 | fit: CardFit 28 | ) { 29 | Box(modifier = modifier.clip(cardCorners)) { 30 | SpotifyShareCard( 31 | modifier = Modifier 32 | .fillMaxWidth() 33 | .shaderBackground( 34 | MeshGradient( 35 | colors = generateGradientColors( 36 | cardColors.containerColor.lighten(2f), 37 | cardColors.containerColor.darken(2f) 38 | ).toTypedArray() 39 | ), 40 | fallback = { 41 | Brush.horizontalGradient( 42 | generateGradientColors( 43 | cardColors.containerColor.lighten(2f), 44 | cardColors.containerColor.darken(2f) 45 | ) 46 | ) 47 | } 48 | ), 49 | song = song, 50 | sortedLines = sortedLines, 51 | cardColors = cardColors.copy(containerColor = Color.Transparent), 52 | cardCorners = cardCorners, 53 | fit = fit 54 | ) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/share/component/ListSelect.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.share.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.FlowRow 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.InputChip 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.unit.dp 15 | 16 | @Composable 17 | fun ListSelect( 18 | title: String, 19 | options: List, 20 | selected: T, 21 | onSelectedChange: (T) -> Unit, 22 | labelProvider: @Composable (T) -> Unit 23 | ) { 24 | Column( 25 | modifier = Modifier.fillMaxWidth(), 26 | horizontalAlignment = Alignment.CenterHorizontally, 27 | verticalArrangement = Arrangement.spacedBy(8.dp) 28 | ) { 29 | Text( 30 | text = title, 31 | style = MaterialTheme.typography.titleMedium 32 | ) 33 | 34 | FlowRow( 35 | horizontalArrangement = Arrangement.Center 36 | ) { 37 | options.forEach { option -> 38 | InputChip( 39 | modifier = Modifier.padding(horizontal = 4.dp), 40 | selected = option == selected, 41 | onClick = { onSelectedChange(option) }, 42 | label = { labelProvider(option) } 43 | ) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/share/component/QuoteShareCard.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.share.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Card 11 | import androidx.compose.material3.CardColors 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 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.platform.LocalDensity 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.style.TextOverflow 22 | import androidx.compose.ui.unit.dp 23 | import com.shub39.rush.core.domain.data_classes.SongDetails 24 | import com.shub39.rush.core.domain.enums.CardFit 25 | import com.shub39.rush.core.presentation.ArtFromUrl 26 | import compose.icons.FontAwesomeIcons 27 | import compose.icons.fontawesomeicons.Solid 28 | import compose.icons.fontawesomeicons.solid.QuoteLeft 29 | 30 | @Composable 31 | fun QuoteShareCard( 32 | modifier: Modifier, 33 | song: SongDetails, 34 | sortedLines: Map, 35 | cardColors: CardColors, 36 | cardCorners: RoundedCornerShape, 37 | fit: CardFit 38 | ) { 39 | Card( 40 | modifier = modifier, 41 | colors = cardColors, 42 | shape = cardCorners 43 | ) { 44 | Column( 45 | modifier = Modifier 46 | .padding(32.dp) 47 | .let { 48 | if (fit == CardFit.STANDARD) { 49 | it.weight(1f) 50 | } else it 51 | }, 52 | verticalArrangement = Arrangement.Center 53 | ) { 54 | Icon( 55 | imageVector = FontAwesomeIcons.Solid.QuoteLeft, 56 | contentDescription = "Quote", 57 | modifier = Modifier.size(30.dp) 58 | ) 59 | 60 | Spacer(modifier = Modifier.padding(8.dp)) 61 | 62 | Text( 63 | text = sortedLines.values.firstOrNull() ?: "Woah...", 64 | style = MaterialTheme.typography.displayMedium, 65 | fontWeight = FontWeight.ExtraBold, 66 | lineHeight = with(LocalDensity.current) { 100.toSp() }, 67 | fontSize = with(LocalDensity.current) { 80.toSp() } 68 | ) 69 | 70 | Spacer(modifier = Modifier.padding(32.dp)) 71 | 72 | Row( 73 | verticalAlignment = Alignment.CenterVertically 74 | ) { 75 | ArtFromUrl( 76 | imageUrl = song.artUrl, 77 | modifier = Modifier 78 | .size(50.dp) 79 | .clip(MaterialTheme.shapes.small) 80 | ) 81 | 82 | Column( 83 | modifier = Modifier.padding(horizontal = 16.dp) 84 | ) { 85 | Text( 86 | text = song.title, 87 | style = MaterialTheme.typography.titleMedium, 88 | fontWeight = FontWeight.ExtraBold, 89 | fontSize = with(LocalDensity.current) { 40.toSp() }, 90 | maxLines = 1, 91 | overflow = TextOverflow.Ellipsis 92 | ) 93 | 94 | Text( 95 | text = song.artist, 96 | style = MaterialTheme.typography.bodySmall, 97 | fontWeight = FontWeight.Bold, 98 | fontSize = with(LocalDensity.current) { 35.toSp() }, 99 | maxLines = 1, 100 | overflow = TextOverflow.Ellipsis 101 | ) 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/share/component/SpotifyShareCard.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.share.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxHeight 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardColors 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.platform.LocalDensity 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.text.style.TextOverflow 23 | import androidx.compose.ui.unit.dp 24 | import com.shub39.rush.core.domain.data_classes.SongDetails 25 | import com.shub39.rush.core.domain.enums.CardFit 26 | import com.shub39.rush.core.presentation.ArtFromUrl 27 | 28 | @Composable 29 | fun SpotifyShareCard( 30 | modifier: Modifier, 31 | song: SongDetails, 32 | sortedLines: Map, 33 | cardColors: CardColors, 34 | cardCorners: RoundedCornerShape, 35 | fit: CardFit 36 | ) { 37 | Card( 38 | modifier = modifier, 39 | shape = cardCorners, 40 | colors = cardColors 41 | ) { 42 | Column( 43 | modifier = Modifier 44 | .padding(32.dp) 45 | .let { 46 | if (fit == CardFit.STANDARD) { 47 | it.fillMaxHeight() 48 | } else it 49 | }, 50 | verticalArrangement = Arrangement.Center 51 | ) { 52 | Row( 53 | verticalAlignment = Alignment.CenterVertically 54 | ) { 55 | ArtFromUrl( 56 | imageUrl = song.artUrl, 57 | modifier = Modifier 58 | .size(50.dp) 59 | .clip(MaterialTheme.shapes.small) 60 | ) 61 | 62 | Column( 63 | modifier = Modifier.padding(horizontal = 16.dp) 64 | ) { 65 | Text( 66 | text = song.title, 67 | style = MaterialTheme.typography.titleMedium, 68 | fontWeight = FontWeight.ExtraBold, 69 | fontSize = with(LocalDensity.current) { 35.toSp() }, 70 | maxLines = 1, 71 | overflow = TextOverflow.Ellipsis 72 | ) 73 | 74 | Text( 75 | text = song.artist, 76 | style = MaterialTheme.typography.bodySmall, 77 | fontWeight = FontWeight.Bold, 78 | fontSize = with(LocalDensity.current) { 30.toSp() }, 79 | maxLines = 1, 80 | overflow = TextOverflow.Ellipsis 81 | ) 82 | } 83 | } 84 | 85 | Spacer(modifier = Modifier.padding(8.dp)) 86 | 87 | LazyColumn { 88 | sortedLines.forEach { 89 | item { 90 | Text( 91 | text = it.value, 92 | style = MaterialTheme.typography.bodyMedium, 93 | fontWeight = FontWeight.Bold, 94 | fontSize = with(LocalDensity.current) { 45.toSp() }, 95 | modifier = Modifier.padding(bottom = 10.dp) 96 | ) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/androidMain/kotlin/com/shub39/rush/lyrics/presentation/share/util.kt: -------------------------------------------------------------------------------- 1 | package com.shub39.rush.lyrics.presentation.share 2 | 3 | import android.content.ContentValues 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Bitmap 7 | import android.net.Uri 8 | import android.os.Environment 9 | import android.provider.MediaStore 10 | import android.widget.Toast 11 | import androidx.core.content.FileProvider 12 | import kotlinx.datetime.Clock 13 | import kotlinx.datetime.TimeZone 14 | import kotlinx.datetime.toLocalDateTime 15 | import java.io.File 16 | import java.io.FileOutputStream 17 | import java.io.IOException 18 | 19 | fun isValidFilename(filename: String): Boolean { 20 | val invalidCharsPattern = Regex("[/\\\\:*?\"<>|\u0000\r\n]") 21 | return !invalidCharsPattern.containsMatchIn(filename) 22 | && filename.length <= 50 23 | && filename.isNotBlank() 24 | && filename.isNotEmpty() 25 | && filename.endsWith(".png") 26 | } 27 | 28 | /* 29 | * Converts a given Bitmap into a png image and opens dialog to share the image 30 | * if shareToPictures is True then it stores the image in /Pictures/Rush in internal storage 31 | */ 32 | fun shareImage(context: Context, bitmap: Bitmap, name: String, saveToPictures: Boolean = false) { 33 | val file: File = if (saveToPictures) { 34 | val resolver = context.contentResolver 35 | val contentValues = ContentValues().apply { 36 | put(MediaStore.MediaColumns.DISPLAY_NAME, name) 37 | put(MediaStore.MediaColumns.MIME_TYPE, "image/png") 38 | put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Rush") 39 | } 40 | val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) 41 | uri?.let { 42 | val stream = resolver.openOutputStream(it) 43 | if (stream != null) { 44 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) 45 | } 46 | stream?.close() 47 | } ?: run { 48 | Toast.makeText(context, "Error saving image", Toast.LENGTH_SHORT).show() 49 | return 50 | } 51 | File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), name) 52 | } else { 53 | val cachePath = File(context.cacheDir, "images") 54 | cachePath.mkdirs() 55 | File(cachePath, name) 56 | } 57 | 58 | try { 59 | if (!saveToPictures) { 60 | val stream = FileOutputStream(file) 61 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) 62 | stream.close() 63 | } 64 | } catch (e: IOException) { 65 | e.printStackTrace() 66 | return 67 | } 68 | 69 | if (saveToPictures) { 70 | Toast.makeText(context, "Image saved to Pictures/$name", Toast.LENGTH_SHORT).show() 71 | } else { 72 | val contentUri: Uri = 73 | FileProvider.getUriForFile(context, "${context.packageName}.provider", file) 74 | val shareIntent = Intent().apply { 75 | action = Intent.ACTION_SEND 76 | putExtra(Intent.EXTRA_STREAM, contentUri) 77 | type = "image/png" 78 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 79 | } 80 | context.startActivity(Intent.createChooser(shareIntent, "Share image using")) 81 | } 82 | } 83 | 84 | fun getFormattedTime(): String { 85 | val now = Clock.System.now() 86 | val localTime = now.toLocalDateTime(TimeZone.currentSystemDefault()).time 87 | 88 | val hour = localTime.hour % 12 89 | val minute = localTime.minute 90 | val amPm = if (localTime.hour < 12) "AM" else "PM" 91 | 92 | val hourFormatted = if (hour == 0) 12 else hour 93 | val minuteFormatted = minute.toString().padStart(2, '0') 94 | 95 | return "$hourFormatted:$minuteFormatted $amPm" 96 | } -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/androidMain/res/values-night/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/androidMain/res/values/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/androidMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |