├── .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 | [
]()
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 | > [
](https://play.google.com/store/apps/details?id=com.shub39.rush.play)
18 | > [
](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 | |  |  |
27 | |:-------------------------------------------------------------------:|:-------------------------------------------------------------------:|
28 | |  |  |
29 | |  |  |
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 | [
](https://hosted.weblate.org/engage/rush/)
52 | [
](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 | [](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 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/androidMain/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/androidMain/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/androidMain/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/drawable/genius.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/drawable/rush_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/drawable/rush_transparent.png
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/dm_sans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/dm_sans.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/figtree.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/figtree.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/inter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/inter.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/jost.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/jost.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/manrope.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/manrope.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/montserrat.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/montserrat.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/open_sans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/open_sans.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/outfit.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/outfit.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/poppins_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/poppins_regular.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/font/quicksand.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/app/src/commonMain/composeResources/font/quicksand.ttf
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/values-et/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Otsi laulusõnu
4 | Salvestatud
5 | Seadistused
6 | Siin on tühjavõitu
7 | Jaga
8 | Kasuta elavaid värvide
9 | Salvesta muudatused
10 | Kas sa oled kindel, et soovid kõik salvestatud laulusõnad kustutada?
11 |
12 |
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/values-fa/strings.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 | نام (A-Z)
27 | نام (Z-A)
28 | گروه بندی بر پایه هنرمندان
29 | گروه بندی بر پایه آلبوم
30 | بارگیری دستهای متن ترانهها
31 | انتخاب پوشه
32 | بارگیری
33 | انجام شد
34 | در حال بارگیری متن ترانه
35 | در حال جستجوی
36 | مشکلی پیش آمد…
37 | دوباره تلاش میکنید؟
38 | متن ترانه صحیح
39 | اینترنت ندارید!!
40 | درخواست ناموفق بود
41 | هیچ نتیجهای نداشت
42 | خطای تجزیه
43 | خطای شبکه
44 | خطایی رخ داد
45 | پوسته کارت
46 | رنگ کارت
47 | اندازه کارت
48 | گوشههای کارت
49 | بهکارگیری رنگهای زنده تر برای صفحه متن ترانه
50 | پشتیبان گیری
51 | وارد یا صادر کردن پایگاه داده متن ترانههای ذخیره شده
52 | بارگیری متن ترانهها برای آهنگهای محلی ذخیره شده به صورت یکجا.
53 | آخرین افزودهه
54 | نشان Rush
55 | صادر کردن
56 | صادر شد
57 | بازیابی ترانهها
58 | ترانهها بازیابی شدند
59 | بازیابی ناموفق بود
60 | درباره
61 | هیچ پرونده صوتی نیست
62 | هیچ پوشهای انتخاب نشده
63 | صادر کردن
64 | صادر کردن ترانهها به پوشه دریافتها/Rush
65 | بازیابی
66 | بازیابی از پرونده صادرات انتخاب شده
67 | انتخاب فایل
68 |
69 |
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Cerca testi
4 | Salvati
5 | Impostazioni
6 | Niente qui
7 | Salva modifiche
8 | Sei sicuro di voler eliminare tutti i testi salvati?
9 | Non è stato possibile recuperare il testo
10 | Prova a cercare qualcosa
11 | Testi salvati
12 | Rimuovi tutti i testi
13 | Servizio di ascolto delle notifiche
14 | Album sconosciuto
15 | Larghezza
16 | Colori
17 | Dai l\'accesso alle notifiche
18 | L\'accesso alle notifiche è necessario per trovare automaticamente i testi delle canzoni che stai ascoltando. Riavvia l\'app dopo aver dato il permesso.
19 | Angoli arrotondati
20 | Linee massime condivisibili
21 | Tema dell\'app
22 | Titolo (A-Z)
23 | Titolo (Z-A)
24 | Raggruppati per Artista
25 | Raggruppati per Album
26 | traccia
27 | artista
28 | Seleziona una cartella
29 | Ultimi aggiunti
30 | Scarica i testi in lotto
31 | Importa o esporta la base di dati dei testi salvati
32 | Finito
33 | Scarica i testi per le tue canzoni salvate localmente in una sola volta.
34 | Caricamento testi per
35 | Condividi
36 | Scarica
37 | Backup
38 |
39 |
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 歌詞を見つける
4 | 保存済み
5 | 設定
6 | 何もありません
7 | 変更内容を保存
8 | 保存した歌詞を全て削除してもよろしいですか?
9 | 歌詞を取得出来ませんでした
10 | 何か見つけてみませんか?
11 | 保存した歌詞
12 | 全ての歌詞を削除
13 | 通知リスナーサービス
14 | 不明なアルバム
15 | 幅
16 | 色
17 | 通知アクセスを許可
18 | Notification Access is required to get lyrics automatically. Clear and restart the app after granting permission.
19 | 丸い角
20 | 共有できる最大行数
21 | アプリのテーマ
22 | タイトル(A-Z)
23 | タイトル (Z-A)
24 | アーティスト別にグループ化
25 | アルバム別にグループ化
26 | ダウンロード
27 | エラーが発生しました
28 | エラーが発生しました……
29 | ソース
30 | Spotify
31 |
32 |
--------------------------------------------------------------------------------
/app/src/commonMain/composeResources/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pesquisar letras
4 | Salvos
5 | Configurações
6 | Nada ainda…
7 | Compartilhar
8 | Usar Cores Vibrantes
9 | Salvar mudanças
10 | Tem certeza de que deseja excluir todas as letras salvas?
11 | Não foi possível obter a letra
12 | Talvez pesquise por algo?
13 | Letras salvas
14 | Excluir todas as letras
15 | Serviço Ouvinte de Notificação
16 | Álbum desconhecido
17 | faixa
18 | artista
19 | Largura
20 | Cores
21 | Conceder Acesso à Notificação
22 | O Acesso à Notificação é necessário para obter letras automaticamente. Limpe e reinicie o aplicativo após conceder permissão.
23 | Cantos arredondados
24 | Máximo de linhas para compartilhar
25 | Tema do aplicativo
26 | Título (A-Z)
27 | Título (Z-A)
28 | Agrupado por artistas
29 | Agrupado por álbum
30 | Baixar letras em lote
31 | Selecionar pasta
32 | Download
33 | Feito
34 | Carregando letras para
35 | Pesquisando por
36 | Algo deu errado…
37 | Tentar novamente?
38 | Letra correta
39 | Sem internet!!
40 | Falha na solicitação
41 | Nenhum resultado
42 | Erro de análise
43 | Erro de rede
44 | Ocorreu um erro
45 | Tema do Cartão
46 | Cores do Cartão
47 | Tamanho do Cartão
48 | Cantos do Cartão
49 | Usar cores mais vibrantes para a página de letras
50 | Backup
51 | Importar ou Exportar o banco de dados de letras salvas
52 | Baixar letras de suas músicas salvas localmente de uma só vez.
53 | Última adicionada
54 | Rush branding
55 | Exportando músicas para Downloads/Rush
56 | Músicas exportadas para Downloads/Rush
57 | Restaurando músicas
58 | Músicas restauradas
59 | Falha na restauração
60 | Sobre
61 | Nenhum arquivo de áudio
62 | Nenhuma pasta selecionada
63 | Exportar
64 | Exportar músicas para Downloads/Rush
65 | Restaurar
66 | Restaurar do arquivo de exportação selecionado
67 | Escolher arquivo
68 |
69 |
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/data/DatastoreFactory.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.data
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory
5 | import androidx.datastore.preferences.core.Preferences
6 | import okio.Path.Companion.toPath
7 |
8 | expect class DatastoreFactory {
9 | fun getLyricsPagePreferencesDataStore() : DataStore
10 | fun getOtherPreferencesDataStore(): DataStore
11 | fun getSharePagePreferencesDataStore(): DataStore
12 | }
13 |
14 | internal const val LYRICS_DATASTORE = "rush.lyrics.preferences_pb"
15 | internal const val OTHER_DATASTORE = "rush.other.preferences_pb"
16 | internal const val SHARE_DATASTORE = "rush.share.preferences_pb"
17 |
18 | fun createDataStore(producePath: () -> String): DataStore =
19 | PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() })
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/data/HttpClientExt.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.data
2 |
3 | import com.shub39.rush.core.domain.Result
4 | import com.shub39.rush.core.domain.SourceError
5 | import io.ktor.client.call.NoTransformationFoundException
6 | import io.ktor.client.call.body
7 | import io.ktor.client.network.sockets.SocketTimeoutException
8 | import io.ktor.client.statement.HttpResponse
9 | import io.ktor.util.network.UnresolvedAddressException
10 | import kotlinx.coroutines.ensureActive
11 | import kotlin.coroutines.coroutineContext
12 |
13 | suspend inline fun safeCall(
14 | execute: () -> HttpResponse
15 | ): Result {
16 | val response = try {
17 | execute()
18 | } catch (e: SocketTimeoutException) {
19 | return Result.Error(SourceError.Network.REQUEST_FAILED)
20 | } catch (e: UnresolvedAddressException) {
21 | return Result.Error(SourceError.Network.NO_INTERNET)
22 | } catch (e: Exception) {
23 | coroutineContext.ensureActive()
24 | return Result.Error(SourceError.Data.UNKNOWN)
25 | }
26 |
27 | return responseToResult(response)
28 | }
29 |
30 | suspend inline fun responseToResult(
31 | response: HttpResponse
32 | ): Result {
33 | return when(response.status.value) {
34 | in 200..299 -> {
35 | try {
36 | Result.Success(response.body())
37 | } catch (e: NoTransformationFoundException) {
38 | Result.Error(SourceError.Data.PARSE_ERROR)
39 | }
40 | }
41 |
42 | else -> Result.Error(SourceError.Data.UNKNOWN)
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/data/HttpClientFactory.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.data
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.okhttp.OkHttp
5 | import io.ktor.client.plugins.HttpTimeout
6 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
7 | import io.ktor.client.plugins.defaultRequest
8 | import io.ktor.client.plugins.logging.Logger
9 | import io.ktor.client.plugins.logging.Logging
10 | import io.ktor.http.ContentType
11 | import io.ktor.http.contentType
12 | import io.ktor.serialization.kotlinx.json.json
13 | import kotlinx.serialization.json.Json
14 |
15 | object HttpClientFactory {
16 | fun create(): HttpClient {
17 | return HttpClient(OkHttp) {
18 | install(ContentNegotiation) {
19 | json(
20 | json = Json {
21 | ignoreUnknownKeys = true
22 | }
23 | )
24 | }
25 | install(HttpTimeout) {
26 | socketTimeoutMillis = 20_000
27 | requestTimeoutMillis = 20_000
28 | }
29 | install(Logging) {
30 | logger = object : Logger {
31 | override fun log(message: String) {
32 | println(message )
33 | }
34 | }
35 | }
36 | defaultRequest {
37 | contentType(ContentType.Application.Json)
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/data/PaletteGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.data
2 |
3 | import com.shub39.rush.core.domain.data_classes.ExtractedColors
4 |
5 | expect class PaletteGenerator {
6 | suspend fun generatePaletteFromUrl(url: String): ExtractedColors
7 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/Error.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
2 |
3 | sealed interface Error
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/LyricsPagePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
2 |
3 | import androidx.compose.ui.text.style.TextAlign
4 | import com.shub39.rush.core.domain.enums.CardColors
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface LyricsPagePreferences {
8 | suspend fun reset()
9 |
10 | fun getHypnoticCanvasFlow(): Flow
11 | suspend fun updateHypnoticCanvas(newHypnoticCanvas: Boolean)
12 |
13 | fun getLyricsColorFlow(): Flow
14 | suspend fun updateLyricsColor(new: CardColors)
15 |
16 | fun getCardBackgroundFlow(): Flow
17 | suspend fun updateCardBackground(newCardBackground: Int)
18 |
19 | fun getCardContentFlow(): Flow
20 | suspend fun updateCardContent(newCardContent: Int)
21 |
22 | fun getUseExtractedFlow(): Flow
23 | suspend fun updateUseExtractedFlow(pref: Boolean)
24 |
25 | fun getLyricAlignmentFlow(): Flow
26 | suspend fun updateLyricAlignment(alignment: TextAlign)
27 |
28 | fun getFontSizeFlow(): Flow
29 | suspend fun updateFontSize(newFontSize: Float)
30 |
31 | fun getLineHeightFlow(): Flow
32 | suspend fun updateLineHeight(newLineHeight: Float)
33 |
34 | fun getLetterSpacingFlow(): Flow
35 | suspend fun updateLetterSpacing(newLetterSpacing: Float)
36 |
37 | fun getFullScreenFlow(): Flow
38 | suspend fun setFullScreen(pref: Boolean)
39 |
40 | fun getMaxLinesFlow(): Flow
41 | suspend fun updateMaxLines(newMaxLines: Int)
42 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/OtherPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
2 |
3 | import com.materialkolor.PaletteStyle
4 | import com.shub39.rush.core.domain.enums.AppTheme
5 | import com.shub39.rush.core.domain.enums.Fonts
6 | import com.shub39.rush.core.domain.enums.SortOrder
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | interface OtherPreferences {
10 | fun getAppThemePrefFlow(): Flow
11 | suspend fun updateAppThemePref(pref: AppTheme)
12 |
13 | fun getSeedColorFlow(): Flow
14 | suspend fun updateSeedColor(newCardContent: Int)
15 |
16 | fun getAmoledPrefFlow(): Flow
17 | suspend fun updateAmoledPref(amoled: Boolean)
18 |
19 | fun getPaletteStyle(): Flow
20 | suspend fun updatePaletteStyle(style: PaletteStyle)
21 |
22 | fun getSortOrderFlow(): Flow
23 | suspend fun updateSortOrder(newSortOrder: SortOrder)
24 |
25 | fun getOnboardingDoneFlow(): Flow
26 | suspend fun updateOnboardingDone(done: Boolean)
27 |
28 | fun getMaterialYouFlow(): Flow
29 | suspend fun updateMaterialTheme(pref: Boolean)
30 |
31 | fun getFontFlow(): Flow
32 | suspend fun updateFonts(font: Fonts)
33 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/Result.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
2 |
3 | typealias RootError = Error
4 |
5 | sealed interface Result {
6 | data class Success(val data: D): Result
7 | data class Error(val error: E): Result
8 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/Route.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | sealed interface Route {
6 | @Serializable
7 | data object SavedPage: Route
8 |
9 | @Serializable
10 | data object LyricsGraph: Route
11 |
12 | @Serializable
13 | data object SettingsGraph: Route
14 |
15 | companion object {
16 | val allRoutes = listOf(SavedPage, LyricsGraph, SettingsGraph)
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/SharePagePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
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 | import kotlinx.coroutines.flow.Flow
9 |
10 | interface SharePagePreferences {
11 | fun getCardFitFlow(): Flow
12 | suspend fun updateCardFit(newCardFit: CardFit)
13 |
14 | fun getCardBackgroundFlow(): Flow
15 | suspend fun updateCardBackground(newCardBackground: Int)
16 |
17 | fun getCardContentFlow(): Flow
18 | suspend fun updateCardContent(newCardContent: Int)
19 |
20 | fun getCardThemeFlow(): Flow
21 | suspend fun updateCardTheme(newCardTheme: CardTheme)
22 |
23 | fun getCardColorFlow(): Flow
24 | suspend fun updateCardColor(newCardColor: CardColors)
25 |
26 | fun getCardRoundnessFlow(): Flow
27 | suspend fun updateCardRoundness(newCardRoundness: CornerRadius)
28 |
29 | fun getCardFontFlow(): Flow
30 | suspend fun updateCardFont(newCardFont: Fonts)
31 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/SourceError.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain
2 |
3 | sealed interface SourceError: Error {
4 | enum class Network : SourceError {
5 | NO_INTERNET,
6 | REQUEST_FAILED,
7 | }
8 | enum class Data: SourceError {
9 | NO_RESULTS,
10 | PARSE_ERROR,
11 | IO_ERROR,
12 | UNKNOWN
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/data_classes/ExtractedColors.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.data_classes
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | data class ExtractedColors(
6 | val cardBackgroundDominant: Color = Color.DarkGray,
7 | val cardContentDominant: Color = Color.White,
8 | val cardBackgroundMuted: Color = Color.Gray,
9 | val cardContentMuted: Color = Color.White
10 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/data_classes/SongDetails.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.data_classes
2 |
3 | data class SongDetails(
4 | val title: String = "",
5 | val artist: String = "",
6 | val album: String? = null,
7 | val artUrl: String = ""
8 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/data_classes/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.data_classes
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.toArgb
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 | data class Theme(
10 | val seedColor: Int = Color.White.toArgb(),
11 | val appTheme: AppTheme = AppTheme.SYSTEM,
12 | val withAmoled: Boolean = false,
13 | val style: PaletteStyle = PaletteStyle.TonalSpot,
14 | val materialTheme: Boolean = false,
15 | val fonts: Fonts = Fonts.POPPINS
16 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/AppTheme.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.dark
6 | import rush.app.generated.resources.light
7 | import rush.app.generated.resources.system
8 |
9 | enum class AppTheme(val stringRes: StringResource) {
10 | SYSTEM(Res.string.system),
11 | LIGHT(Res.string.light),
12 | DARK(Res.string.dark)
13 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/CardColors.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.custom
6 | import rush.app.generated.resources.muted
7 | import rush.app.generated.resources.vibrant
8 |
9 | enum class CardColors(
10 | val stringRes: StringResource
11 | ) {
12 | MUTED(Res.string.muted),
13 | VIBRANT(Res.string.vibrant),
14 | CUSTOM(Res.string.custom)
15 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/CardFit.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.fit
6 | import rush.app.generated.resources.standard
7 |
8 | enum class CardFit(
9 | val stringRes: StringResource
10 | ) {
11 | FIT(Res.string.fit),
12 | STANDARD(Res.string.standard)
13 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/CardTheme.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.chat
6 | import rush.app.generated.resources.couplet
7 | import rush.app.generated.resources.hypnotic
8 | import rush.app.generated.resources.messy
9 | import rush.app.generated.resources.quote
10 | import rush.app.generated.resources.rushed
11 | import rush.app.generated.resources.spotify
12 | import rush.app.generated.resources.vertical
13 |
14 | enum class CardTheme(
15 | val stringRes: StringResource
16 | ) {
17 | SPOTIFY(Res.string.spotify),
18 | RUSHED(Res.string.rushed),
19 | HYPNOTIC(Res.string.hypnotic),
20 | VERTICAL(Res.string.vertical),
21 | QUOTE(Res.string.quote),
22 | COUPLET(Res.string.couplet),
23 | MESSY(Res.string.messy),
24 | CHAT(Res.string.chat)
25 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/CornerRadius.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.default_
6 | import rush.app.generated.resources.rounded
7 |
8 | // default is a reserved kotlin keyword
9 | enum class CornerRadius(
10 | val stringRes: StringResource
11 | ) {
12 | DEFAULT(Res.string.default_),
13 | ROUNDED(Res.string.rounded)
14 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/Fonts.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.FontResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.dm_sans
6 | import rush.app.generated.resources.figtree
7 | import rush.app.generated.resources.inter
8 | import rush.app.generated.resources.jost
9 | import rush.app.generated.resources.manrope
10 | import rush.app.generated.resources.montserrat
11 | import rush.app.generated.resources.open_sans
12 | import rush.app.generated.resources.outfit
13 | import rush.app.generated.resources.poppins_regular
14 | import rush.app.generated.resources.quicksand
15 |
16 | enum class Fonts(
17 | val fullName: String,
18 | val font: FontResource
19 | ) {
20 | POPPINS("Poppins", Res.font.poppins_regular),
21 | DM_SANS("DM Sans", Res.font.dm_sans),
22 | FIGTREE("Figtree", Res.font.figtree),
23 | INTER("Inter", Res.font.inter),
24 | MANROPE("Manrope", Res.font.manrope),
25 | MONTSERRAT("Montserrat", Res.font.montserrat),
26 | OPEN_SANS("Open Sans", Res.font.open_sans),
27 | OUTFIT("Outfit", Res.font.outfit),
28 | QUICKSAND("Quicksand", Res.font.quicksand),
29 | JOSH("Jost", Res.font.jost)
30 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/SortOrder.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import rush.app.generated.resources.Res
5 | import rush.app.generated.resources.sort_album_asc
6 | import rush.app.generated.resources.sort_artists_asc
7 | import rush.app.generated.resources.sort_date_added
8 | import rush.app.generated.resources.sort_title_asc
9 | import rush.app.generated.resources.sort_title_desc
10 |
11 | enum class SortOrder(
12 | val stringRes: StringResource
13 | ) {
14 | DATE_ADDED(Res.string.sort_date_added),
15 | TITLE_ASC(Res.string.sort_title_asc),
16 | TITLE_DESC(Res.string.sort_title_desc),
17 | ARTISTS_ASC(Res.string.sort_artists_asc),
18 | ALBUM_ASC(Res.string.sort_album_asc)
19 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/domain/enums/Sources.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.domain.enums
2 |
3 | enum class Sources {
4 | Genius,
5 | LrcLib
6 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/ArtFromUrl.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.material3.Icon
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.layout.ContentScale
10 | import coil3.ImageLoader
11 | import com.skydoves.landscapist.ImageOptions
12 | import com.skydoves.landscapist.coil3.CoilImage
13 | import com.skydoves.landscapist.components.rememberImageComponent
14 | import com.skydoves.landscapist.placeholder.shimmer.Shimmer
15 | import com.skydoves.landscapist.placeholder.shimmer.ShimmerPlugin
16 | import compose.icons.FontAwesomeIcons
17 | import compose.icons.fontawesomeicons.Solid
18 | import compose.icons.fontawesomeicons.solid.Music
19 | import org.jetbrains.compose.resources.painterResource
20 | import org.koin.compose.koinInject
21 | import rush.app.generated.resources.Res
22 | import rush.app.generated.resources.rush_transparent
23 |
24 | // General Image Composable
25 | @Composable
26 | fun ArtFromUrl(
27 | imageUrl: Any?,
28 | modifier: Modifier = Modifier,
29 | contentScale: ContentScale = ContentScale.Crop,
30 | baseColor: Color = MaterialTheme.colorScheme.surface,
31 | highlightColor: Color = MaterialTheme.colorScheme.primary,
32 | imageLoader: ImageLoader = koinInject()
33 | ) {
34 | CoilImage(
35 | imageModel = { imageUrl },
36 | modifier = modifier,
37 | imageLoader = { imageLoader },
38 | component = rememberImageComponent {
39 | +ShimmerPlugin(
40 | Shimmer.Resonate(
41 | baseColor = baseColor,
42 | highlightColor = highlightColor
43 | )
44 | )
45 | },
46 | imageOptions = ImageOptions(
47 | alignment = Alignment.Center,
48 | contentScale = contentScale
49 | ),
50 | previewPlaceholder = painterResource(Res.drawable.rush_transparent),
51 | failure = {
52 | Icon(
53 | imageVector = FontAwesomeIcons.Solid.Music,
54 | contentDescription = "Placeholder"
55 | )
56 | }
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/ColorPickerDialog.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
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.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.foundation.layout.wrapContentSize
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material3.Button
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.unit.dp
21 | import com.github.skydoves.colorpicker.compose.AlphaTile
22 | import com.github.skydoves.colorpicker.compose.BrightnessSlider
23 | import com.github.skydoves.colorpicker.compose.HsvColorPicker
24 | import com.github.skydoves.colorpicker.compose.rememberColorPickerController
25 | import org.jetbrains.compose.resources.stringResource
26 | import rush.app.generated.resources.Res
27 | import rush.app.generated.resources.done
28 |
29 | // Color picker used app wide
30 | @Composable
31 | fun ColorPickerDialog(
32 | initialColor: Color,
33 | onSelect: (Color) -> Unit,
34 | onDismiss: () -> Unit
35 | ) {
36 | val controller = rememberColorPickerController()
37 |
38 | RushDialog(
39 | onDismissRequest = onDismiss
40 | ) {
41 | Column(
42 | modifier = Modifier
43 | .wrapContentSize()
44 | .padding(16.dp),
45 | horizontalAlignment = Alignment.CenterHorizontally,
46 | verticalArrangement = Arrangement.Center
47 | ) {
48 | HsvColorPicker(
49 | modifier = Modifier
50 | .width(350.dp)
51 | .height(300.dp)
52 | .padding(top = 10.dp),
53 | initialColor = initialColor,
54 | controller = controller
55 | )
56 |
57 | BrightnessSlider(
58 | modifier = Modifier
59 | .padding(top = 10.dp)
60 | .height(35.dp),
61 | initialColor = initialColor,
62 | controller = controller
63 | )
64 |
65 | AlphaTile(
66 | modifier = Modifier
67 | .size(80.dp)
68 | .padding(vertical = 10.dp)
69 | .clip(RoundedCornerShape(6.dp)),
70 | controller = controller
71 | )
72 |
73 | Button(
74 | onClick = {
75 | onSelect(controller.selectedColor.value)
76 | onDismiss()
77 | }
78 | ) {
79 | Text(
80 | text = stringResource(Res.string.done),
81 | modifier = Modifier.fillMaxWidth(),
82 | textAlign = TextAlign.Center
83 | )
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/Empty.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import compose.icons.FontAwesomeIcons
16 | import compose.icons.fontawesomeicons.Solid
17 | import compose.icons.fontawesomeicons.solid.Music
18 | import org.jetbrains.compose.resources.stringResource
19 | import rush.app.generated.resources.Res
20 | import rush.app.generated.resources.empty
21 | import rush.app.generated.resources.suggestion
22 |
23 | @Composable
24 | fun Empty(
25 | suggestion: Boolean = true
26 | ) {
27 | val color = Color.LightGray
28 |
29 | Column(
30 | modifier = Modifier.fillMaxSize(),
31 | horizontalAlignment = Alignment.CenterHorizontally,
32 | verticalArrangement = Arrangement.Center
33 | ) {
34 | Icon(
35 | imageVector = FontAwesomeIcons.Solid.Music,
36 | contentDescription = "Empty Library",
37 | modifier = Modifier.size(128.dp).padding(16.dp),
38 | tint = color
39 | )
40 |
41 | Text(
42 | text = stringResource(Res.string.empty),
43 | color = color
44 | )
45 |
46 | if (suggestion) {
47 | Text(
48 | text = stringResource(Res.string.suggestion),
49 | color = color
50 | )
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/PageFill.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 |
9 | @Composable
10 | fun PageFill(content: @Composable () -> Unit) {
11 | Box(
12 | modifier = Modifier.fillMaxSize(),
13 | contentAlignment = Alignment.Center
14 | ) {
15 | content()
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/RushDialog.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.foundation.layout.widthIn
4 | import androidx.compose.material3.Card
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 | import androidx.compose.ui.window.Dialog
10 |
11 | // Generic Dialog used app wide
12 | @Composable
13 | fun RushDialog(
14 | onDismissRequest: () -> Unit,
15 | content: @Composable () -> Unit
16 | ) {
17 | Dialog(
18 | onDismissRequest = onDismissRequest
19 | ) {
20 | Card(
21 | modifier = Modifier.widthIn(max = 500.dp),
22 | shape = MaterialTheme.shapes.extraLarge
23 | ) { content() }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/RushTheme.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.shub39.rush.core.domain.data_classes.Theme
5 |
6 | @Composable
7 | expect fun RushTheme(
8 | state: Theme = Theme(),
9 | content: @Composable () -> Unit
10 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/SettingsSlider.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.annotation.IntRange
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Slider
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 |
17 | // yeeted from dn0ne/lotus
18 | @Composable
19 | fun SettingSlider(
20 | title: String,
21 | value: Float,
22 | onValueChange: (Float) -> Unit,
23 | modifier: Modifier = Modifier,
24 | enabled: Boolean = true,
25 | onValueChangeFinished: (() -> Unit)? = null,
26 | valueToShow: String? = null,
27 | @IntRange steps: Int = 0,
28 | valueRange: ClosedFloatingPointRange = 0f..1f,
29 | ) {
30 | Column(
31 | modifier = modifier
32 | ) {
33 | Row(
34 | modifier = Modifier.fillMaxWidth(),
35 | horizontalArrangement = Arrangement.SpaceBetween
36 | ) {
37 | Text(
38 | text = title,
39 | style = MaterialTheme.typography.titleMedium,
40 | color = MaterialTheme.colorScheme.onSurface
41 | )
42 |
43 | Text(
44 | text = "${valueToShow ?: value.toInt()}",
45 | style = MaterialTheme.typography.titleMedium,
46 | color = MaterialTheme.colorScheme.onSurface
47 | )
48 | }
49 |
50 | Spacer(modifier = Modifier.height(4.dp))
51 |
52 | Slider(
53 | value = value,
54 | onValueChange = onValueChange,
55 | onValueChangeFinished = onValueChangeFinished,
56 | steps = steps,
57 | valueRange = valueRange,
58 | enabled = enabled,
59 | modifier = Modifier.fillMaxWidth()
60 | )
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/errorStringRes.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import com.shub39.rush.core.domain.Error
4 | import com.shub39.rush.core.domain.SourceError
5 | import org.jetbrains.compose.resources.StringResource
6 | import rush.app.generated.resources.Res
7 | import rush.app.generated.resources.io_error
8 | import rush.app.generated.resources.no_internet
9 | import rush.app.generated.resources.no_results
10 | import rush.app.generated.resources.parse_error
11 | import rush.app.generated.resources.request_failed
12 | import rush.app.generated.resources.unknown_error
13 |
14 | fun errorStringRes(error: Error): StringResource {
15 | return when (error) {
16 | SourceError.Data.NO_RESULTS -> Res.string.no_results
17 | SourceError.Data.PARSE_ERROR -> Res.string.parse_error
18 | SourceError.Data.IO_ERROR -> Res.string.io_error
19 | SourceError.Data.UNKNOWN -> Res.string.unknown_error
20 | SourceError.Network.NO_INTERNET -> Res.string.no_internet
21 | SourceError.Network.REQUEST_FAILED -> Res.string.request_failed
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/core/presentation/scrollbar.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.animation.core.Spring.StiffnessMediumLow
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.animation.core.spring
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.lazy.LazyListState
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.draw.drawWithContent
13 | import androidx.compose.ui.geometry.CornerRadius
14 | import androidx.compose.ui.geometry.Offset
15 | import androidx.compose.ui.geometry.Size
16 | import androidx.compose.ui.unit.Dp
17 | import androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | fun Modifier.simpleVerticalScrollbar(
21 | state: LazyListState,
22 | width: Dp = 4.dp,
23 | ): Modifier {
24 |
25 | val targetAlpha = if (state.isScrollInProgress) .7f else 0f
26 | val duration = if (state.isScrollInProgress) 150 else 1000
27 |
28 | val alpha by animateFloatAsState(
29 | targetValue = targetAlpha,
30 | animationSpec = tween(durationMillis = duration)
31 | )
32 |
33 | val firstIndex by animateFloatAsState(
34 | targetValue = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index?.toFloat() ?: 0f,
35 | animationSpec = spring(stiffness = StiffnessMediumLow)
36 | )
37 |
38 | val lastIndex by animateFloatAsState(
39 | targetValue = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index?.toFloat() ?: 0f,
40 | animationSpec = spring(stiffness = StiffnessMediumLow)
41 | )
42 |
43 | val color = MaterialTheme.colorScheme.tertiary
44 |
45 | return drawWithContent {
46 | drawContent()
47 |
48 | val itemsCount = state.layoutInfo.totalItemsCount
49 |
50 | if (itemsCount > 0 && alpha > 0f) {
51 | val scrollbarTop = firstIndex / itemsCount * size.height
52 | val scrollBottom = (lastIndex + 1f) / itemsCount * size.height
53 | val scrollbarHeight = scrollBottom - scrollbarTop
54 | drawRoundRect(
55 | cornerRadius = CornerRadius(width.toPx() / 2, width.toPx() / 2),
56 | color = color,
57 | topLeft = Offset(size.width - width.toPx(), scrollbarTop),
58 | size = Size(width.toPx(), scrollbarHeight),
59 | alpha = alpha
60 | )
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/di/RushModules.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.di
2 |
3 | import androidx.sqlite.driver.bundled.BundledSQLiteDriver
4 | import com.shub39.rush.core.data.DatastoreFactory
5 | import com.shub39.rush.core.data.HttpClientFactory
6 | import com.shub39.rush.core.data.LyricsPagePreferencesImpl
7 | import com.shub39.rush.core.data.OtherPreferencesImpl
8 | import com.shub39.rush.core.data.SharePagePreferencesImpl
9 | import com.shub39.rush.core.domain.LyricsPagePreferences
10 | import com.shub39.rush.core.domain.OtherPreferences
11 | import com.shub39.rush.core.domain.SharePagePreferences
12 | import com.shub39.rush.lyrics.data.database.DatabaseFactory
13 | import com.shub39.rush.lyrics.data.database.SongDatabase
14 | import com.shub39.rush.lyrics.data.network.GeniusApi
15 | import com.shub39.rush.lyrics.data.network.GeniusScraper
16 | import com.shub39.rush.lyrics.data.network.LrcLibApi
17 | import com.shub39.rush.lyrics.data.repository.RushRepository
18 | import com.shub39.rush.lyrics.domain.SongRepo
19 | import com.shub39.rush.lyrics.presentation.viewmodels.LyricsVM
20 | import com.shub39.rush.lyrics.presentation.viewmodels.SavedVM
21 | import com.shub39.rush.lyrics.presentation.viewmodels.SearchSheetVM
22 | import com.shub39.rush.lyrics.presentation.viewmodels.StateLayer
23 | import org.koin.core.module.Module
24 | import org.koin.core.module.dsl.singleOf
25 | import org.koin.core.module.dsl.viewModelOf
26 | import org.koin.core.qualifier.named
27 | import org.koin.dsl.bind
28 | import org.koin.dsl.module
29 |
30 | expect val platformModule: Module
31 |
32 | val rushModule = module {
33 | // Database
34 | single {
35 | get()
36 | .create()
37 | .fallbackToDestructiveMigration(true)
38 | .setDriver(BundledSQLiteDriver())
39 | .build()
40 | }
41 | single { get().songDao() }
42 | single { HttpClientFactory.create() }
43 |
44 | // Network Stuff
45 | singleOf(::GeniusScraper)
46 | singleOf(::GeniusApi)
47 | singleOf(::LrcLibApi)
48 |
49 | // Repositories and backup stuff
50 | singleOf(::RushRepository).bind()
51 |
52 | // Datastore
53 | single(named("LyricsPage")) { get().getLyricsPagePreferencesDataStore() }
54 | single(named("SharePage")) { get().getSharePagePreferencesDataStore() }
55 | single(named("Other")) { get().getOtherPreferencesDataStore() }
56 | single { OtherPreferencesImpl(get(named("Other"))) }.bind()
57 | single { LyricsPagePreferencesImpl(get(named("LyricsPage"))) }.bind()
58 | single { SharePagePreferencesImpl(get(named("SharePage"))) }.bind()
59 |
60 | // ViewModels
61 | singleOf(::StateLayer)
62 | viewModelOf(::SearchSheetVM)
63 | viewModelOf(::SavedVM)
64 | viewModelOf(::LyricsVM)
65 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/di/initKoin.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.di
2 |
3 | import org.koin.core.context.startKoin
4 | import org.koin.dsl.KoinAppDeclaration
5 |
6 | fun initKoin(config: KoinAppDeclaration? = null) {
7 | startKoin{
8 | config?.invoke(this)
9 | modules(rushModule, platformModule)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/backup/ExportImpl.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.backup
2 |
3 | import com.shub39.rush.lyrics.domain.backup.ExportRepo
4 |
5 | expect class ExportImpl: ExportRepo
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/backup/RestoreImpl.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.backup
2 |
3 | import com.shub39.rush.lyrics.domain.backup.RestoreRepo
4 |
5 | expect class RestoreImpl: RestoreRepo
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/database/DatabaseFactory.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.database
2 |
3 | import androidx.room.RoomDatabase
4 |
5 | expect class DatabaseFactory {
6 | fun create(): RoomDatabase.Builder
7 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/database/SongDao.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.database
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface SongDao {
12 | @Query("SELECT * FROM songs")
13 | fun getAllSongs(): Flow>
14 |
15 | @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | suspend fun insertSong(songEntity: SongEntity)
17 |
18 | @Query("DELETE FROM songs WHERE id = :id")
19 | suspend fun deleteSong(id: Long)
20 |
21 | @Update
22 | suspend fun updateSong(songEntity: SongEntity)
23 |
24 | @Query("SELECT * FROM songs WHERE id = :id")
25 | suspend fun getSongById(id: Long): SongEntity
26 |
27 | @Query("SELECT * FROM songs WHERE title LIKE :query || '%'")
28 | suspend fun searchSong(query: String): List
29 |
30 | @Query("UPDATE songs SET lyrics = :lrcAsync, syncedLyrics = :lrcSync WHERE id = :id")
31 | suspend fun updateLrcLyricsById(id: Long, lrcAsync: String, lrcSync: String?)
32 |
33 | @Query("UPDATE songs SET geniusLyrics = :lyrics WHERE id = :id")
34 | suspend fun updateGeniusLyrics(id: Long, lyrics: String)
35 |
36 | @Query("DELETE FROM songs")
37 | suspend fun deleteAllSongs()
38 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/database/SongDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 |
6 | @Database(entities = [SongEntity::class], version = 3, exportSchema = false)
7 | abstract class SongDatabase : RoomDatabase() {
8 | abstract fun songDao(): SongDao
9 |
10 | companion object {
11 | const val DB_NAME = "song_database"
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/database/SongEntity.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.database
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "songs")
7 | data class SongEntity(
8 | @PrimaryKey(autoGenerate = true)
9 | val id: Long,
10 | val title: String,
11 | val artists: String,
12 | val lyrics: String,
13 | val album: String?,
14 | val sourceUrl: String,
15 | val artUrl: String?,
16 | val geniusLyrics: String?,
17 | val syncedLyrics: String?,
18 | val dateAdded: Long = 0
19 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/listener/MediaListenerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.listener
2 |
3 | import com.shub39.rush.lyrics.domain.MediaInterface
4 |
5 | expect class MediaListenerImpl: MediaInterface
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/mappers/Mappers.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.mappers
2 |
3 | import com.shub39.rush.lyrics.data.database.SongEntity
4 | import com.shub39.rush.lyrics.domain.Song
5 | import com.shub39.rush.lyrics.domain.backup.SongSchema
6 |
7 | fun SongEntity.toSong(): Song {
8 | return Song(
9 | id = id,
10 | title = title,
11 | artists = artists,
12 | lyrics = lyrics,
13 | album = album,
14 | sourceUrl = sourceUrl,
15 | artUrl = artUrl,
16 | syncedLyrics = syncedLyrics,
17 | geniusLyrics = geniusLyrics,
18 | dateAdded = dateAdded
19 | )
20 | }
21 |
22 | fun Song.toSongEntity(): SongEntity {
23 | return SongEntity(
24 | id = id,
25 | title = title,
26 | artists = artists,
27 | lyrics = lyrics,
28 | album = album,
29 | sourceUrl = sourceUrl,
30 | artUrl = artUrl,
31 | syncedLyrics = syncedLyrics,
32 | dateAdded = dateAdded,
33 | geniusLyrics = geniusLyrics
34 | )
35 | }
36 |
37 | fun Song.toSongSchema(): SongSchema {
38 | return SongSchema(
39 | id = id,
40 | title = title,
41 | artists = artists,
42 | lyrics = lyrics,
43 | album = album,
44 | sourceUrl = sourceUrl,
45 | artUrl = artUrl,
46 | syncedLyrics = syncedLyrics,
47 | dateAdded = dateAdded,
48 | geniusLyrics = geniusLyrics
49 | )
50 | }
51 |
52 | fun SongSchema.toSong(): Song {
53 | return Song(
54 | id = id,
55 | title = title,
56 | artists = artists,
57 | lyrics = lyrics,
58 | album = album,
59 | sourceUrl = sourceUrl,
60 | artUrl = artUrl,
61 | syncedLyrics = syncedLyrics,
62 | dateAdded = dateAdded,
63 | geniusLyrics = geniusLyrics
64 | )
65 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/GeniusApi.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network
2 |
3 | import com.shub39.rush.core.data.safeCall
4 | import com.shub39.rush.core.domain.Result
5 | import com.shub39.rush.core.domain.SourceError
6 | import com.shub39.rush.lyrics.data.network.dto.genius.GeniusSearchDto
7 | import com.shub39.rush.lyrics.data.network.dto.genius.GeniusSongDto
8 | import io.ktor.client.HttpClient
9 | import io.ktor.client.request.get
10 | import io.ktor.client.request.header
11 | import io.ktor.client.request.parameter
12 | import io.ktor.http.HttpHeaders
13 |
14 | class GeniusApi(
15 | private val client: HttpClient
16 | ) {
17 | suspend fun geniusSearch(query: String): Result = safeCall {
18 | client.get(
19 | urlString = "$BASE_URL/search"
20 | ) {
21 | header(HttpHeaders.Authorization, BEARER_TOKEN)
22 | parameter("q", query)
23 | }
24 | }
25 |
26 |
27 | suspend fun geniusSong(id: Long): Result = safeCall {
28 | client.get(
29 | urlString = "$BASE_URL/songs/$id"
30 | ) {
31 | header(HttpHeaders.Authorization, BEARER_TOKEN)
32 | }
33 | }
34 |
35 |
36 | private companion object {
37 | private const val BASE_URL = "https://api.genius.com"
38 | private const val BEARER_TOKEN = "Bearer ${Tokens.GENIUS_API}"
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/GeniusScraper.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network
2 |
3 | import com.fleeksoft.ksoup.Ksoup
4 | import com.fleeksoft.ksoup.nodes.Element
5 | import com.fleeksoft.ksoup.nodes.TextNode
6 | import com.shub39.rush.core.data.safeCall
7 | import com.shub39.rush.core.domain.Result
8 | import io.ktor.client.HttpClient
9 | import io.ktor.client.call.body
10 | import io.ktor.client.request.get
11 | import io.ktor.client.statement.HttpResponse
12 |
13 | // thanks to https://github.com/imjyotiraditya/genius-lyrics-cli
14 | class GeniusScraper(
15 | private val client: HttpClient
16 | ) {
17 | suspend fun scrapeLyrics(songUrl: String): String? {
18 |
19 | val response = safeCall {
20 | client.get(
21 | urlString = songUrl
22 | )
23 | }
24 |
25 | when (response) {
26 | is Result.Success -> {
27 | val lyricsElements = Ksoup.parse(response.data.body()).select("div[data-lyrics-container='true']")
28 |
29 | return buildString {
30 | lyricsElements.forEach { element ->
31 | element.childNodes().forEachIndexed { _, node ->
32 | val text = when (node) {
33 | is TextNode -> node.text()
34 | is Element -> node.wholeText()
35 | else -> return@forEachIndexed
36 | }.trim()
37 |
38 | if (text.isBlank()) return@forEachIndexed
39 |
40 | // skips contributor info and all that
41 | if (text.matches(Regex("^\\d+ Contributors.*"))) return@forEachIndexed
42 | if (text.matches(Regex("^\\d+ Contributor.*"))) return@forEachIndexed
43 |
44 | if (text.isNotEmpty()) {
45 | if (text.startsWith("[") || text.endsWith("]")) {
46 | append("\n")
47 | }
48 | append("\n$text")
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | is Result.Error -> return null
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/LrcLibApi.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network
2 |
3 | import com.shub39.rush.core.data.safeCall
4 | import com.shub39.rush.core.domain.Result
5 | import com.shub39.rush.core.domain.SourceError
6 | import com.shub39.rush.lyrics.data.network.dto.lrclib.LrcGetDto
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.request.get
9 | import io.ktor.client.request.parameter
10 |
11 | class LrcLibApi(
12 | private val client: HttpClient
13 | ) {
14 | suspend fun getLrcLyrics(
15 | trackName: String,
16 | artistName: String
17 | ): LrcGetDto? {
18 | val result = safeCall {
19 | client.get(
20 | urlString = "$LRC_BASE_URL/api/get"
21 | ) {
22 | parameter("track_name", trackName)
23 | parameter("artist_name", artistName)
24 | }
25 | }
26 |
27 | return when (result) {
28 | is Result.Success -> result.data
29 | is Result.Error -> null
30 | }
31 | }
32 |
33 | suspend fun searchLrcLyrics(
34 | trackName: String,
35 | artistName: String
36 | ): Result, SourceError> = safeCall {
37 | client.get(
38 | urlString = "$LRC_BASE_URL/api/search"
39 | ) {
40 | parameter("track_name", trackName)
41 | parameter("artist_name", artistName)
42 | }
43 | }
44 |
45 |
46 | private companion object {
47 | private const val LRC_BASE_URL = "https://lrclib.net"
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/Tokens.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network
2 |
3 | object Tokens {
4 | const val GENIUS_API = "qLSDtgIqHgzGNjOFUmdOxJKGJOg5RIAPzOKTfrs7rNxqYXwfdSh9HTHMJUs2X27Y"
5 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/dto/genius/GeniusSearchDto.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network.dto.genius
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.json.JsonArray
6 |
7 | @Serializable
8 | data class GeniusSearchDto (
9 | val response: SearchResponse
10 | )
11 |
12 | @Serializable
13 | data class SearchResponse (
14 | val hits: List
15 | )
16 |
17 | @Serializable
18 | data class Hit (
19 | val highlights: JsonArray,
20 | val index: String,
21 | val type: String,
22 | val result: Result
23 | )
24 |
25 | @Serializable
26 | data class Result (
27 | @SerialName("_type")
28 | val type: String? = null,
29 |
30 | @SerialName("artist_names")
31 | val artistNames: String,
32 |
33 | @SerialName("full_title")
34 | val fullTitle: String,
35 |
36 | val id: Long,
37 |
38 | val instrumental: Boolean = false,
39 |
40 | @SerialName("song_art_image_url")
41 | val songArtImageURL: String,
42 |
43 | val title: String,
44 |
45 | val url: String,
46 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/dto/genius/GeniusSongDto.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network.dto.genius
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class GeniusSongDto (
8 | val response: SongResponse
9 | )
10 |
11 | @Serializable
12 | data class SongResponse (
13 | val song: Song
14 | )
15 |
16 | @Serializable
17 | data class Song (
18 | @SerialName("artist_names")
19 | val artistNames: String,
20 |
21 | val id: Long,
22 |
23 | val url: String,
24 |
25 | val path: String,
26 |
27 | @SerialName("song_art_image_url")
28 | val songArtImageURL: String?,
29 |
30 | val title: String,
31 |
32 | val album: Album?,
33 | )
34 |
35 | @Serializable
36 | data class Album (
37 | val name: String,
38 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/data/network/dto/lrclib/LrcGetDto.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.network.dto.lrclib
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LrcGetDto (
7 | val id: Long,
8 | val name: String,
9 | val trackName: String,
10 | val artistName: String?,
11 | val albumName: String?,
12 | val duration: Double?,
13 | val instrumental: Boolean?,
14 | val plainLyrics: String?,
15 | val syncedLyrics: String?
16 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/AudioFile.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | data class AudioFile(val title: String, val artist: String)
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/LrcLibSong.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | data class LrcLibSong (
4 | val id: Int,
5 | val name: String,
6 | val trackName: String,
7 | val artistName: String,
8 | val albumName: String,
9 | val duration: Double,
10 | val instrumental: Boolean,
11 | val plainLyrics: String?,
12 | val syncedLyrics: String?
13 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/Lyric.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | data class Lyric(val time: Long, val text: String)
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/MediaInterface.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | import kotlinx.coroutines.flow.MutableSharedFlow
4 |
5 | interface MediaInterface {
6 | val songInfoFlow: MutableSharedFlow>
7 | val songPositionFlow: MutableSharedFlow
8 | val playbackSpeedFlow: MutableSharedFlow
9 |
10 | fun destroy()
11 | fun startListening()
12 | fun seek(timestamp: Long)
13 | fun pauseOrResume(resume: Boolean)
14 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/SearchResult.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | data class SearchResult(
4 | val title: String,
5 | val artist: String,
6 | val album: String?,
7 | val artUrl: String,
8 | val url: String,
9 | val id: Long
10 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/Song.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | data class Song(
4 | val id: Long,
5 | val title: String,
6 | val artists: String,
7 | val lyrics: String,
8 | val album: String?,
9 | val sourceUrl: String,
10 | val artUrl: String?,
11 | val geniusLyrics: String?,
12 | val syncedLyrics: String?,
13 | val dateAdded: Long
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/SongRepo.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | import com.shub39.rush.core.domain.Result
4 | import com.shub39.rush.core.domain.SourceError
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface SongRepo {
8 | // network
9 | suspend fun fetchSong(id: Long): Result
10 | suspend fun scrapeGeniusLyrics(id: Long, url: String): Result
11 | suspend fun searchGenius(query: String): Result, SourceError>
12 | suspend fun searchLrcLib(track: String, artist: String): Result, SourceError>
13 |
14 | // database input/ output
15 | suspend fun insertSong(song: Song)
16 | suspend fun getSongs(): Flow>
17 | suspend fun getAllSongs(): List
18 | suspend fun getSong(id: Long): Song
19 | suspend fun getSong(query: String): List
20 | suspend fun deleteSong(id: Long)
21 | suspend fun updateLrcLyrics(id: Long, plainLyrics: String, syncedLyrics: String?)
22 | suspend fun deleteAllSongs()
23 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/SongUi.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain
2 |
3 | data class SongUi(
4 | val id: Long = 0,
5 | val title: String = "",
6 | val artists: String = "",
7 | val album: String? = null,
8 | val sourceUrl: String = "",
9 | val artUrl: String? = null,
10 | val lyrics: List> = emptyList(),
11 | val syncedLyrics: List? = null,
12 | val geniusLyrics: List>? = null
13 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/backup/ExportRepo.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain.backup
2 |
3 | interface ExportRepo {
4 | suspend fun exportToJson()
5 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/backup/ExportSchema.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain.backup
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | @SerialName("Export")
8 | data class ExportSchema(
9 | val schemaVersion: Int = 3,
10 | val songs: List
11 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/backup/ExportState.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain.backup
2 |
3 | enum class ExportState {
4 | IDLE,
5 | EXPORTING,
6 | EXPORTED
7 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/backup/RestoreRepo.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain.backup
2 |
3 | interface RestoreRepo {
4 | suspend fun restoreSongs(path: String): RestoreResult
5 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/backup/RestoreResult.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain.backup
2 |
3 | sealed class RestoreResult {
4 | data object Success : RestoreResult()
5 | data class Failure(val exceptionType: RestoreFailedException) : RestoreResult()
6 | }
7 |
8 | enum class RestoreState {
9 | IDLE,
10 | RESTORING,
11 | RESTORED,
12 | FAILURE
13 | }
14 |
15 | sealed interface RestoreFailedException {
16 | data object InvalidFile : RestoreFailedException
17 | data object OldSchema : RestoreFailedException
18 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/domain/backup/SongSchema.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.domain.backup
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | @SerialName("Song")
8 | data class SongSchema(
9 | val id: Long,
10 | val title: String,
11 | val artists: String,
12 | val lyrics: String,
13 | val album: String?,
14 | val sourceUrl: String,
15 | val artUrl: String?,
16 | val syncedLyrics: String?,
17 | val geniusLyrics: String?,
18 | val dateAdded: Long = 0
19 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/lyrics/LyricsPageAction.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.lyrics
2 |
3 | import androidx.compose.ui.text.style.TextAlign
4 | import com.shub39.rush.core.domain.data_classes.SongDetails
5 | import com.shub39.rush.core.domain.enums.Sources
6 |
7 | sealed interface LyricsPageAction {
8 | data class OnMaxLinesChange(val lines: Int): LyricsPageAction
9 | data class OnFullscreenChange(val pref: Boolean): LyricsPageAction
10 | data object OnCustomisationReset: LyricsPageAction
11 | data class OnLetterSpacingChange(val spacing: Float): LyricsPageAction
12 | data class OnLineHeightChange(val height: Float): LyricsPageAction
13 | data class OnFontSizeChange(val size: Float): LyricsPageAction
14 | data class OnAlignmentChange(val alignment: TextAlign): LyricsPageAction
15 | data class OnChangeSelectedLines(val lines: Map): LyricsPageAction
16 | data class OnScrapeGeniusLyrics(val id: Long, val url: String): LyricsPageAction
17 | data class OnLyricsCorrect(val show: Boolean): LyricsPageAction
18 | data class OnSyncAvailable(val sync: Boolean): LyricsPageAction
19 | data class OnToggleColorPref(val pref: Boolean): LyricsPageAction
20 | data class OnUpdatemBackground(val color: Int): LyricsPageAction
21 | data class OnUpdatemContent(val color: Int): LyricsPageAction
22 | data class OnHypnoticToggle(val pref: Boolean): LyricsPageAction
23 | data class OnVibrantToggle(val pref: Boolean): LyricsPageAction
24 | data class OnMeshSpeedChange(val speed: Float): LyricsPageAction
25 | data class OnSync(val sync: Boolean) : LyricsPageAction
26 | data class OnSourceChange(val source: Sources): LyricsPageAction
27 | data object OnToggleAutoChange: LyricsPageAction
28 | data object OnPauseOrResume: LyricsPageAction
29 | data class OnSeek(val position: Long): LyricsPageAction
30 | data class OnUpdateShareLines(val songDetails: SongDetails) :
31 | LyricsPageAction
32 | data object OnToggleSearchSheet: LyricsPageAction
33 | data class UpdateExtractedColors(val url: String) : LyricsPageAction
34 | data class OnLrcSearch(val track: String, val artist: String) : LyricsPageAction
35 | data class OnUpdateSongLyrics(val id: Long, val plainLyrics: String, val syncedLyrics: String?) :
36 | LyricsPageAction
37 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/lyrics/LyricsPageState.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.lyrics
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.toArgb
5 | import androidx.compose.ui.text.style.TextAlign
6 | import com.shub39.rush.core.domain.Error
7 | import com.shub39.rush.core.domain.data_classes.ExtractedColors
8 | import com.shub39.rush.core.domain.enums.CardColors
9 | import com.shub39.rush.core.domain.enums.Sources
10 | import com.shub39.rush.lyrics.domain.LrcLibSong
11 | import com.shub39.rush.lyrics.domain.SongUi
12 | import org.jetbrains.compose.resources.StringResource
13 |
14 | data class LyricsPageState(
15 | val song: SongUi? = null,
16 | val fetching: Pair = Pair(false, ""),
17 | val searching: Pair = Pair(false, ""),
18 | val scraping: Pair = Pair(false, null),
19 | val error: StringResource? = null,
20 | val autoChange: Boolean = false,
21 | val textAlign: TextAlign = TextAlign.Center,
22 | val fontSize: Float = 28f,
23 | val lineHeight: Float = 32f,
24 | val letterSpacing: Float = 0f,
25 | val playingSong: PlayingSong = PlayingSong(),
26 | val lrcCorrect: LrcCorrect = LrcCorrect(),
27 | val extractedColors: ExtractedColors = ExtractedColors(),
28 | val syncedAvailable: Boolean = false,
29 | val sync: Boolean = false,
30 | val lyricsCorrect: Boolean = false,
31 | val source: Sources = Sources.LrcLib,
32 | val selectedLines: Map = emptyMap(),
33 | val cardColors: CardColors = CardColors.MUTED,
34 | val hypnoticCanvas: Boolean = false,
35 | val maxLines: Int = 6,
36 | val meshSpeed: Float = 1f,
37 | val useExtractedColors: Boolean = true,
38 | val mCardBackground: Int = Color.DarkGray.toArgb(),
39 | val mCardContent: Int = Color.White.toArgb(),
40 | val fullscreen: Boolean = false
41 | )
42 |
43 | data class PlayingSong(
44 | val title: String = "",
45 | val artist: String? = null,
46 | val position: Long = 0,
47 | val speed: Float = 0f
48 | )
49 |
50 | data class LrcCorrect(
51 | val searchResults: List = emptyList(),
52 | val searching: Boolean = false,
53 | val error: StringResource? = null
54 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/lyrics/component/ErrorCard.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.lyrics.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Warning
10 | import androidx.compose.material3.Icon
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.graphics.Color
16 | import androidx.compose.ui.unit.dp
17 | import org.jetbrains.compose.resources.StringResource
18 | import org.jetbrains.compose.resources.stringResource
19 |
20 | @Composable
21 | fun ErrorCard(
22 | error: StringResource,
23 | colors: Pair
24 | ) {
25 | Column(
26 | modifier = Modifier.fillMaxSize(),
27 | horizontalAlignment = Alignment.CenterHorizontally,
28 | verticalArrangement = Arrangement.Center
29 | ) {
30 | Icon(
31 | imageVector = Icons.Default.Warning,
32 | contentDescription = "Error",
33 | modifier = Modifier
34 | .size(128.dp)
35 | .padding(16.dp),
36 | tint = colors.first
37 | )
38 |
39 | Text(
40 | text = stringResource(error),
41 | color = colors.first
42 | )
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/lyrics/component/LoadingCard.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.lyrics.component
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.LinearProgressIndicator
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.unit.dp
23 | import compose.icons.FontAwesomeIcons
24 | import compose.icons.fontawesomeicons.Solid
25 | import compose.icons.fontawesomeicons.solid.Save
26 | import compose.icons.fontawesomeicons.solid.Search
27 |
28 | @Composable
29 | fun LoadingCard(
30 | fetching: Pair,
31 | searching: Pair,
32 | colors: Pair
33 | ) {
34 | AnimatedVisibility (
35 | visible = searching.first,
36 | enter = fadeIn(),
37 | exit = fadeOut()
38 | ) {
39 | Column(
40 | modifier = Modifier
41 | .fillMaxSize()
42 | .padding(32.dp),
43 | horizontalAlignment = Alignment.CenterHorizontally,
44 | verticalArrangement = Arrangement.Center
45 | ) {
46 | Icon(
47 | imageVector = FontAwesomeIcons.Solid.Search,
48 | contentDescription = null,
49 | modifier = Modifier.size(120.dp),
50 | tint = colors.first
51 | )
52 |
53 | Spacer(modifier = Modifier.padding(16.dp))
54 |
55 | LinearProgressIndicator(
56 | color = colors.first,
57 | trackColor = Color.Transparent
58 | )
59 |
60 | Spacer(modifier = Modifier.height(8.dp))
61 |
62 | Text(
63 | text = searching.second,
64 | style = MaterialTheme.typography.titleMedium,
65 | textAlign = TextAlign.Center
66 | )
67 | }
68 | }
69 |
70 | AnimatedVisibility (
71 | visible = fetching.first,
72 | enter = fadeIn(),
73 | exit = fadeOut()
74 | ) {
75 | Column(
76 | modifier = Modifier
77 | .fillMaxSize()
78 | .padding(32.dp),
79 | horizontalAlignment = Alignment.CenterHorizontally,
80 | verticalArrangement = Arrangement.Center
81 | ) {
82 | Icon(
83 | imageVector = FontAwesomeIcons.Solid.Save,
84 | contentDescription = null,
85 | modifier = Modifier.size(100.dp),
86 | tint = colors.first
87 | )
88 |
89 | Spacer(modifier = Modifier.padding(16.dp))
90 |
91 | LinearProgressIndicator(
92 | color = colors.first,
93 | trackColor = Color.Transparent
94 | )
95 |
96 | Spacer(modifier = Modifier.height(8.dp))
97 |
98 | Text(
99 | text = fetching.second,
100 | style = MaterialTheme.typography.titleMedium,
101 | textAlign = TextAlign.Center
102 | )
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/saved/SavedPageAction.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.saved
2 |
3 | import com.shub39.rush.core.domain.enums.SortOrder
4 | import com.shub39.rush.lyrics.domain.Song
5 |
6 | sealed interface SavedPageAction {
7 | data object OnToggleAutoChange : SavedPageAction
8 | data object OnToggleSearchSheet: SavedPageAction
9 | data class OnDeleteSong(val song: Song) : SavedPageAction
10 | data class ChangeCurrentSong(val id: Long): SavedPageAction
11 | data class UpdateSortOrder(val sortOrder: SortOrder): SavedPageAction
12 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/saved/SavedPageState.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.saved
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.shub39.rush.core.domain.enums.SortOrder
5 | import com.shub39.rush.lyrics.domain.Song
6 |
7 | @Immutable
8 | data class SavedPageState(
9 | val songsByTime: List = emptyList(),
10 | val songsAsc: List = emptyList(),
11 | val songsDesc: List = emptyList(),
12 | val groupedAlbum: List>> = emptyList(),
13 | val groupedArtist: List>> = emptyList(),
14 | val sortOrder: SortOrder = SortOrder.DATE_ADDED,
15 | val onboarding: Boolean = true
16 | )
17 |
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/saved/component/GroupedCard.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.saved.component
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material3.Card
12 | import androidx.compose.material3.ElevatedCard
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.text.font.FontWeight
20 | import androidx.compose.ui.text.style.TextOverflow
21 | import androidx.compose.ui.unit.dp
22 | import com.shub39.rush.core.presentation.ArtFromUrl
23 | import com.shub39.rush.lyrics.domain.Song
24 | import org.jetbrains.compose.resources.stringResource
25 | import rush.app.generated.resources.Res
26 | import rush.app.generated.resources.downloaded
27 |
28 | @Composable
29 | fun GroupedCard(
30 | map: Map.Entry>,
31 | isExpanded: Boolean = false,
32 | onClick: (Song) -> Unit,
33 | onCardClick: () -> Unit,
34 | ) {
35 | Card(
36 | modifier = Modifier
37 | .fillMaxWidth()
38 | .padding(start = 16.dp, end = 16.dp, bottom = 4.dp, top = 4.dp)
39 | .clickable { onCardClick() }
40 | ) {
41 | Row(
42 | modifier = Modifier
43 | .fillMaxWidth()
44 | .padding(8.dp),
45 | verticalAlignment = Alignment.CenterVertically
46 | ) {
47 | ArtFromUrl(
48 | imageUrl = map.value.first().artUrl,
49 | modifier = Modifier
50 | .size(60.dp)
51 | .clip(MaterialTheme.shapes.small)
52 | )
53 | Column(
54 | modifier = Modifier.padding(start = 8.dp)
55 | ) {
56 | Text(
57 | text = map.key ?: "Unknown",
58 | style = MaterialTheme.typography.titleMedium,
59 | fontWeight = FontWeight.ExtraBold,
60 | maxLines = 1,
61 | overflow = TextOverflow.Ellipsis
62 | )
63 | Text(
64 | text = map.value.size.toString() + " " + stringResource(Res.string.downloaded),
65 | style = MaterialTheme.typography.bodyMedium
66 | )
67 | }
68 | }
69 |
70 | AnimatedVisibility(visible = isExpanded) {
71 | ElevatedCard(
72 | modifier = Modifier
73 | .fillMaxWidth()
74 | .padding(start = 8.dp, end = 8.dp, bottom = 8.dp, top = 4.dp)
75 | ) {
76 | map.value.forEachIndexed { index, song ->
77 | Row(
78 | modifier = Modifier
79 | .padding(8.dp)
80 | .fillMaxWidth()
81 | .clickable { onClick(song) },
82 | verticalAlignment = Alignment.CenterVertically
83 | ) {
84 | Text(
85 | text = index.plus(1).toString(),
86 | style = MaterialTheme.typography.bodyMedium,
87 | fontWeight = FontWeight.ExtraBold,
88 | modifier = Modifier.padding(4.dp),
89 | color = MaterialTheme.colorScheme.primary
90 | )
91 | Spacer(modifier = Modifier.padding(4.dp))
92 | Text(text = song.title)
93 | }
94 | }
95 | }
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/saved/component/SongCard.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.saved.component
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Delete
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.dp
25 | import com.shub39.rush.core.presentation.ArtFromUrl
26 | import com.shub39.rush.lyrics.domain.Song
27 |
28 | @Composable
29 | fun SongCard(
30 | result: Song,
31 | onClick: () -> Unit,
32 | onDelete: () -> Unit,
33 | ) {
34 | Row(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .clickable { onClick() }
38 | .padding(start = 15.dp, bottom = 7.5.dp, top = 7.5.dp),
39 | verticalAlignment = Alignment.CenterVertically,
40 | horizontalArrangement = Arrangement.SpaceBetween
41 | ) {
42 | Row(
43 | verticalAlignment = Alignment.CenterVertically,
44 | modifier = Modifier
45 | .fillMaxWidth(0.8f)
46 | ) {
47 | ArtFromUrl(
48 | imageUrl = result.artUrl,
49 | modifier = Modifier
50 | .size(70.dp)
51 | .clip(MaterialTheme.shapes.small),
52 | )
53 | Column(
54 | modifier = Modifier
55 | .padding(start = 10.dp)
56 | ) {
57 | Text(
58 | text = result.title,
59 | style = MaterialTheme.typography.titleMedium,
60 | fontWeight = FontWeight.Bold,
61 | maxLines = 1,
62 | overflow = TextOverflow.Ellipsis
63 | )
64 | Text(
65 | text = result.artists,
66 | style = MaterialTheme.typography.bodyMedium,
67 | maxLines = 1,
68 | overflow = TextOverflow.Ellipsis
69 | )
70 | }
71 | }
72 | Row {
73 | IconButton(onClick = { onDelete() }) {
74 | Icon(
75 | imageVector = Icons.Default.Delete,
76 | contentDescription = "Delete"
77 | )
78 | }
79 | Spacer(modifier = Modifier.width(15.dp))
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/search_sheet/SearchResultCard.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.search_sheet
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.layout.width
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.text.font.FontWeight
20 | import androidx.compose.ui.text.style.TextOverflow
21 | import androidx.compose.ui.unit.dp
22 | import com.shub39.rush.core.presentation.ArtFromUrl
23 | import com.shub39.rush.lyrics.domain.SearchResult
24 | import compose.icons.FontAwesomeIcons
25 | import compose.icons.fontawesomeicons.Solid
26 | import compose.icons.fontawesomeicons.solid.Download
27 |
28 | @Composable
29 | fun SearchResultCard(
30 | result: SearchResult,
31 | onClick: () -> Unit,
32 | downloaded: Boolean = false
33 | ) {
34 | Box(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .padding(horizontal = 16.dp, vertical = 8.dp)
38 | .clickable { onClick() }
39 | ) {
40 | Row(
41 | verticalAlignment = Alignment.CenterVertically
42 | ) {
43 | ArtFromUrl(
44 | imageUrl = result.artUrl,
45 | modifier = Modifier
46 | .size(80.dp)
47 | .clip(MaterialTheme.shapes.small)
48 | )
49 |
50 | Spacer(modifier = Modifier.width(8.dp))
51 |
52 | Column {
53 | Text(
54 | text = result.title,
55 | style = MaterialTheme.typography.titleMedium,
56 | maxLines = 1,
57 | overflow = TextOverflow.Ellipsis,
58 | fontWeight = FontWeight.Bold
59 | )
60 | Text(
61 | text = result.artist,
62 | maxLines = 1,
63 | overflow = TextOverflow.Ellipsis,
64 | style = MaterialTheme.typography.bodyMedium
65 | )
66 | }
67 |
68 | Spacer(modifier = Modifier.weight(1f))
69 |
70 | if (downloaded) {
71 | Icon(
72 | imageVector = FontAwesomeIcons.Solid.Download,
73 | contentDescription = "Downloaded",
74 | modifier = Modifier.size(20.dp)
75 | )
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/search_sheet/SearchSheetAction.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.search_sheet
2 |
3 | sealed interface SearchSheetAction {
4 | data object OnToggleSearchSheet: SearchSheetAction
5 | data class OnQueryChange(val query: String): SearchSheetAction
6 | data class OnCardClicked(val id: Long): SearchSheetAction
7 | }
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/search_sheet/SearchSheetState.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.search_sheet
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.shub39.rush.lyrics.domain.SearchResult
5 | import org.jetbrains.compose.resources.StringResource
6 |
7 | @Immutable
8 | data class SearchSheetState (
9 | val visible: Boolean = false,
10 | val searchQuery: String = "",
11 | val searchResults: List = emptyList(),
12 | val localSearchResults: List = emptyList(),
13 | val isSearching: Boolean = false,
14 | val error: StringResource? = null
15 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/setting/SettingsPageState.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.setting
2 |
3 | import com.shub39.rush.core.domain.data_classes.Theme
4 | import com.shub39.rush.lyrics.domain.AudioFile
5 | import com.shub39.rush.lyrics.domain.backup.ExportState
6 | import com.shub39.rush.lyrics.domain.backup.RestoreState
7 |
8 | data class SettingsPageState(
9 | val theme: Theme = Theme(),
10 | val batchDownload: BatchDownload = BatchDownload(),
11 | val exportState: ExportState = ExportState.IDLE,
12 | val restoreState: RestoreState = RestoreState.IDLE
13 | )
14 |
15 | data class BatchDownload(
16 | val indexes: Map = emptyMap(),
17 | val audioFiles: List = emptyList(),
18 | val isDownloading: Boolean = false,
19 | val isLoadingFiles: Boolean = false,
20 | val error: Int? = null
21 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/share/SharePageState.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.share
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.toArgb
5 | import com.shub39.rush.core.domain.data_classes.ExtractedColors
6 | import com.shub39.rush.core.domain.data_classes.SongDetails
7 | import com.shub39.rush.core.domain.enums.CardColors
8 | import com.shub39.rush.core.domain.enums.CardFit
9 | import com.shub39.rush.core.domain.enums.CardTheme
10 | import com.shub39.rush.core.domain.enums.CornerRadius
11 | import com.shub39.rush.core.domain.enums.Fonts
12 |
13 | data class SharePageState(
14 | val songDetails: SongDetails = SongDetails(),
15 | val selectedLines: Map = emptyMap(),
16 | val extractedColors: ExtractedColors = ExtractedColors(),
17 |
18 | val cardFont: Fonts = Fonts.POPPINS,
19 | val cardColors: CardColors = CardColors.MUTED,
20 | val cardBackground: Int = Color.Gray.toArgb(),
21 | val cardContent: Int = Color.White.toArgb(),
22 | val cardFit: CardFit = CardFit.FIT,
23 | val cardRoundness: CornerRadius = CornerRadius.ROUNDED,
24 | val cardTheme: CardTheme = CardTheme.SPOTIFY
25 | )
--------------------------------------------------------------------------------
/app/src/commonMain/kotlin/com/shub39/rush/lyrics/presentation/viewmodels/StateLayer.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation.viewmodels
2 |
3 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsPageState
4 | import com.shub39.rush.lyrics.presentation.saved.SavedPageState
5 | import com.shub39.rush.lyrics.presentation.search_sheet.SearchSheetState
6 | import com.shub39.rush.lyrics.presentation.share.SharePageState
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 |
9 | // all the states in a single place so that they can be updated from different viewmodels
10 | class StateLayer {
11 | val lyricsState = MutableStateFlow(LyricsPageState())
12 | val searchSheetState = MutableStateFlow(SearchSheetState())
13 | val savedPageState = MutableStateFlow(SavedPageState())
14 | val sharePageState = MutableStateFlow(SharePageState())
15 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/core/data/DatastoreFactory.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.data
2 |
3 |
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import java.io.File
7 |
8 | actual class DatastoreFactory {
9 | private val os = System.getProperty("os.name").lowercase()
10 | val userHome: String? = System.getProperty("user.home")
11 | val appDataDir = when {
12 | os.contains("win") -> File(System.getenv("APPDATA"), "Rush")
13 | os.contains("mac") -> File(userHome, "Library/Application Support/Rush")
14 | else -> File(userHome, ".local/share/Rush")
15 | }
16 |
17 |
18 | actual fun getLyricsPagePreferencesDataStore() : DataStore {
19 | if (!appDataDir.exists()) appDataDir.mkdirs()
20 |
21 | val dbFile = File(appDataDir, LYRICS_DATASTORE)
22 | return createDataStore(
23 | producePath = { dbFile.absolutePath }
24 | )
25 | }
26 |
27 | actual fun getOtherPreferencesDataStore(): DataStore {
28 | if (!appDataDir.exists()) appDataDir.mkdirs()
29 |
30 | val dbFile = File(appDataDir, OTHER_DATASTORE)
31 | return createDataStore(
32 | producePath = { dbFile.absolutePath }
33 | )
34 | }
35 |
36 | actual fun getSharePagePreferencesDataStore(): DataStore {
37 | if (!appDataDir.exists()) appDataDir.mkdirs()
38 |
39 | val dbFile = File(appDataDir, SHARE_DATASTORE)
40 | return createDataStore(
41 | producePath = { dbFile.absolutePath }
42 | )
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/core/data/PaletteGenerator.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.data
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.asComposeImageBitmap
5 | import androidx.compose.ui.graphics.toArgb
6 | import coil3.ImageLoader
7 | import coil3.PlatformContext
8 | import coil3.request.ImageRequest
9 | import coil3.request.SuccessResult
10 | import coil3.toBitmap
11 | import com.kmpalette.palette.graphics.Palette
12 | import com.shub39.rush.core.domain.data_classes.ExtractedColors
13 |
14 | actual class PaletteGenerator(
15 | private val imageLoader: ImageLoader
16 | ) {
17 | actual suspend fun generatePaletteFromUrl(url: String): ExtractedColors {
18 | val request = ImageRequest.Builder(PlatformContext.INSTANCE)
19 | .data(url)
20 | .build()
21 | val result = (imageLoader.execute(request) as? SuccessResult)?.image?.toBitmap()?.asComposeImageBitmap()
22 |
23 | return result?.let { bitmap ->
24 | val colors = Palette.from(bitmap).generate()
25 |
26 | ExtractedColors(
27 | cardBackgroundDominant =
28 | Color(
29 | colors.vibrantSwatch?.rgb ?: colors.lightVibrantSwatch?.rgb
30 | ?: colors.darkVibrantSwatch?.rgb ?: colors.dominantSwatch?.rgb
31 | ?: Color.DarkGray.toArgb()
32 | ),
33 | cardContentDominant =
34 | Color(
35 | colors.vibrantSwatch?.bodyTextColor
36 | ?: colors.lightVibrantSwatch?.bodyTextColor
37 | ?: colors.darkVibrantSwatch?.bodyTextColor
38 | ?: colors.dominantSwatch?.bodyTextColor
39 | ?: Color.White.toArgb()
40 | ),
41 | cardBackgroundMuted =
42 | Color(
43 | colors.mutedSwatch?.rgb ?: colors.darkMutedSwatch?.rgb
44 | ?: colors.lightMutedSwatch?.rgb ?: Color.DarkGray.toArgb()
45 | ),
46 | cardContentMuted =
47 | Color(
48 | colors.mutedSwatch?.bodyTextColor
49 | ?: colors.darkMutedSwatch?.bodyTextColor
50 | ?: colors.lightMutedSwatch?.bodyTextColor
51 | ?: Color.White.toArgb()
52 | )
53 | )
54 | } ?: ExtractedColors()
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/core/presentation/RushTheme.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import com.materialkolor.DynamicMaterialTheme
7 | import com.shub39.rush.core.domain.data_classes.Theme
8 | import com.shub39.rush.core.domain.enums.AppTheme
9 |
10 | @Composable
11 | actual fun RushTheme(
12 | state: Theme,
13 | content: @Composable (() -> Unit)
14 | ) {
15 | DynamicMaterialTheme(
16 | seedColor = Color(state.seedColor),
17 | useDarkTheme = when (state.appTheme) {
18 | AppTheme.SYSTEM -> isSystemInDarkTheme()
19 | AppTheme.LIGHT -> false
20 | AppTheme.DARK -> true
21 | },
22 | withAmoled = state.withAmoled,
23 | style = state.style,
24 | typography = provideTypography(
25 | font = state.fonts.font
26 | ),
27 | content = content
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/core/presentation/util.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.platform.ClipEntry
5 | import androidx.compose.ui.platform.Clipboard
6 | import java.awt.datatransfer.StringSelection
7 |
8 | actual fun hypnoticAvailable() = true
9 |
10 | @Composable
11 | actual fun KeepScreenOn() {}
12 |
13 | actual suspend fun Clipboard.copyToClipboard(text: String) {
14 | setClipEntry(
15 | ClipEntry(StringSelection(text))
16 | )
17 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/di/RushModules.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.di
2 |
3 | import coil3.ImageLoader
4 | import coil3.PlatformContext
5 | import coil3.request.CachePolicy
6 | import coil3.request.crossfade
7 | import com.shub39.rush.core.data.DatastoreFactory
8 | import com.shub39.rush.core.data.PaletteGenerator
9 | import com.shub39.rush.lyrics.data.backup.ExportImpl
10 | import com.shub39.rush.lyrics.data.backup.RestoreImpl
11 | import com.shub39.rush.lyrics.data.database.DatabaseFactory
12 | import com.shub39.rush.lyrics.data.listener.MediaListenerImpl
13 | import com.shub39.rush.lyrics.domain.MediaInterface
14 | import com.shub39.rush.lyrics.domain.backup.ExportRepo
15 | import com.shub39.rush.lyrics.domain.backup.RestoreRepo
16 | import org.koin.core.module.dsl.singleOf
17 | import org.koin.dsl.bind
18 | import org.koin.dsl.module
19 |
20 | actual val platformModule = module {
21 | singleOf(::DatabaseFactory)
22 | singleOf(::DatastoreFactory)
23 | singleOf(::ExportImpl).bind()
24 | singleOf(::RestoreImpl).bind()
25 | singleOf(::PaletteGenerator)
26 | singleOf(::MediaListenerImpl).bind()
27 |
28 | single {
29 | ImageLoader.Builder(PlatformContext.INSTANCE)
30 | .crossfade(true)
31 | .memoryCachePolicy(CachePolicy.ENABLED)
32 | .diskCachePolicy(CachePolicy.ENABLED)
33 | .build()
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/lyrics/data/backup/ExportImpl.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.backup
2 |
3 | import com.shub39.rush.lyrics.domain.backup.ExportRepo
4 |
5 | actual class ExportImpl: ExportRepo {
6 | override suspend fun exportToJson() {
7 | TODO("Not yet implemented")
8 | }
9 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/lyrics/data/backup/RestoreImpl.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.backup
2 |
3 | import com.shub39.rush.lyrics.domain.backup.RestoreRepo
4 | import com.shub39.rush.lyrics.domain.backup.RestoreResult
5 |
6 | actual class RestoreImpl(): RestoreRepo {
7 | override suspend fun restoreSongs(path: String): RestoreResult {
8 | TODO("Not yet implemented")
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/lyrics/data/database/DatabaseFactory.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.database
2 |
3 | import androidx.room.Room
4 | import androidx.room.RoomDatabase
5 | import java.io.File
6 |
7 | actual class DatabaseFactory {
8 | actual fun create(): RoomDatabase.Builder {
9 | val os = System.getProperty("os.name").lowercase()
10 | val useHome = System.getProperty("user.home")
11 | val appDataDir = when {
12 | os.contains("win") -> File(System.getenv("APPDATA"), "Rush")
13 | os.contains("mac") -> File(useHome, "Library/Application Support/Rush")
14 | else -> File(useHome, ".local/share/Rush")
15 | }
16 | if (!appDataDir.exists()) {
17 | appDataDir.mkdirs()
18 | }
19 | val dbFile = File(appDataDir, SongDatabase.DB_NAME)
20 |
21 | return Room.databaseBuilder(dbFile.absolutePath)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/lyrics/data/listener/MediaListenerImpl.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.data.listener
2 |
3 | import com.fleeksoft.io.BufferedReader
4 | import com.shub39.rush.lyrics.domain.MediaInterface
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.flow.MutableSharedFlow
9 | import kotlinx.coroutines.isActive
10 | import kotlinx.coroutines.launch
11 | import java.io.InputStreamReader
12 |
13 | actual class MediaListenerImpl: MediaInterface {
14 |
15 | private var pollingJob: Job? = null
16 | private val coroutineScope = CoroutineScope(Dispatchers.IO)
17 |
18 | override val playbackSpeedFlow = MutableSharedFlow()
19 | override val songInfoFlow = MutableSharedFlow>()
20 | override val songPositionFlow = MutableSharedFlow()
21 |
22 | init { startListening() }
23 |
24 | override fun destroy() {
25 | pollingJob?.cancel()
26 | }
27 |
28 | override fun startListening() {
29 | pollingJob?.cancel()
30 | pollingJob = coroutineScope.launch {
31 | while (isActive) {
32 | updateMediaInfo()
33 | }
34 | }
35 | }
36 |
37 | override fun seek(timestamp: Long) {
38 | coroutineScope.launch {
39 | try {
40 | executeCommand("playerctl position ${(timestamp/1000).toInt()}")
41 | songPositionFlow.emit(timestamp)
42 | } catch (e: Exception) {
43 | e.printStackTrace()
44 | }
45 | }
46 | }
47 |
48 | override fun pauseOrResume(resume: Boolean) {
49 | coroutineScope.launch {
50 | try {
51 | if (resume) {
52 | executeCommand("playerctl play")
53 | } else {
54 | executeCommand("playerctl pause")
55 | }
56 | } catch (e: Exception) {
57 | e.printStackTrace()
58 | }
59 | }
60 | }
61 |
62 | private suspend fun updateMediaInfo() {
63 | try {
64 | val status = executeCommand("playerctl status")
65 | val position = executeCommand("playerctl position")
66 | val metadataTitle = executeCommand("playerctl metadata xesam:title")
67 | val metadataArtist = executeCommand("playerctl metadata xesam:artist")
68 |
69 | //parse the output and emit updates
70 | playbackSpeedFlow.emit(
71 | when (status?.trim()) {
72 | "Playing" -> 1f
73 | else -> 0f
74 | }
75 | )
76 |
77 | val title = metadataTitle?.substringAfter("xesam:title ")?.trim() ?: ""
78 | val artist = metadataArtist?.substringAfter("xesam:artist ")?.trim() ?: ""
79 | songInfoFlow.emit(Pair(title, artist))
80 |
81 | val positionLong = position?.trim()?.toFloatOrNull()?.toLong() ?: 0L
82 | songPositionFlow.emit(positionLong * 1000)
83 |
84 | } catch (e: Exception) {
85 | e.printStackTrace()
86 | //handle potential errors
87 | }
88 | }
89 |
90 | private fun executeCommand(command: String): String? {
91 | return try {
92 | val process = ProcessBuilder(*command.split(" ").toTypedArray())
93 | .redirectErrorStream(true)
94 | .start()
95 |
96 | val reader = BufferedReader(InputStreamReader(process.inputStream))
97 | val output = reader.use { it.readText() }
98 | process.waitFor()
99 | output.trim()
100 | } catch (e: Exception) {
101 | e.printStackTrace()
102 | null
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/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.fillMaxSize
6 | import androidx.compose.foundation.layout.widthIn
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 | import androidx.navigation.compose.NavHost
11 | import androidx.navigation.compose.composable
12 | import androidx.navigation.compose.rememberNavController
13 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsCustomisationsPage
14 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsPageAction
15 | import com.shub39.rush.lyrics.presentation.lyrics.LyricsPageState
16 | import kotlinx.serialization.Serializable
17 |
18 | private sealed interface LyricsRoutes {
19 | @Serializable
20 | data object LyricsPage : LyricsRoutes
21 |
22 | @Serializable
23 | data object LyricsCustomisations : LyricsRoutes
24 | }
25 |
26 | @Composable
27 | fun LyricsGraph(
28 | lyricsState: LyricsPageState,
29 | lyricsAction: (LyricsPageAction) -> Unit
30 | ) {
31 | val navController = rememberNavController()
32 |
33 | NavHost(
34 | navController = navController,
35 | startDestination = LyricsRoutes.LyricsPage,
36 | enterTransition = { fadeIn() },
37 | exitTransition = { fadeOut() },
38 | popEnterTransition = { fadeIn() },
39 | popExitTransition = { fadeOut() }
40 | ) {
41 | composable {
42 | LyricsPage(
43 | onEdit = {
44 | navController.navigate(LyricsRoutes.LyricsCustomisations) {
45 | launchSingleTop = true
46 | }
47 | },
48 | action = lyricsAction,
49 | state = lyricsState
50 | )
51 | }
52 |
53 | composable {
54 | LyricsCustomisationsPage(
55 | onNavigateBack = { navController.navigateUp() },
56 | state = lyricsState,
57 | action = lyricsAction,
58 | modifier = Modifier.widthIn(max = 1000.dp).fillMaxSize()
59 | )
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/lyrics/presentation/SettingsGraph.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush.lyrics.presentation
2 |
3 | import androidx.compose.material3.Button
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 |
7 | @Composable
8 | fun SettingsGraph(
9 | onBack: () -> Unit,
10 | ) {
11 | Button(
12 | onClick = onBack
13 | ) {
14 | Text("WIP")
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/desktopMain/kotlin/com/shub39/rush/main.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush
2 |
3 | import androidx.compose.ui.unit.dp
4 | import androidx.compose.ui.window.WindowState
5 | import androidx.compose.ui.window.singleWindowApplication
6 | import com.shub39.rush.di.initKoin
7 |
8 | fun main() {
9 | initKoin()
10 |
11 | singleWindowApplication(
12 | title = "Rush",
13 | state = WindowState(width = 1200.dp, height = 900.dp),
14 | resizable = false,
15 | alwaysOnTop = true
16 | ) {
17 | RushApp()
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/shub39/rush/ApiTest.kt:
--------------------------------------------------------------------------------
1 | package com.shub39.rush
2 |
3 | import com.shub39.rush.core.data.HttpClientFactory
4 | import com.shub39.rush.lyrics.data.network.GeniusApi
5 | import com.shub39.rush.lyrics.data.network.GeniusScraper
6 | import com.shub39.rush.lyrics.data.network.LrcLibApi
7 | import kotlinx.coroutines.runBlocking
8 | import org.junit.Test
9 |
10 | class ApiTest {
11 | private val client = HttpClientFactory.create()
12 |
13 | @Test
14 | fun getGeniusSearchResults() = runBlocking {
15 | val api = GeniusApi(client)
16 | val result = api.geniusSearch("Satan in the wait")
17 | println(result)
18 | }
19 |
20 | @Test
21 | fun getGeniusSong() = runBlocking {
22 | val api = GeniusApi(client)
23 | val result = api.geniusSong(3836181)
24 | println(result)
25 | }
26 |
27 | @Test
28 | fun getLrc() = runBlocking {
29 | val api = LrcLibApi(client)
30 | val result = api.getLrcLyrics("Little lamb", "sematary")
31 | println(result)
32 | }
33 |
34 | @Test
35 | fun searchLrc() = runBlocking {
36 | val api = LrcLibApi(client)
37 | val result = api.searchLrcLyrics("talk talk", "charli xcx")
38 | println(result)
39 | }
40 |
41 | @Test
42 | fun scrape() = runBlocking {
43 | val scraper = GeniusScraper(client)
44 | val lyrics = scraper.scrapeLyrics("https://genius.com/Vestron-vulture-sadistic-glee-lyrics")
45 | println(lyrics)
46 | }
47 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.compose.compiler) apply false
4 | alias(libs.plugins.ksp) apply false
5 | alias(libs.plugins.composeMultiplatform) apply false
6 | alias(libs.plugins.kotlinMultiplatform) apply false
7 | }
8 |
9 | buildscript {
10 | dependencies {
11 | classpath(libs.kotlin.gradle.plugin)
12 | }
13 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar-DZ/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush هو تطبيق أندرويد بسيط للبحث عن كلمات الأغاني المفضلة لديك وعرضها وحفظها ومشاركتها
2 |
3 | الميزات
4 |
5 | - البحث عن كلمات الأغاني وعرضها وتخزينها
6 | - مشاركة كلمات الأغاني كبطاقة قابلة للتخصيص مع أصدقائك!
7 | - البحث التلقائي عن الأغنية الحالية (يتطلب إذن الوصول إلى الإشعارات)
8 | - كلمات الأغاني المتزامنة
9 | - تنزيل كلمات الأغاني دفعة واحدة
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar-DZ/short_description.txt:
--------------------------------------------------------------------------------
1 | تطبيق للبحث عن كلمات الأغاني وعرضها وحفظها ومشاركتها
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar-DZ/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/as/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush এটা সহজ Android এপ যি আপোনাৰ প্ৰিয় গীতবোৰৰ গীতৰ কলি বিচাৰি পোৱা, চাব পৰা, সংৰক্ষণ কৰা আৰু শ্বেয়াৰ কৰিব পাৰে।
2 |
3 | বৈশিষ্ট্যসমূহ
4 |
5 | - গীতৰ কলি বিচৰা, চোৱা আৰু সংৰক্ষণ কৰা
6 | - গীতৰ কলি এটা নিজানুকূল কাৰ্ড হিচাপে আপোনাৰ বন্ধু-বান্ধৱীৰ সৈতে শ্বেয়াৰ কৰক!
7 | - বৰ্তমান বজাই থকা গীতটো আপোনা-আপোনি চিনাক্ত কৰে (ইয়াৰ বাবে "বতৰনি প্ৰৱেশাধিকাৰ" অনুমতিৰ প্ৰয়োজন)
8 | - মিলি থকা কলি (সমন্বিত গীতৰ কলি)
9 | - একেলগে বহুত গীতৰ কলি ডাউনলোড কৰক
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/as/short_description.txt:
--------------------------------------------------------------------------------
1 | গীতৰ কথা সন্ধান, চাব, সংৰক্ষণ আৰু শ্বেয়াৰ কৰিবলৈ এপ
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/as/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush ist eine einfache App, um Songtexte deiner Lieblingssongs zu suchen, anzusehen, zu speichern und zu teilen.
2 |
3 | Funktionen:
4 |
5 | - Songtexte suchen, ansehen und speichern
6 | - Songtexte als anpassbare Karte mit deinen Freunden teilen!
7 | - Automatische Suche nach dem aktuell gespielten Song (Erfordert Benachrichtigungszugriff)
8 | - Synchronisierte Songtexte
9 | - Songtexte im Batch herunterladen
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/short_description.txt:
--------------------------------------------------------------------------------
1 | App zum Suchen, Ansehen, Speichern und Teilen von Songtexten
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de-DE/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush is a simple android app to search, view, save and share lyrics for your favourite Songs
2 |
3 | Features
4 |
5 | - Search, View and store lyrics
6 | - Share lyrics as a customisable card to your friends!
7 | - Auto-Search current playing Song (Requires Notification Access permission)
8 | - Synced Lyrics
9 | - Batch download lyrics
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon200x200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/icon200x200.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | App to search, view, save and share lyrics
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Rush - Lyrics App
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush es una sencilla aplicación para Android para buscar, ver, guardar y compartir la letra de sus canciones favoritas.
2 |
3 | Funcionalidades
4 |
5 | - Buscar, leer y almacenar letras
6 | - Comparta la letra en una tarjeta personalizable con sus amigos
7 | - Búsqueda automática de la canción que se está reproduciendo actualmente (requiere permiso de acceso a notificaciones)
8 | - Letras sincronizadas
9 | - Descarga de letras por lotes
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplicación para buscar, ver, guardar y compartir letras
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/title.txt:
--------------------------------------------------------------------------------
1 | Rush: letras de canciones
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/et/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush on lihtne Androidi rakendus sinu lemmiklaulude sõnade otsimiseks, vaatamiseks, salvestamiseks ja jagamiseks
2 |
3 | Funktsionaalsused
4 |
5 | - Otsi, vaata ja salvesta laulusõnu
6 | - Jaga kohendatava kaardina laulusõnu oma sõpradega!
7 | - Otsi esitamisel oleva laulu sõnu automaatselt (eeldab, et rakendusel on õigus teavitusi saata)
8 | - Sünkroniseeritud laulusõnad
9 | - Laadi laulusõnad alla pakktöötlusena
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/et/short_description.txt:
--------------------------------------------------------------------------------
1 | Rakendus, mis aitab otsida, vaadata, salvestada ja jagada laulusõnu
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/et/title.txt:
--------------------------------------------------------------------------------
1 | Rush - laulusõnade tekstid
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fa-IR/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush یک برنامه اندرویدی ساده برای کاوش، مشاهده، نگهداری و همرسانی متن ترانهها است
2 |
3 | ویژگیها:
4 |
5 | - کاوش، مشاهده و نگهداری متن ترانهها
6 | - همرسانی متن ترانهها در قالب کارت قابل سفارشیسازی با دوستان!
7 | - کاوش خودکار ترانهی در حال پخش (نیاز به اجازهی دسترسی به اعلانها)
8 | - متن ترانههای هماهنگ با موسیقی
9 | - دریافت همزمان چندین متن ترانه
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fa-IR/short_description.txt:
--------------------------------------------------------------------------------
1 | کارهای برای کاوش، مشاهده، نگهداری و همرسانی متن ترانهها
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fa-IR/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush est une application android pour rechercher, visionner, sauvegarder et partager les paroles de vos musiques préférées
2 |
3 | Fonctionnalités
4 |
5 | - Rechercher, visionner et enregistrer des paroles
6 | - Partagez les paroles avec une carte personnalisable à vos amis !
7 | - Recherche automatique de la musique actuellement jouée (requiert la permission Accès aux notifications)
8 | - Paroles synchronisées
9 | - Téléchargez les paroles en lot
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/short_description.txt:
--------------------------------------------------------------------------------
1 | App pour rechercher, visionner, enregistrer et partager des paroles
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/hi-IN/full_description.txt:
--------------------------------------------------------------------------------
1 | रश आपके पसंदीदा गानों के बोल खोजने, देखने, सहेजने और साझा करने के लिए एक सरल एंड्रॉइड ऐप है
2 |
3 | विशेषताएँ
4 |
5 | - बोल खोजें, देखें और संग्रहीत करें
6 | - अपने दोस्तों के साथ कस्टमाइज़ करने योग्य कार्ड के रूप में गीत साझा करें!
7 | - वर्तमान में चल रहे गाने को ऑटो-सर्च करें (नोटिफिकेशन एक्सेस अनुमति की आवश्यकता है)
8 | - सिंक किए गए गानों के बोल
9 | - बैच डाउनलोड गानों के बोल
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/hi-IN/short_description.txt:
--------------------------------------------------------------------------------
1 | गीत खोजने, देखने, सहेजने और साझा करने के लिए ऐप
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/hi-IN/title.txt:
--------------------------------------------------------------------------------
1 | रश
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/id-ID/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush adalah aplikasi Android sederhana untuk mencari, melihat, menyimpan, dan membagikan lirik lagu favorit Anda
2 |
3 | Fitur
4 |
5 | - Mencari, melihat, dan menyimpan lirik
6 | - Bagikan lirik ke teman Anda dalam bentuk kartu yang dapat disesuaikan!
7 | - Pencarian otomatis lagu yang sedang diputar (Memerlukan izin Akses Notifikasi)
8 | - Lirik Tersinkronisasi
9 | - Unduh lirik masal
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/id-ID/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplikasi untuk mencari, melihat, menyimpan, dan membagikan lirik
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/id-ID/title.txt:
--------------------------------------------------------------------------------
1 | Rush - Aplikasi Lirik
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush è una semplice app per android, utilizzabile per cercare, vedere, salvare e condividere i testi delle tue canzoni preferite
2 |
3 | Funzionalità:
4 |
5 | - Cerca, vedi e archivia i testi delle canzoni
6 | - Condividi parte dei testi sotto forma di una cartolina personalizzabile
7 | - Autocompleta la ricerca, utilizzando le canzoni attualmente in riproduzione(Richiede l'accesso alle notifiche)
8 | - Testi sincronizzati
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/short_description.txt:
--------------------------------------------------------------------------------
1 | App per cercare, vedere, salvare e condividere i testi delle canzoni
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/it-IT/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/iw-IL/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush היא אפליקציית אנדרואיד פשוטה לחיפוש, צפייה, שמירה ושיתוף של מילים לשירים האהובים עליכם
2 |
3 | תכונות
4 |
5 | - חיפוש, צפייה ואחסון של מילים
6 | - שתפו מילים ככרטיס הניתן להתאמה אישית עם חבריכם!
7 | - חיפוש אוטומטי של השיר הנוכחי המתנגן (דורש הרשאת גישה להתראות)
8 | - סינכרון מלים עם שירים
9 | - הורדה מילים לשירים
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/iw-IL/short_description.txt:
--------------------------------------------------------------------------------
1 | אפליקציה לחיפוש, צפייה, שמירה ושיתוף של מילים
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/iw-IL/title.txt:
--------------------------------------------------------------------------------
1 | Rush - אפלקציית כתוביות
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ja-JP/full_description.txt:
--------------------------------------------------------------------------------
1 | Rushは、お気に入りの曲の歌詞を検索、表示、保存、共有できるシンプルなAndroidアプリです。🌟
2 |
3 | 特徴 🌠
4 |
5 | - 歌詞の検索、表示、保存
6 | - 歌詞をカスタマイズ可能なカードとして友達と共有しましょう!
7 | - 検索時に現在再生中の曲を自動入力(通知アクセス許可が必要)
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ja-JP/short_description.txt:
--------------------------------------------------------------------------------
1 | 歌詞を検索、表示、保存、共有できるアプリです。
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ja-JP/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-rBR/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush é um aplicativo Android simples para pesquisar, visualizar, salvar e compartilhar letras de suas músicas favoritas
2 |
3 | Funcionalidades
4 |
5 | - Pesquise, visualize e armazene letras
6 | - Compartilhe letras como um cartão personalizável para seus amigos!
7 | - Busca automática da música atual em reprodução (Requer permissão de Acesso à Notificação)
8 | - Letras Sincronizadas
9 | - Download em lote de letras
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-rBR/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplicativo para pesquisar, visualizar, salvar e compartilhar letras
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/pt-rBR/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ro-rRO/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush este o aplicație Android simplă pentru a căuta, vizualiza, salva și distribui versurile melodiilor tale preferate.
2 |
3 | Funcționalități
4 |
5 | - Căutare, vizualizare și stocare a versurilor
6 | - Partajează versurile ca un card personalizabil prietenilor tăi!
7 | - Căutare automată a melodiei redate în prezent (necesită permisiunea de acces la notificări)
8 | - Versuri sincronizate
9 | - Descărcare în lot a versurilor
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ro-rRO/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplicație pentru căutarea, vizualizarea, salvarea și partajarea versurilor
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ro-rRO/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru-RU/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush это простое приложение для того чтобы искать, смотреть, сохранять и делиться словами ваших любимых песен
2 |
3 | Возможности
4 |
5 | - Поиск, просмотр и хранение текстов
6 | - Делитесь текстами как карточками с вашими друзьями!
7 | - Авто-поиск сейчас звучащей песни (требуется разрешение на уведомления)
8 | - Синхронизация слов
9 | - Пакетное скачивание текстов
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru-RU/short_description.txt:
--------------------------------------------------------------------------------
1 | Ищите, просматривайте, сохраняйте и делитесь текстами песен
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru-RU/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/tr-TR/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush, en sevdiğiniz şarkıların sözlerini aramak, görüntülemek, kaydetmek ve paylaşmak için basit bir Android uygulamasıdır.
2 |
3 | Özellikler
4 |
5 | - Şarkı sözlerini arama, görüntüleme ve kaydetme
6 | - Şarkı sözlerini özelleştirilebilir bir kart olarak arkadaşlarınızla paylaşma!
7 | - Arama sırasında oynatılan mevcut şarkıyı otomatik doldurma (Bildirim Erişimi izni gerektirir)
8 | - Senkronize şarkı sözleri
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/tr-TR/short_description.txt:
--------------------------------------------------------------------------------
1 | Şarkı sözlerini aramak, görüntülemek, kaydetmek ve paylaşmak için mükemmel bir uygulama
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/tr-TR/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk-UA/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush — це простий додаток для Android, який дозволяє шукати, переглядати, зберігати та ділитися текстами ваших улюблених пісень.
2 |
3 | Особливості:
4 |
5 | - Шукайте, переглядайте та зберігайте тексти пісень
6 | - Налаштовуйте вигляд картки та діліться текстами у вигляді картки з друзями!
7 | - Автоматичний пошук текстів для пісні, що відтворюється (потрібен дозвіл на доступ до сповіщень)
8 | - Синхронізований текст
9 | - Пакетне завантаження текстів
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk-UA/short_description.txt:
--------------------------------------------------------------------------------
1 | Додаток для пошуку, перегляду, збереження та обміну текстами пісень
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/uk-UA/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-rCN/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush是一款简单的Android应用程序,可以搜索、查看、保存和分享您喜爱的歌曲的歌词🌟
2 |
3 | 功能🌠
4 |
5 | - 搜索、查看、保存歌词
6 | - 将歌词变为定制的卡片分享给朋友!
7 | - 自动搜索当前播放的歌曲(需要通知访问权限)
8 | - 同步歌词
9 | - 批量下载歌词
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-rCN/short_description.txt:
--------------------------------------------------------------------------------
1 | 能搜索、查看、保存和分享歌词的应用程序
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-rCN/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-rTW/full_description.txt:
--------------------------------------------------------------------------------
1 | Rush是一款簡單的Android應用程式,可以搜索、檢視、儲存和分享您喜愛的歌曲的歌詞🌟
2 |
3 | 功能🌠
4 |
5 | - 搜索、檢視、儲存歌詞
6 | - 將歌詞變為自訂的卡片分享給朋友!
7 | - 自動搜索目前播放的歌曲(需要通知存取權限)
8 | - 同步歌詞
9 | - 批量下載歌詞
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-rTW/short_description.txt:
--------------------------------------------------------------------------------
1 | 能搜索、檢視、儲存和分享歌詞的應用程式
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/zh-rTW/title.txt:
--------------------------------------------------------------------------------
1 | Rush
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | android.useAndroidX=true
3 | kotlin.code.style=official
4 | android.nonTransitiveRClass=true
5 | android.suppressUnsupportedCompileSdk=35
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shub39/Rush/a59f9383019442dd8fcaed317265e1e975ce4166/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jun 17 18:41:13 IST 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 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/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 | }
13 | }
14 |
15 | dependencyResolutionManagement {
16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17 | repositories {
18 | google()
19 | mavenCentral()
20 | gradlePluginPortal()
21 | }
22 | }
23 |
24 | rootProject.name = "Rush"
25 | include(":app")
--------------------------------------------------------------------------------