├── .ci └── lint.sh ├── .codespellrc ├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── checks.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── io.github.teccheck.fastlyrics.api.storage.LyricsDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ └── 5.json └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── teccheck │ │ └── fastlyrics │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── teccheck │ │ │ └── fastlyrics │ │ │ ├── BaseActivity.kt │ │ │ ├── FastLyricsApp.kt │ │ │ ├── MainActivity.kt │ │ │ ├── Settings.kt │ │ │ ├── Tokens.kt │ │ │ ├── api │ │ │ ├── LyricStorage.kt │ │ │ ├── LyricsApi.kt │ │ │ ├── MediaSession.kt │ │ │ ├── provider │ │ │ │ ├── Deezer.kt │ │ │ │ ├── Genius.kt │ │ │ │ ├── LrcLib.kt │ │ │ │ ├── LyricsProvider.kt │ │ │ │ ├── Netease.kt │ │ │ │ └── PetitLyrics.kt │ │ │ └── storage │ │ │ │ ├── LyricsDatabase.kt │ │ │ │ └── SongsDao.kt │ │ │ ├── exceptions │ │ │ ├── GenericException.kt │ │ │ ├── LyricsApiException.kt │ │ │ ├── LyricsNotFoundException.kt │ │ │ ├── NetworkException.kt │ │ │ ├── NoMusicPlayingException.kt │ │ │ ├── NoNotifPermsException.kt │ │ │ └── ParseException.kt │ │ │ ├── model │ │ │ ├── LyricsType.kt │ │ │ ├── SearchResult.kt │ │ │ ├── SongMeta.kt │ │ │ ├── SongWithLyrics.kt │ │ │ └── SyncedLyrics.kt │ │ │ ├── service │ │ │ └── DummyNotificationListenerService.kt │ │ │ ├── ui │ │ │ ├── about │ │ │ │ ├── AboutActivity.kt │ │ │ │ └── RecyclerAdapter.kt │ │ │ ├── fastlyrics │ │ │ │ ├── FastLyricsFragment.kt │ │ │ │ ├── FastLyricsViewModel.kt │ │ │ │ └── States.kt │ │ │ ├── permission │ │ │ │ └── PermissionActivity.kt │ │ │ ├── saved │ │ │ │ ├── DetailsLookup.kt │ │ │ │ ├── RecyclerAdapter.kt │ │ │ │ ├── SavedActivity.kt │ │ │ │ └── SavedViewModel.kt │ │ │ ├── search │ │ │ │ ├── DetailsLookup.kt │ │ │ │ ├── RecyclerAdapter.kt │ │ │ │ ├── SearchFragment.kt │ │ │ │ ├── SearchTimer.kt │ │ │ │ ├── SearchViewModel.kt │ │ │ │ └── SelectionPredicate.kt │ │ │ ├── settings │ │ │ │ ├── ProviderRecyclerAdapter.kt │ │ │ │ ├── ProviderSettingsFragment.kt │ │ │ │ ├── SettingsActivity.kt │ │ │ │ └── SettingsFragment.kt │ │ │ └── viewlyrics │ │ │ │ ├── ViewLyricsActivity.kt │ │ │ │ └── ViewLyricsViewModel.kt │ │ │ └── utils │ │ │ ├── DebouncedMutableLiveData.kt │ │ │ ├── PlaceholderDrawable.kt │ │ │ ├── ProviderOrder.kt │ │ │ └── Utils.kt │ └── res │ │ ├── drawable │ │ ├── baseline_arrow_forward_24.xml │ │ ├── baseline_check_circle_24.xml │ │ ├── baseline_close_24.xml │ │ ├── baseline_code_24.xml │ │ ├── baseline_drag_handle_24.xml │ │ ├── baseline_error_outline_24.xml │ │ ├── baseline_open_in_new_24.xml │ │ ├── deezer.xml │ │ ├── fastlyrics.xml │ │ ├── genius.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── lrclib.xml │ │ ├── netease.xml │ │ ├── outline_cloud_download_24.xml │ │ ├── outline_delete_24.xml │ │ ├── outline_info_24.xml │ │ ├── outline_music_off_24.xml │ │ ├── outline_notifications_24.xml │ │ ├── outline_queue_music_24.xml │ │ ├── outline_search_24.xml │ │ ├── outline_settings_24.xml │ │ ├── petitlyrics.xml │ │ └── round_music_note_24.xml │ │ ├── layout │ │ ├── activity_about.xml │ │ ├── activity_main.xml │ │ ├── activity_permission.xml │ │ ├── activity_saved.xml │ │ ├── activity_settings.xml │ │ ├── activity_view_lyrics.xml │ │ ├── app_bar_main.xml │ │ ├── fragment_fast_lyrics.xml │ │ ├── fragment_provider_settings.xml │ │ ├── fragment_search.xml │ │ ├── layout_error.xml │ │ ├── layout_header.xml │ │ ├── layout_lyrics.xml │ │ ├── layout_toolbar.xml │ │ ├── list_item_provider.xml │ │ ├── list_item_provider_setting.xml │ │ ├── list_item_song.xml │ │ ├── nav_header_main.xml │ │ └── preference_widget_switch_m3.xml │ │ ├── menu │ │ ├── activity_main_drawer.xml │ │ ├── fragment_saved_contextual_appbar_menu.xml │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ └── mobile_navigation.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-night │ │ ├── colors.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ ├── locales_config.xml │ │ └── root_preferences.xml │ └── test │ └── java │ └── io │ └── github │ └── teccheck │ └── fastlyrics │ └── ExampleUnitTest.kt ├── assets └── screenshots.svg ├── build.gradle ├── fastlane ├── metadata │ └── android │ │ ├── de-DE │ │ ├── changelogs │ │ │ ├── 1.txt │ │ │ ├── 2.txt │ │ │ ├── 3.txt │ │ │ ├── 4.txt │ │ │ ├── 5.txt │ │ │ └── 6.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ │ ├── en-US │ │ ├── changelogs │ │ │ ├── 1.txt │ │ │ ├── 2.txt │ │ │ ├── 3.txt │ │ │ ├── 4.txt │ │ │ ├── 5.txt │ │ │ └── 6.txt │ │ ├── full_description.txt │ │ ├── images │ │ │ ├── featureGraphic.png │ │ │ ├── icon.png │ │ │ └── phoneScreenshots │ │ │ │ ├── 1.png │ │ │ │ ├── 2.png │ │ │ │ ├── 3.png │ │ │ │ ├── 4.png │ │ │ │ ├── 5.png │ │ │ │ ├── 6.png │ │ │ │ └── 7.png │ │ ├── short_description.txt │ │ └── title.txt │ │ ├── it │ │ ├── changelogs │ │ │ ├── 1.txt │ │ │ └── 2.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ │ └── pl │ │ ├── changelogs │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ └── 4.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt └── svg │ ├── AppIcon.svg │ └── Banner.svg ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.ci/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FAILED=0 4 | STEP_MESSAGES= 5 | 6 | fail() { 7 | FAILED=$((FAILED + 1)) 8 | STEP_MESSAGES="${STEP_MESSAGES} Step \"$1\" failed with code $2 9 | " 10 | } 11 | 12 | run_codespell() { 13 | codespell || fail codespell $? 14 | } 15 | 16 | run_ktlint() { 17 | ktlint || fail ktlint $? 18 | } 19 | 20 | all() { 21 | run_codespell 22 | run_ktlint 23 | } 24 | 25 | main() { 26 | set -xeu 27 | 28 | if [ $# -ge 1 ]; then 29 | "run_$(printf '%s' "$1" | tr '-' '_')" 30 | else 31 | all 32 | fi 33 | 34 | if [ $FAILED -eq 0 ]; then 35 | printf "\n\e[92mSUMMARY: All jobs succesfull\e[0m\n" 36 | else 37 | printf "\n\e[91mSUMMARY (%d %s failed):\e[0m\n%s" $FAILED "job$([ $FAILED -ne 1 ] && printf s)" "$STEP_MESSAGES" 38 | fi 39 | 40 | return $FAILED 41 | } 42 | 43 | main "$@" 44 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = ./gradle,.idea,./app/build,./assets,./fastlane,./app/src/main/res/values-*/strings.xml 3 | 4 | ignore-words-list = showIn 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | max_line_length = 120 10 | 11 | [*.{kt,kts}] 12 | ktlint_code_style = android_studio 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/.github/workflows/build.yml -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize, reopened ] 6 | push: 7 | branches: [ master ] 8 | 9 | jobs: 10 | wrapper-validation: 11 | name: "Gradle Wrapper Validation" 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 45 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: gradle/wrapper-validation-action@v2 17 | 18 | ktlint: 19 | name: "ktlint" 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 45 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v4 26 | with: 27 | distribution: 'temurin' 28 | java-version: '17' 29 | - name: "Install ktlint" 30 | run: | 31 | curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ 32 | - name: "Run ktlint" 33 | run: sh .ci/lint.sh ktlint 34 | 35 | codespell: 36 | name: "codespell" 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 45 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: "Install codespell" 42 | run: sudo apt install codespell -y 43 | - name: "Run codespell" 44 | run: sh .ci/lint.sh codespell 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .gradle/ 3 | .idea 4 | .idea/ 5 | *.iml 6 | local.properties 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | .cxx 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/v/release/TecCheck/FastLyrics?include_prereleases)](https://github.com/TecCheck/FastLyrics/releases) 2 | 3 | # FastLyrics 4 | 5 | > How to get lyrics online (fast) 6 | 7 | FastLyrics is an app that downloads lyrics for the song, you're listening to. It is mostly a clone of [QuickLyric](https://github.com/QuickLyric/QuickLyric), but more modern and in active development. 8 | 9 | [Get it on F-Droid](https://f-droid.org/packages/io.github.teccheck.fastlyrics/) 12 | 13 | Or download the latest APK from the [Releases Section](https://github.com/TecCheck/FastLyrics/releases/latest). 14 | 15 | ## Features 16 | Not all of the features, planned are currently implemented. Here's an overview. 17 | 18 | * [x] Getting the song, currently playing on the device 19 | * [x] Fetching lyrics for a playing song 20 | * [x] Saving lyrics for offline use 21 | * [x] Material 1 and 2 Design 22 | * [x] Manual search 23 | * [ ] Nice onboarding UI 24 | * [x] Automatic refresh, once the current song changes 25 | 26 | ## Screenshots 27 | ![](assets/screenshots.svg) 28 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'com.google.devtools.ksp' version '2.0.0-1.0.22' 5 | } 6 | 7 | android { 8 | namespace 'io.github.teccheck.fastlyrics' 9 | 10 | compileSdk 35 11 | 12 | defaultConfig { 13 | applicationId "io.github.teccheck.fastlyrics" 14 | minSdk 22 15 | targetSdk 35 16 | versionCode 11 17 | versionName "0.6.2" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_17 30 | targetCompatibility JavaVersion.VERSION_17 31 | } 32 | kotlinOptions { 33 | jvmTarget = '17' 34 | } 35 | buildFeatures { 36 | viewBinding true 37 | buildConfig true 38 | } 39 | 40 | dependenciesInfo { 41 | // Disables dependency metadata when building APKs. 42 | includeInApk = false 43 | // Disables dependency metadata when building Android App Bundles. 44 | includeInBundle = false 45 | } 46 | } 47 | 48 | // For KSP, configure using KSP extension: 49 | ksp { 50 | arg('room.schemaLocation', "$projectDir/schemas") 51 | } 52 | 53 | dependencies { 54 | implementation 'androidx.core:core-ktx:1.15.0' 55 | implementation 'androidx.appcompat:appcompat:1.7.0' 56 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0' 57 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7' 58 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' 59 | implementation 'androidx.navigation:navigation-fragment-ktx:2.8.5' 60 | implementation 'androidx.navigation:navigation-ui-ktx:2.8.5' 61 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 62 | implementation 'androidx.preference:preference-ktx:1.2.1' 63 | 64 | implementation 'com.google.android.material:material:1.12.0' 65 | implementation 'com.google.code.gson:gson:2.10.1' 66 | 67 | implementation 'com.squareup.picasso:picasso:2.71828' 68 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 69 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 70 | 71 | implementation(platform('dev.forkhandles:forkhandles-bom:2.12.2.0')) 72 | implementation("dev.forkhandles:result4k") 73 | 74 | implementation 'androidx.room:room-runtime:2.6.1' 75 | implementation 'androidx.room:room-ktx:2.6.1' 76 | implementation 'androidx.recyclerview:recyclerview-selection:1.1.0' 77 | 78 | implementation 'com.github.Moriafly:LyricViewX:1.3.2' 79 | implementation 'androidx.activity:activity-ktx:1.9.3' 80 | 81 | annotationProcessor 'androidx.room:room-compiler:2.6.1' 82 | ksp 'androidx.room:room-compiler:2.6.1' 83 | 84 | testImplementation 'junit:junit:4.13.2' 85 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 86 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 87 | } 88 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/schemas/io.github.teccheck.fastlyrics.api.storage.LyricsDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "fa40dfd08bb655814cdf917cfd3f3ad4", 6 | "entities": [ 7 | { 8 | "tableName": "songs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `sourceUrl` TEXT NOT NULL, `album` TEXT, `artUrl` TEXT)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "artist", 25 | "columnName": "artist", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "lyrics", 31 | "columnName": "lyrics", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "sourceUrl", 37 | "columnName": "sourceUrl", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "album", 43 | "columnName": "album", 44 | "affinity": "TEXT", 45 | "notNull": false 46 | }, 47 | { 48 | "fieldPath": "artUrl", 49 | "columnName": "artUrl", 50 | "affinity": "TEXT", 51 | "notNull": false 52 | } 53 | ], 54 | "primaryKey": { 55 | "autoGenerate": true, 56 | "columnNames": [ 57 | "id" 58 | ] 59 | }, 60 | "indices": [], 61 | "foreignKeys": [] 62 | } 63 | ], 64 | "views": [], 65 | "setupQueries": [ 66 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 67 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa40dfd08bb655814cdf917cfd3f3ad4')" 68 | ] 69 | } 70 | } -------------------------------------------------------------------------------- /app/schemas/io.github.teccheck.fastlyrics.api.storage.LyricsDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "e6f48320db21e766e03d74e51d1c2748", 6 | "entities": [ 7 | { 8 | "tableName": "songs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `sourceUrl` TEXT NOT NULL, `album` TEXT, `artUrl` TEXT, `type` TEXT NOT NULL DEFAULT 'RAW_TEXT')", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "artist", 25 | "columnName": "artist", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "lyrics", 31 | "columnName": "lyrics", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "sourceUrl", 37 | "columnName": "sourceUrl", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "album", 43 | "columnName": "album", 44 | "affinity": "TEXT", 45 | "notNull": false 46 | }, 47 | { 48 | "fieldPath": "artUrl", 49 | "columnName": "artUrl", 50 | "affinity": "TEXT", 51 | "notNull": false 52 | }, 53 | { 54 | "fieldPath": "type", 55 | "columnName": "type", 56 | "affinity": "TEXT", 57 | "notNull": true, 58 | "defaultValue": "'RAW_TEXT'" 59 | } 60 | ], 61 | "primaryKey": { 62 | "autoGenerate": true, 63 | "columnNames": [ 64 | "id" 65 | ] 66 | }, 67 | "indices": [], 68 | "foreignKeys": [] 69 | } 70 | ], 71 | "views": [], 72 | "setupQueries": [ 73 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 74 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e6f48320db21e766e03d74e51d1c2748')" 75 | ] 76 | } 77 | } -------------------------------------------------------------------------------- /app/schemas/io.github.teccheck.fastlyrics.api.storage.LyricsDatabase/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 3, 5 | "identityHash": "7e31f87487b12d0a44dc17b5e4335f05", 6 | "entities": [ 7 | { 8 | "tableName": "songs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `sourceUrl` TEXT NOT NULL, `album` TEXT, `artUrl` TEXT, `type` TEXT NOT NULL DEFAULT 'RAW_TEXT', `provider` TEXT NOT NULL DEFAULT 'genius')", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "artist", 25 | "columnName": "artist", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "lyrics", 31 | "columnName": "lyrics", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "sourceUrl", 37 | "columnName": "sourceUrl", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "album", 43 | "columnName": "album", 44 | "affinity": "TEXT", 45 | "notNull": false 46 | }, 47 | { 48 | "fieldPath": "artUrl", 49 | "columnName": "artUrl", 50 | "affinity": "TEXT", 51 | "notNull": false 52 | }, 53 | { 54 | "fieldPath": "type", 55 | "columnName": "type", 56 | "affinity": "TEXT", 57 | "notNull": true, 58 | "defaultValue": "'RAW_TEXT'" 59 | }, 60 | { 61 | "fieldPath": "provider", 62 | "columnName": "provider", 63 | "affinity": "TEXT", 64 | "notNull": true, 65 | "defaultValue": "'genius'" 66 | } 67 | ], 68 | "primaryKey": { 69 | "autoGenerate": true, 70 | "columnNames": [ 71 | "id" 72 | ] 73 | }, 74 | "indices": [], 75 | "foreignKeys": [] 76 | } 77 | ], 78 | "views": [], 79 | "setupQueries": [ 80 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 81 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7e31f87487b12d0a44dc17b5e4335f05')" 82 | ] 83 | } 84 | } -------------------------------------------------------------------------------- /app/schemas/io.github.teccheck.fastlyrics.api.storage.LyricsDatabase/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 4, 5 | "identityHash": "322bc07ec0f4bdd728810dcf135d3063", 6 | "entities": [ 7 | { 8 | "tableName": "songs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `lyricsPlain` TEXT, `lyricsSynced` TEXT, `sourceUrl` TEXT NOT NULL, `album` TEXT, `artUrl` TEXT, `type` TEXT NOT NULL DEFAULT 'RAW_TEXT', `provider` TEXT NOT NULL DEFAULT 'genius')", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "artist", 25 | "columnName": "artist", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "lyricsPlain", 31 | "columnName": "lyricsPlain", 32 | "affinity": "TEXT", 33 | "notNull": false 34 | }, 35 | { 36 | "fieldPath": "lyricsSynced", 37 | "columnName": "lyricsSynced", 38 | "affinity": "TEXT", 39 | "notNull": false 40 | }, 41 | { 42 | "fieldPath": "sourceUrl", 43 | "columnName": "sourceUrl", 44 | "affinity": "TEXT", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "album", 49 | "columnName": "album", 50 | "affinity": "TEXT", 51 | "notNull": false 52 | }, 53 | { 54 | "fieldPath": "artUrl", 55 | "columnName": "artUrl", 56 | "affinity": "TEXT", 57 | "notNull": false 58 | }, 59 | { 60 | "fieldPath": "type", 61 | "columnName": "type", 62 | "affinity": "TEXT", 63 | "notNull": true, 64 | "defaultValue": "'RAW_TEXT'" 65 | }, 66 | { 67 | "fieldPath": "provider", 68 | "columnName": "provider", 69 | "affinity": "TEXT", 70 | "notNull": true, 71 | "defaultValue": "'genius'" 72 | } 73 | ], 74 | "primaryKey": { 75 | "autoGenerate": true, 76 | "columnNames": [ 77 | "id" 78 | ] 79 | }, 80 | "indices": [], 81 | "foreignKeys": [] 82 | } 83 | ], 84 | "views": [], 85 | "setupQueries": [ 86 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 87 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '322bc07ec0f4bdd728810dcf135d3063')" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/schemas/io.github.teccheck.fastlyrics.api.storage.LyricsDatabase/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 5, 5 | "identityHash": "1d4c70872a679932413876bdd1f790ef", 6 | "entities": [ 7 | { 8 | "tableName": "songs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `lyricsPlain` TEXT, `lyricsSynced` TEXT, `sourceUrl` TEXT NOT NULL, `album` TEXT, `artUrl` TEXT, `duration` INTEGER, `type` TEXT NOT NULL DEFAULT 'RAW_TEXT', `provider` TEXT NOT NULL DEFAULT 'genius')", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "artist", 25 | "columnName": "artist", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "lyricsPlain", 31 | "columnName": "lyricsPlain", 32 | "affinity": "TEXT", 33 | "notNull": false 34 | }, 35 | { 36 | "fieldPath": "lyricsSynced", 37 | "columnName": "lyricsSynced", 38 | "affinity": "TEXT", 39 | "notNull": false 40 | }, 41 | { 42 | "fieldPath": "sourceUrl", 43 | "columnName": "sourceUrl", 44 | "affinity": "TEXT", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "album", 49 | "columnName": "album", 50 | "affinity": "TEXT", 51 | "notNull": false 52 | }, 53 | { 54 | "fieldPath": "artUrl", 55 | "columnName": "artUrl", 56 | "affinity": "TEXT", 57 | "notNull": false 58 | }, 59 | { 60 | "fieldPath": "duration", 61 | "columnName": "duration", 62 | "affinity": "INTEGER", 63 | "notNull": false 64 | }, 65 | { 66 | "fieldPath": "type", 67 | "columnName": "type", 68 | "affinity": "TEXT", 69 | "notNull": true, 70 | "defaultValue": "'RAW_TEXT'" 71 | }, 72 | { 73 | "fieldPath": "provider", 74 | "columnName": "provider", 75 | "affinity": "TEXT", 76 | "notNull": true, 77 | "defaultValue": "'genius'" 78 | } 79 | ], 80 | "primaryKey": { 81 | "autoGenerate": true, 82 | "columnNames": [ 83 | "id" 84 | ] 85 | }, 86 | "indices": [], 87 | "foreignKeys": [] 88 | } 89 | ], 90 | "views": [], 91 | "setupQueries": [ 92 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 93 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1d4c70872a679932413876bdd1f790ef')" 94 | ] 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/teccheck/fastlyrics/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | Assert.assertEquals("io.github.teccheck.fastlyrics", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.annotation.StringRes 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.appcompat.app.AppCompatDelegate 9 | import androidx.appcompat.widget.Toolbar 10 | 11 | abstract class BaseActivity : AppCompatActivity() { 12 | 13 | protected lateinit var settings: Settings 14 | private var homeAsUp = false 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | settings = Settings(this) 18 | setTheme(settings.getMaterialStyle()) 19 | setNightMode(settings.getAppTheme()) 20 | enableEdgeToEdge() 21 | 22 | super.onCreate(savedInstanceState) 23 | } 24 | 25 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 26 | if (homeAsUp && item.itemId == android.R.id.home) { 27 | onBackPressedDispatcher.onBackPressed() 28 | return true 29 | } 30 | 31 | return super.onOptionsItemSelected(item) 32 | } 33 | 34 | protected fun setupToolbar(toolbar: Toolbar, @StringRes title: Int? = null) { 35 | setSupportActionBar(toolbar) 36 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 37 | title?.let { supportActionBar?.setTitle(it) } 38 | homeAsUp = true 39 | } 40 | 41 | private fun setNightMode(mode: Int) { 42 | AppCompatDelegate.setDefaultNightMode(mode) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/FastLyricsApp.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | import android.app.Application 4 | import io.github.teccheck.fastlyrics.api.LyricStorage 5 | import io.github.teccheck.fastlyrics.api.MediaSession 6 | import io.github.teccheck.fastlyrics.utils.ProviderOrder 7 | 8 | class FastLyricsApp : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | LyricStorage.init(this) 13 | MediaSession.init(this) 14 | ProviderOrder.init(this) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.Menu 6 | import android.view.MenuItem 7 | import androidx.navigation.NavController 8 | import androidx.navigation.findNavController 9 | import androidx.navigation.ui.AppBarConfiguration 10 | import androidx.navigation.ui.navigateUp 11 | import androidx.navigation.ui.setupActionBarWithNavController 12 | import com.google.android.material.navigation.NavigationView 13 | import io.github.teccheck.fastlyrics.databinding.ActivityMainBinding 14 | import io.github.teccheck.fastlyrics.service.DummyNotificationListenerService 15 | import io.github.teccheck.fastlyrics.ui.about.AboutActivity 16 | import io.github.teccheck.fastlyrics.ui.permission.PermissionActivity 17 | import io.github.teccheck.fastlyrics.ui.saved.SavedActivity 18 | import io.github.teccheck.fastlyrics.ui.settings.SettingsActivity 19 | 20 | class MainActivity : 21 | BaseActivity(), 22 | NavigationView.OnNavigationItemSelectedListener { 23 | 24 | private lateinit var appBarConfiguration: AppBarConfiguration 25 | private lateinit var navController: NavController 26 | private lateinit var binding: ActivityMainBinding 27 | 28 | private var searchMenuItem: MenuItem? = null 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | binding = ActivityMainBinding.inflate(layoutInflater) 34 | setContentView(binding.root) 35 | 36 | navController = findNavController(R.id.nav_host_fragment_content_main) 37 | appBarConfiguration = AppBarConfiguration(setOf(R.id.nav_fast_lyrics), binding.drawerLayout) 38 | binding.navView.setNavigationItemSelectedListener(this) 39 | binding.navView.setCheckedItem(R.id.nav_fast_lyrics) 40 | 41 | setSupportActionBar(binding.appBarMain.toolbarLayout.toolbar) 42 | setupActionBarWithNavController(navController, appBarConfiguration) 43 | 44 | if (!DummyNotificationListenerService.canAccessNotifications(this)) { 45 | startActivity(Intent(this, PermissionActivity::class.java)) 46 | } 47 | } 48 | 49 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 50 | menuInflater.inflate(R.menu.menu_main, menu) 51 | menu?.findItem(R.id.app_bar_search)?.let { searchMenuItem = it } 52 | return super.onCreateOptionsMenu(menu) 53 | } 54 | 55 | override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 56 | R.id.app_bar_search -> { 57 | if (navController.currentDestination?.id != R.id.nav_search) { 58 | navController.navigate(R.id.nav_search) 59 | } 60 | true 61 | } 62 | 63 | else -> super.onOptionsItemSelected(item) 64 | } 65 | 66 | override fun onSupportNavigateUp(): Boolean = 67 | navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() 68 | 69 | override fun onNavigationItemSelected(item: MenuItem): Boolean { 70 | when (item.itemId) { 71 | R.id.nav_saved -> startActivity(Intent(this, SavedActivity::class.java)) 72 | R.id.nav_permission -> startActivity(Intent(this, PermissionActivity::class.java)) 73 | R.id.nav_settings -> startActivity(Intent(this, SettingsActivity::class.java)) 74 | R.id.nav_about -> startActivity(Intent(this, AboutActivity::class.java)) 75 | } 76 | 77 | binding.drawerLayout.closeDrawer(binding.navView, true) 78 | 79 | return false 80 | } 81 | 82 | fun getSearchMenuItem(): MenuItem? = searchMenuItem 83 | 84 | companion object { 85 | private const val TAG = "MainActivity" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/Settings.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.annotation.StyleRes 6 | import androidx.appcompat.app.AppCompatDelegate 7 | 8 | class Settings(context: Context) { 9 | 10 | private val sharedPreferences: SharedPreferences = 11 | context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) 12 | 13 | fun getAppTheme(): Int { 14 | return sharedPreferences.getString(KEY_APP_THEME, DEFAULT_APP_THEME)?.toInt() 15 | ?: return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 16 | } 17 | 18 | @StyleRes 19 | fun getMaterialStyle(): Int = when (sharedPreferences.getString(KEY_MATERIAL_STYLE, DEFAULT_MATERIAL_STYLE)) { 20 | MATERIAL_STYLE_ONE -> R.style.Theme_FastLyrics_Material1 21 | MATERIAL_STYLE_TWO -> R.style.Theme_FastLyrics_Material2 22 | MATERIAL_STYLE_THREE -> R.style.Theme_FastLyrics_Material3 23 | else -> R.style.Theme_FastLyrics_Material2 24 | } 25 | 26 | fun getIsAutoRefreshEnabled(): Boolean = sharedPreferences.getBoolean(KEY_AUTO_REFRESH, false) 27 | 28 | fun getSyncedLyricsByDefault(): Boolean = sharedPreferences.getBoolean(KEY_SYNCED_LYRICS_BY_DEFAULT, false) 29 | 30 | fun getTextSize(): Int = sharedPreferences.getInt(KEY_TEXT_SIZE, 18) 31 | 32 | companion object { 33 | private const val KEY_APP_THEME = "app_theme" 34 | private const val KEY_MATERIAL_STYLE = "material_style" 35 | private const val KEY_AUTO_REFRESH = "auto_refresh" 36 | private const val KEY_SYNCED_LYRICS_BY_DEFAULT = "synced_lyrics_by_default" 37 | private const val KEY_TEXT_SIZE = "text_size" 38 | 39 | private const val MATERIAL_STYLE_ONE = "1" 40 | private const val MATERIAL_STYLE_TWO = "2" 41 | private const val MATERIAL_STYLE_THREE = "3" 42 | 43 | private const val DEFAULT_APP_THEME = "-1" 44 | private const val DEFAULT_MATERIAL_STYLE = MATERIAL_STYLE_TWO 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/Tokens.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | object Tokens { 4 | const val GENIUS_API = "ZTejoT_ojOEasIkT9WrMBhBQOz6eYKK5QULCMECmOhvwqjRZ6WbpamFe3geHnvp3" 5 | const val PETIT_LYRICS_API = "p1110417" 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/api/LyricStorage.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.api 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.room.Room 7 | import dev.forkhandles.result4k.Result 8 | import io.github.teccheck.fastlyrics.api.storage.LyricsDatabase 9 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 10 | import io.github.teccheck.fastlyrics.exceptions.LyricsNotFoundException 11 | import io.github.teccheck.fastlyrics.model.LyricsType 12 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 13 | import io.github.teccheck.fastlyrics.utils.Utils 14 | import java.util.concurrent.Executors 15 | 16 | object LyricStorage { 17 | private const val TAG = "LyricsStorage" 18 | 19 | private val executor = Executors.newSingleThreadExecutor() 20 | private lateinit var database: LyricsDatabase 21 | 22 | fun init(context: Context) { 23 | database = Room.databaseBuilder(context, LyricsDatabase::class.java, "lyrics") 24 | .addMigrations(LyricsDatabase.MIGRATION_3_4) 25 | .build() 26 | } 27 | 28 | fun deleteAsync(ids: List) { 29 | executor.submit { database.songsDao().deleteAll(ids) } 30 | } 31 | 32 | fun getSongsAsync(liveDataTarget: MutableLiveData, LyricsApiException>>) { 33 | Log.d(TAG, "fetchSongsAsync") 34 | executor.submit { 35 | liveDataTarget.postValue( 36 | Utils.result( 37 | database.songsDao().getAll(), 38 | LyricsNotFoundException() 39 | ) 40 | ) 41 | } 42 | } 43 | 44 | fun getSongAsync(id: Long, liveDataTarget: MutableLiveData>) { 45 | executor.submit { 46 | liveDataTarget.postValue( 47 | Utils.result(getSong(id), LyricsNotFoundException()) 48 | ) 49 | } 50 | } 51 | 52 | fun store(song: SongWithLyrics) { 53 | if (findSong(song.title, song.artist, song.type) == null) { 54 | database.songsDao().insert(song) 55 | } 56 | } 57 | 58 | private fun getSong(id: Long): SongWithLyrics? = database.songsDao().getSong(id) 59 | 60 | fun findSong(title: String, artist: String): SongWithLyrics? = database.songsDao().findSong(title, artist) 61 | 62 | fun findSong(title: String, artist: String, type: LyricsType): SongWithLyrics? = 63 | database.songsDao().findSong(title, artist, type) 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/api/LyricsApi.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.api 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import dev.forkhandles.result4k.Failure 6 | import dev.forkhandles.result4k.Result 7 | import dev.forkhandles.result4k.Success 8 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 9 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 10 | import io.github.teccheck.fastlyrics.exceptions.LyricsNotFoundException 11 | import io.github.teccheck.fastlyrics.model.LyricsType 12 | import io.github.teccheck.fastlyrics.model.SearchResult 13 | import io.github.teccheck.fastlyrics.model.SongMeta 14 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 15 | import io.github.teccheck.fastlyrics.utils.ProviderOrder 16 | import java.util.concurrent.Executors 17 | 18 | object LyricsApi { 19 | private const val TAG = "LyricsApi" 20 | 21 | private val executor = Executors.newFixedThreadPool(2) 22 | 23 | private val providers: Array get() = ProviderOrder.providers 24 | private val defaultProvider: LyricsProvider get() = providers.first() 25 | 26 | fun getLyricsAsync( 27 | songMeta: SongMeta, 28 | liveDataTarget: MutableLiveData>, 29 | synced: Boolean = false 30 | ) { 31 | executor.submit { 32 | Log.d(TAG, "getLyricsAsync($songMeta, $synced)") 33 | 34 | val type = if (synced) LyricsType.LRC else LyricsType.RAW_TEXT 35 | val song = songMeta.artist?.let { LyricStorage.findSong(songMeta.title, it, type) } 36 | if (song != null) { 37 | Log.d(TAG, "Found cached: $song") 38 | liveDataTarget.postValue(Success(song)) 39 | return@submit 40 | } 41 | 42 | val result = fetchLyrics(songMeta, synced) 43 | liveDataTarget.postValue(result) 44 | 45 | if (result is Success) { 46 | LyricStorage.store(result.value) 47 | } 48 | } 49 | } 50 | 51 | fun getLyricsAsync( 52 | searchResult: SearchResult, 53 | liveDataTarget: MutableLiveData> 54 | ) { 55 | executor.submit { 56 | val result = fetchLyrics(searchResult) 57 | liveDataTarget.postValue(result) 58 | 59 | if (result is Success) { 60 | LyricStorage.store(result.value) 61 | } 62 | } 63 | } 64 | 65 | fun search( 66 | query: String, 67 | liveDataTarget: MutableLiveData, LyricsApiException>>, 68 | provider: LyricsProvider = this.defaultProvider 69 | ) { 70 | executor.submit { liveDataTarget.postValue(provider.search(query)) } 71 | } 72 | 73 | private fun fetchLyrics(songMeta: SongMeta, synced: Boolean = false): Result { 74 | Log.d(TAG, "fetchLyrics($songMeta, $synced)") 75 | var bestResult: SearchResult? = null 76 | var bestResultScore = 0.0 77 | 78 | for (provider in providers) { 79 | val search = provider.search(songMeta) 80 | if (search !is Success) continue 81 | 82 | val result = search.value.maxByOrNull { getResultScore(songMeta, it) } ?: continue 83 | val score = getResultScore(songMeta, result) 84 | 85 | if (score > bestResultScore) { 86 | bestResult = result 87 | bestResultScore = score 88 | } 89 | } 90 | 91 | return fetchLyrics(bestResult) 92 | } 93 | 94 | private fun fetchLyrics(searchResult: SearchResult?): Result { 95 | searchResult?.songWithLyrics?.let { 96 | Log.d(TAG, "Can skip fetch because song is present in search result.") 97 | return Success(it) 98 | } 99 | 100 | if (searchResult?.id == null) return Failure(LyricsNotFoundException()) 101 | return searchResult.provider.fetchLyrics(searchResult) 102 | } 103 | 104 | private fun getResultScore(songMeta: SongMeta, searchResult: SearchResult): Double { 105 | var score = 0.0 106 | 107 | if (songMeta.title == searchResult.title) { 108 | score += 0.5 109 | } else if (songMeta.title.startsWith(searchResult.title)) { 110 | score += 0.4 111 | } else if (searchResult.title.startsWith(songMeta.title)) { 112 | score += 0.3 113 | } 114 | 115 | if (songMeta.artist == null) { 116 | return score 117 | } else if (songMeta.artist == searchResult.artist) { 118 | score += 0.5 119 | } else if (songMeta.artist.startsWith(searchResult.artist)) { 120 | score += 0.4 121 | } else if (searchResult.artist.startsWith(songMeta.artist)) { 122 | score += 0.3 123 | } 124 | 125 | if (songMeta.album == searchResult.album) score += 0.5 126 | 127 | return score 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/api/provider/LyricsProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.api.provider 2 | 3 | import dev.forkhandles.result4k.Failure 4 | import dev.forkhandles.result4k.Result 5 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 6 | import io.github.teccheck.fastlyrics.exceptions.LyricsNotFoundException 7 | import io.github.teccheck.fastlyrics.model.SearchResult 8 | import io.github.teccheck.fastlyrics.model.SongMeta 9 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 10 | import java.io.Serializable 11 | 12 | interface LyricsProvider : Serializable { 13 | fun getName(): String 14 | 15 | fun search(searchQuery: String): Result, LyricsApiException> 16 | 17 | fun search(songMeta: SongMeta): Result, LyricsApiException> { 18 | var searchQuery = songMeta.title 19 | if (songMeta.artist != null) { 20 | searchQuery += " ${songMeta.artist}" 21 | } 22 | 23 | return search(searchQuery) 24 | } 25 | 26 | fun fetchLyrics(songId: Long): Result 27 | 28 | fun fetchLyrics(searchResult: SearchResult): Result { 29 | if (searchResult.id == null) return Failure(LyricsNotFoundException()) 30 | 31 | return fetchLyrics(searchResult.id) 32 | } 33 | 34 | companion object { 35 | fun getAllProviders(): Array = arrayOf(Deezer, Genius, LrcLib, PetitLyrics, Netease) 36 | 37 | fun getProviderByName(name: String) = getAllProviders().find { it.getName() == name } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/api/provider/Netease.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.api.provider 2 | 3 | import android.util.Log 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.JsonObject 6 | import dev.forkhandles.result4k.Failure 7 | import dev.forkhandles.result4k.Result 8 | import dev.forkhandles.result4k.Success 9 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 10 | import io.github.teccheck.fastlyrics.exceptions.LyricsNotFoundException 11 | import io.github.teccheck.fastlyrics.exceptions.NetworkException 12 | import io.github.teccheck.fastlyrics.model.LyricsType 13 | import io.github.teccheck.fastlyrics.model.SearchResult 14 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 15 | import retrofit2.Call 16 | import retrofit2.Retrofit 17 | import retrofit2.converter.gson.GsonConverterFactory 18 | import retrofit2.http.GET 19 | import retrofit2.http.Query 20 | 21 | object Netease : LyricsProvider { 22 | 23 | // TODO: Find out how to get a cover image 24 | 25 | private const val TAG = "Netease" 26 | 27 | private const val KEY_RESULT = "result" 28 | private const val KEY_SONGS = "songs" 29 | private const val KEY_ID = "id" 30 | private const val KEY_NAME = "name" 31 | private const val KEY_ALBUM = "album" 32 | private const val KEY_ARTISTS = "artists" 33 | private const val KEY_UNCOLLECTED = "uncollected" 34 | private const val KEY_LRC = "lrc" 35 | private const val KEY_LYRIC = "lyric" 36 | 37 | private val apiService: ApiService 38 | 39 | init { 40 | val gson = GsonBuilder().disableJdkUnsafe().create() 41 | 42 | val retrofit = Retrofit.Builder() 43 | .baseUrl("https://music.163.com/api/") 44 | .addConverterFactory(GsonConverterFactory.create(gson)) 45 | .build() 46 | 47 | apiService = retrofit.create(ApiService::class.java) 48 | } 49 | 50 | private fun readResolve(): Any = Netease 51 | 52 | override fun getName() = "netease" 53 | 54 | override fun search(searchQuery: String): Result, LyricsApiException> { 55 | try { 56 | val jsonBody = apiService.search(searchQuery).execute().body() 57 | val result = jsonBody?.get(KEY_RESULT)?.asJsonObject 58 | val songs = 59 | result?.get(KEY_SONGS)?.asJsonArray ?: return Failure(LyricsNotFoundException()) 60 | 61 | val results = songs 62 | .filter { !it.asJsonObject.has(KEY_UNCOLLECTED) } 63 | .map { song -> 64 | val songObject = song.asJsonObject 65 | 66 | val id = songObject.get(KEY_ID).asLong 67 | val title = songObject.get(KEY_NAME).asString 68 | val artists = songObject.get(KEY_ARTISTS).asJsonArray 69 | val artist = artists.first().asJsonObject.get(KEY_NAME).asString 70 | val album = songObject.get(KEY_ALBUM).asJsonObject.get(KEY_NAME).asString 71 | val url = "https://music.163.com/#/song?id=$id" 72 | 73 | SearchResult(title, artist, album, null, url, id, this, null) 74 | } 75 | 76 | return Success(results) 77 | } catch (e: Exception) { 78 | Log.e(TAG, e.message, e) 79 | return Failure(NetworkException()) 80 | } 81 | } 82 | 83 | override fun fetchLyrics(songId: Long): Result { 84 | TODO("Not yet implemented") 85 | } 86 | 87 | override fun fetchLyrics(searchResult: SearchResult): Result { 88 | try { 89 | val songId = searchResult.id ?: return Failure(LyricsNotFoundException()) 90 | 91 | val lyricsJson = 92 | apiService.query(songId).execute().body()?.asJsonObject ?: return Failure( 93 | NetworkException() 94 | ) 95 | 96 | val lyrics = lyricsJson.get(KEY_LRC).asJsonObject.get(KEY_LYRIC).asString 97 | 98 | return Success( 99 | SongWithLyrics( 100 | 0, 101 | searchResult.title, 102 | searchResult.artist, 103 | null, 104 | lyrics, 105 | searchResult.url!!, 106 | searchResult.album, 107 | null, 108 | null, 109 | LyricsType.LRC, 110 | getName() 111 | ) 112 | ) 113 | } catch (e: Exception) { 114 | Log.e(TAG, e.message, e) 115 | return Failure(NetworkException()) 116 | } 117 | } 118 | 119 | interface ApiService { 120 | @GET("search/get") 121 | fun search( 122 | @Query("s") query: String, 123 | @Query("limit") csRfToken: Int = 6, 124 | @Query("type") type: Int = 1, 125 | @Query("offset") offset: Int = 0, 126 | @Query("total") total: Boolean = true 127 | ): Call 128 | 129 | @GET("song/lyric") 130 | fun query( 131 | @Query("id") id: Long, 132 | @Query("lv") lv: Int = -1, 133 | @Query("kv") kv: Int = -1, 134 | @Query("tv") tv: Int = -1 135 | ): Call 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/api/storage/LyricsDatabase.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.api.storage 2 | 3 | import android.util.Log 4 | import androidx.room.AutoMigration 5 | import androidx.room.Database 6 | import androidx.room.RoomDatabase 7 | import androidx.room.migration.Migration 8 | import androidx.sqlite.db.SupportSQLiteDatabase 9 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 10 | 11 | @Database( 12 | entities = [SongWithLyrics::class], 13 | version = 5, 14 | exportSchema = true, 15 | autoMigrations = [AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3)] 16 | ) 17 | abstract class LyricsDatabase : RoomDatabase() { 18 | abstract fun songsDao(): SongsDao 19 | 20 | companion object { 21 | val MIGRATION_3_4 = object : Migration(3, 4) { 22 | override fun migrate(db: SupportSQLiteDatabase) { 23 | Log.d("Migration", "Start") 24 | db.execSQL("ALTER TABLE songs ADD COLUMN lyricsPlain TEXT") 25 | db.execSQL("ALTER TABLE songs ADD COLUMN lyricsSynced TEXT") 26 | db.execSQL("UPDATE songs SET lyricsPlain = lyrics") 27 | db.execSQL("UPDATE songs SET lyricsSynced = lyricsPlain WHERE type = 'LRC'") 28 | db.execSQL("UPDATE songs SET lyricsPlain = '' WHERE type = 'LRC'") 29 | db.execSQL("ALTER TABLE songs DROP COLUMN lyrics") 30 | Log.d("Migration", "End") 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/api/storage/SongsDao.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.api.storage 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import io.github.teccheck.fastlyrics.model.LyricsType 7 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 8 | 9 | @Dao 10 | interface SongsDao { 11 | 12 | @Query("SELECT * FROM songs") 13 | fun getAll(): List 14 | 15 | @Query("SELECT * FROM songs WHERE title = :title AND artist = :artist") 16 | fun findSong(title: String, artist: String): SongWithLyrics? 17 | 18 | @Query("SELECT * FROM songs WHERE title = :title AND artist = :artist AND type = :type") 19 | fun findSong(title: String, artist: String, type: LyricsType): SongWithLyrics? 20 | 21 | @Query("SELECT * FROM songs WHERE id = :id") 22 | fun getSong(id: Long): SongWithLyrics? 23 | 24 | @Insert 25 | fun insert(song: SongWithLyrics) 26 | 27 | @Query("DELETE FROM songs WHERE id in (:ids)") 28 | fun deleteAll(ids: List) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/GenericException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | class GenericException : LyricsApiException() 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/LyricsApiException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | abstract class LyricsApiException 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/LyricsNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | class LyricsNotFoundException : LyricsApiException() 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/NetworkException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | class NetworkException : LyricsApiException() 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/NoMusicPlayingException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | class NoMusicPlayingException : LyricsApiException() 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/NoNotifPermsException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | class NoNotifPermsException : LyricsApiException() 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/exceptions/ParseException.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.exceptions 2 | 3 | class ParseException : LyricsApiException() 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/model/LyricsType.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.model 2 | 3 | enum class LyricsType { 4 | RAW_TEXT, 5 | LRC 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/model/SearchResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.model 2 | 3 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 4 | import java.io.Serializable 5 | 6 | data class SearchResult( 7 | val title: String, 8 | val artist: String, 9 | val album: String?, 10 | val artUrl: String?, 11 | val url: String?, 12 | val id: Long?, 13 | val provider: LyricsProvider, 14 | val songWithLyrics: SongWithLyrics? = null 15 | ) : Serializable { 16 | override fun toString(): String = 17 | "SearchResult(title='$title', artist='$artist', album=$album, artUrl=$artUrl, url=$url, id=$id, provider=$provider, songWithLyrics=$songWithLyrics)" 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/model/SongMeta.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.model 2 | 3 | import android.graphics.Bitmap 4 | 5 | data class SongMeta( 6 | val title: String, 7 | val artist: String? = null, 8 | val album: String? = null, 9 | val art: Bitmap? = null, 10 | val duration: Long? = null 11 | ) { 12 | override fun toString(): String = 13 | "SongMeta(title='$title', artist=$artist, album=$album, art=$art, duration=$duration)" 14 | 15 | override fun equals(other: Any?): Boolean { 16 | if (this === other) return true 17 | if (javaClass != other?.javaClass) return false 18 | 19 | other as SongMeta 20 | 21 | if (title != other.title) return false 22 | if (artist != other.artist) return false 23 | if (album != other.album) return false 24 | // Art is left out 25 | if (duration != other.duration) return false 26 | 27 | return true 28 | } 29 | 30 | override fun hashCode(): Int { 31 | var result = title.hashCode() 32 | result = 31 * result + (artist?.hashCode() ?: 0) 33 | result = 31 * result + (album?.hashCode() ?: 0) 34 | result = 31 * result + (art?.hashCode() ?: 0) 35 | result = 31 * result + (duration?.hashCode() ?: 0) 36 | return result 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/model/SongWithLyrics.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import java.io.Serializable 7 | 8 | @Entity(tableName = "songs") 9 | data class SongWithLyrics( 10 | @PrimaryKey(autoGenerate = true) 11 | val id: Long, 12 | val title: String, 13 | val artist: String, 14 | val lyricsPlain: String?, 15 | val lyricsSynced: String?, 16 | val sourceUrl: String, 17 | val album: String?, 18 | val artUrl: String?, 19 | val duration: Long?, 20 | 21 | @Deprecated("Because there can be both synced and plain lyrics, this is not need.") 22 | @ColumnInfo(defaultValue = "RAW_TEXT") 23 | val type: LyricsType, 24 | 25 | @ColumnInfo(defaultValue = "genius") 26 | val provider: String 27 | ) : Serializable { 28 | override fun toString(): String = 29 | "SongWithLyrics(id=$id, title='$title', artist='$artist', lyricsPlain=$lyricsPlain, lyricsSynced=$lyricsSynced, sourceUrl='$sourceUrl', album=$album, artUrl=$artUrl, type=$type, provider='$provider')" 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/model/SyncedLyrics.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.model 2 | 3 | import android.text.format.DateUtils 4 | import com.dirror.lyricviewx.LyricEntry 5 | import java.util.regex.Pattern 6 | 7 | object SyncedLyrics { 8 | 9 | private val PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)") 10 | private val PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]") 11 | 12 | fun parseLrcToList(text: String): List { 13 | if (text.isEmpty()) return emptyList() 14 | 15 | return text.split("\\n".toRegex()).dropLastWhile { it.isEmpty() } 16 | .map { parseLrcLine(it.trim()) }.flatten().sortedBy { it.first } 17 | .map { LyricEntry(it.first, it.second) } 18 | } 19 | 20 | private fun parseLrcLine(line: String): List> { 21 | val entries: MutableList> = ArrayList() 22 | 23 | val lineMatcher = PATTERN_LINE.matcher(line) 24 | if (!lineMatcher.matches()) return entries 25 | 26 | val times = lineMatcher.group(1) ?: return entries 27 | val text = lineMatcher.group(3) ?: return entries 28 | 29 | val timeMatcher = PATTERN_TIME.matcher(times) ?: return entries 30 | while (timeMatcher.find()) { 31 | val min = timeMatcher.group(1)!!.toLong() 32 | val sec = timeMatcher.group(2)!!.toLong() 33 | 34 | val milString = timeMatcher.group(3)!! 35 | var mil = milString.toLong() 36 | if (milString.length == 2) mil *= 10 37 | 38 | val time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil 39 | entries.add(Pair(time, text)) 40 | } 41 | 42 | return entries 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/service/DummyNotificationListenerService.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.service 2 | 3 | import android.content.Context 4 | import android.service.notification.NotificationListenerService 5 | import androidx.core.app.NotificationManagerCompat 6 | 7 | /* Dummy class needed for access to media information */ 8 | class DummyNotificationListenerService : NotificationListenerService() { 9 | 10 | companion object { 11 | fun canAccessNotifications(context: Context): Boolean = 12 | NotificationManagerCompat.getEnabledListenerPackages(context) 13 | .contains(context.packageName) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/about/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.about 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.annotation.StringRes 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import io.github.teccheck.fastlyrics.BaseActivity 9 | import io.github.teccheck.fastlyrics.BuildConfig 10 | import io.github.teccheck.fastlyrics.R 11 | import io.github.teccheck.fastlyrics.databinding.ActivityAboutBinding 12 | 13 | class AboutActivity : BaseActivity() { 14 | 15 | private lateinit var binding: ActivityAboutBinding 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | binding = ActivityAboutBinding.inflate(layoutInflater) 21 | setContentView(binding.root) 22 | setupToolbar(binding.toolbarLayout.toolbar, R.string.menu_about) 23 | 24 | binding.textVersion.text = BuildConfig.VERSION_NAME 25 | binding.layoutSourceCode.setOnClickListener { openUrl(R.string.source_code_url) } 26 | binding.recycler.adapter = RecyclerAdapter(this::openUrl) 27 | binding.recycler.layoutManager = LinearLayoutManager(this) 28 | } 29 | 30 | private fun openUrl(@StringRes urlRes: Int?) { 31 | if (urlRes == null) return 32 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(urlRes)))) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/about/RecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.about 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.annotation.StringRes 9 | import androidx.recyclerview.widget.RecyclerView 10 | import io.github.teccheck.fastlyrics.R 11 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 12 | import io.github.teccheck.fastlyrics.utils.Utils 13 | 14 | class RecyclerAdapter(private val urlOpener: UrlOpener) : RecyclerView.Adapter() { 15 | 16 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 17 | private val icon: ImageView = view.findViewById(R.id.icon) 18 | private val text: TextView = view.findViewById(R.id.text) 19 | 20 | fun bind(provider: LyricsProvider, linkOpener: UrlOpener) { 21 | icon.setImageResource(Utils.getProviderIconRes(provider)) 22 | text.setText(Utils.getProviderNameRes(provider)) 23 | itemView.setOnClickListener { linkOpener.openUrl(Utils.getProviderUrlRes(provider)) } 24 | } 25 | } 26 | 27 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 28 | val view = LayoutInflater.from(viewGroup.context) 29 | .inflate(R.layout.list_item_provider, viewGroup, false) 30 | return ViewHolder(view) 31 | } 32 | 33 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 34 | viewHolder.bind(LyricsProvider.getAllProviders()[position], urlOpener) 35 | } 36 | 37 | override fun getItemCount() = LyricsProvider.getAllProviders().size 38 | 39 | fun interface UrlOpener { 40 | fun openUrl(@StringRes linkRes: Int?) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/fastlyrics/FastLyricsViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.fastlyrics 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import dev.forkhandles.result4k.Result 9 | import dev.forkhandles.result4k.Success 10 | import io.github.teccheck.fastlyrics.api.LyricsApi 11 | import io.github.teccheck.fastlyrics.api.MediaSession 12 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 13 | import io.github.teccheck.fastlyrics.model.SongMeta 14 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 15 | import io.github.teccheck.fastlyrics.service.DummyNotificationListenerService 16 | import io.github.teccheck.fastlyrics.utils.DebouncedMutableLiveData 17 | import java.util.Timer 18 | import kotlin.concurrent.scheduleAtFixedRate 19 | 20 | class FastLyricsViewModel : ViewModel() { 21 | 22 | private val _songMeta = DebouncedMutableLiveData>() 23 | private val _songWithLyrics = MutableLiveData>() 24 | private val _songPosition = MutableLiveData() 25 | 26 | private var songPositionTimer: Timer? = null 27 | 28 | val songMeta: LiveData> = _songMeta 29 | val songWithLyrics: LiveData> = _songWithLyrics 30 | val songPosition: LiveData = _songPosition 31 | 32 | var autoRefresh = false 33 | var state: UiState = StartupState() 34 | 35 | private val songMetaCallback = MediaSession.SongMetaCallback { 36 | if (!autoRefresh) return@SongMetaCallback 37 | 38 | _songMeta.postValue(Success(it)) 39 | loadLyrics(it) 40 | } 41 | 42 | fun loadLyricsForCurrentSong(context: Context): Boolean { 43 | if (!DummyNotificationListenerService.canAccessNotifications(context)) { 44 | Log.w(TAG, "Can't access notifications") 45 | return false 46 | } 47 | 48 | val songMetaResult = MediaSession.getSongInformation() 49 | _songMeta.setValue(songMetaResult) 50 | 51 | if (songMetaResult is Success) { 52 | loadLyrics(songMetaResult.value) 53 | } 54 | 55 | return songMetaResult is Success 56 | } 57 | 58 | private fun loadLyrics(songMeta: SongMeta) { 59 | LyricsApi.getLyricsAsync(songMeta, _songWithLyrics, false) 60 | } 61 | 62 | fun setupSongMetaListener() { 63 | MediaSession.registerSongMetaCallback(songMetaCallback) 64 | } 65 | 66 | fun setupPositionPolling(enabled: Boolean) { 67 | if (enabled) { 68 | val timer = Timer() 69 | timer.scheduleAtFixedRate(REFRESH_DELAY, REFRESH_DELAY) { 70 | _songPosition.postValue(MediaSession.getSongPosition()) 71 | } 72 | 73 | songPositionTimer = timer 74 | } else { 75 | songPositionTimer?.cancel() 76 | songPositionTimer = null 77 | } 78 | } 79 | 80 | override fun onCleared() { 81 | super.onCleared() 82 | MediaSession.unregisterSongMetaCallback(songMetaCallback) 83 | } 84 | 85 | companion object { 86 | private const val TAG = "FastLyricsViewModel" 87 | private const val REFRESH_DELAY = 500L 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/fastlyrics/States.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.fastlyrics 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.DrawableRes 5 | import androidx.annotation.StringRes 6 | import io.github.teccheck.fastlyrics.R 7 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 8 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 9 | import io.github.teccheck.fastlyrics.model.SongMeta 10 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 11 | import io.github.teccheck.fastlyrics.utils.Utils 12 | import io.github.teccheck.fastlyrics.utils.Utils.getLyrics 13 | 14 | open class UiState( 15 | val showHeader: Boolean, 16 | val showError: Boolean, 17 | val showText: Boolean, 18 | val isRefreshing: Boolean = false, 19 | val startRefresh: Boolean = false 20 | ) { 21 | // Header 22 | open fun getSongTitle(): String = "" 23 | open fun getSongArtist(): String = "" 24 | open fun getArtBitmap(): Bitmap? = null 25 | open fun getArtUrl(): String? = null 26 | 27 | // Lyrics 28 | open fun getSongProvider(): LyricsProvider? = null 29 | open fun getSourceUrl(): String = "" 30 | open fun getLyrics(): String = "" 31 | open fun getSyncedLyrics(): String? = null 32 | fun hasSyncedLyrics() = getSyncedLyrics() != null 33 | 34 | // Error 35 | @StringRes 36 | open fun getErrorText(): Int? = null 37 | 38 | @DrawableRes 39 | open fun getErrorIcon(): Int? = null 40 | } 41 | 42 | class StartupState : UiState(false, false, false, true, true) 43 | 44 | class NoMusicState : UiState(false, true, false) { 45 | override fun getErrorIcon() = R.drawable.outline_music_off_24 46 | override fun getErrorText() = R.string.no_song_playing 47 | } 48 | 49 | class ErrorState(private val songMeta: SongMeta?, private val exception: LyricsApiException) : 50 | UiState(true, true, false) { 51 | override fun getErrorIcon() = Utils.getErrorIconRes(exception) 52 | override fun getErrorText() = Utils.getErrorTextRes(exception) 53 | 54 | override fun getSongTitle() = songMeta?.title ?: "" 55 | override fun getSongArtist() = songMeta?.artist ?: "" 56 | override fun getArtBitmap() = songMeta?.art 57 | } 58 | 59 | class LoadingState(private val songMeta: SongMeta) : UiState(true, false, false, true) { 60 | override fun getSongTitle() = songMeta.title 61 | override fun getSongArtist() = songMeta.artist ?: "" 62 | override fun getArtBitmap() = songMeta.art 63 | } 64 | 65 | class TextState(private val songMeta: SongMeta?, private val songWithLyrics: SongWithLyrics) : 66 | UiState(true, false, true) { 67 | override fun getSongTitle() = songWithLyrics.title 68 | override fun getSongArtist() = songWithLyrics.artist 69 | override fun getArtUrl() = songWithLyrics.artUrl 70 | override fun getArtBitmap() = songMeta?.art 71 | 72 | override fun getSongProvider() = LyricsProvider.getProviderByName(songWithLyrics.provider) 73 | override fun getSourceUrl() = songWithLyrics.sourceUrl 74 | override fun getLyrics() = songWithLyrics.getLyrics(false) 75 | override fun getSyncedLyrics() = songWithLyrics.lyricsSynced 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/permission/PermissionActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.permission 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import io.github.teccheck.fastlyrics.BaseActivity 6 | import io.github.teccheck.fastlyrics.R 7 | import io.github.teccheck.fastlyrics.api.MediaSession 8 | import io.github.teccheck.fastlyrics.databinding.ActivityPermissionBinding 9 | 10 | class PermissionActivity : BaseActivity() { 11 | 12 | private lateinit var binding: ActivityPermissionBinding 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | binding = ActivityPermissionBinding.inflate(layoutInflater) 18 | setContentView(binding.root) 19 | setupToolbar(binding.toolbarLayout.toolbar, R.string.menu_permission) 20 | 21 | binding.gotoSettingsButton.setOnClickListener { startNotificationsSettings() } 22 | } 23 | 24 | override fun onResume() { 25 | super.onResume() 26 | MediaSession.init(this) 27 | } 28 | 29 | private fun startNotificationsSettings() { 30 | val intent = Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS") 31 | startActivity(intent) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/saved/DetailsLookup.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.saved 2 | 3 | import android.view.MotionEvent 4 | import androidx.recyclerview.selection.ItemDetailsLookup 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class DetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { 8 | override fun getItemDetails(e: MotionEvent): ItemDetails? { 9 | val view = recyclerView.findChildViewUnder(e.x, e.y) ?: return null 10 | val viewHolder = recyclerView.findContainingViewHolder(view) as RecyclerAdapter.ViewHolder? 11 | ?: return null 12 | 13 | return SongWithLyricsDetails( 14 | viewHolder.adapterPosition, 15 | viewHolder.itemId, 16 | viewHolder.getSongId() 17 | ) 18 | } 19 | 20 | class SongWithLyricsDetails(private val position: Int, private val itemId: Long?, val songId: Long?) : 21 | ItemDetails() { 22 | 23 | override fun getPosition() = position 24 | 25 | override fun getSelectionKey() = itemId 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/saved/RecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.saved 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.recyclerview.selection.SelectionTracker 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.squareup.picasso.Picasso 12 | import io.github.teccheck.fastlyrics.R 13 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 14 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 15 | import io.github.teccheck.fastlyrics.utils.PlaceholderDrawable 16 | import io.github.teccheck.fastlyrics.utils.Utils 17 | 18 | class RecyclerAdapter : RecyclerView.Adapter() { 19 | 20 | private var songs: List = listOf() 21 | private var selectionTracker: SelectionTracker? = null 22 | 23 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 24 | private val imageArt: ImageView = view.findViewById(R.id.image_song_art) 25 | private val textTitle: TextView = view.findViewById(R.id.text_song_title) 26 | private val textArtist: TextView = view.findViewById(R.id.text_song_artist) 27 | private val iconProvider: ImageView = view.findViewById(R.id.provider_icon) 28 | private val selectionIcon: ImageView = view.findViewById(R.id.selection_icon) 29 | 30 | private var song: SongWithLyrics? = null 31 | 32 | fun bind(song: SongWithLyrics, selected: Boolean) { 33 | this.song = song 34 | textTitle.text = song.title 35 | textArtist.text = song.artist 36 | 37 | val picasso = Picasso.get().load(song.artUrl) 38 | 39 | LyricsProvider.getProviderByName(song.provider)?.let { provider -> 40 | Utils.getProviderIconRes(provider).let { 41 | iconProvider.setImageResource(it) 42 | picasso.placeholder(PlaceholderDrawable(imageArt.context, it)) 43 | } 44 | } 45 | 46 | picasso.into(imageArt) 47 | selectionIcon.visibility = if (selected) View.VISIBLE else View.GONE 48 | } 49 | 50 | fun getSongId() = song?.id 51 | } 52 | 53 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 54 | val view = LayoutInflater.from(viewGroup.context) 55 | .inflate(R.layout.list_item_song, viewGroup, false) 56 | 57 | return ViewHolder(view) 58 | } 59 | 60 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 61 | val song = songs[position] 62 | val selected = selectionTracker?.isSelected(position.toLong()) ?: false 63 | viewHolder.bind(song, selected) 64 | } 65 | 66 | override fun getItemCount() = songs.size 67 | 68 | override fun getItemId(position: Int) = position.toLong() 69 | 70 | @SuppressLint("NotifyDataSetChanged") 71 | fun setSongs(songs: List) { 72 | this.songs = songs 73 | notifyDataSetChanged() 74 | } 75 | 76 | fun setSelectionTracker(selectionTracker: SelectionTracker) { 77 | this.selectionTracker = selectionTracker 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/saved/SavedViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.saved 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import dev.forkhandles.result4k.Result 7 | import io.github.teccheck.fastlyrics.api.LyricStorage 8 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 9 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 10 | 11 | class SavedViewModel : ViewModel() { 12 | private val _songs = MutableLiveData, LyricsApiException>>() 13 | val songs: LiveData, LyricsApiException>> = _songs 14 | 15 | fun fetchSongs() { 16 | LyricStorage.getSongsAsync(_songs) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/search/DetailsLookup.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.search 2 | 3 | import android.view.MotionEvent 4 | import androidx.recyclerview.selection.ItemDetailsLookup 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class DetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { 8 | override fun getItemDetails(e: MotionEvent): ItemDetails? { 9 | val view = recyclerView.findChildViewUnder(e.x, e.y) ?: return null 10 | val viewHolder = recyclerView.findContainingViewHolder(view) as RecyclerAdapter.ViewHolder? 11 | ?: return null 12 | 13 | return SearchResultDetails(viewHolder.adapterPosition, viewHolder.itemId) 14 | } 15 | 16 | class SearchResultDetails(private val position: Int, private val itemId: Long?) : ItemDetails() { 17 | 18 | override fun getPosition() = position 19 | 20 | override fun getSelectionKey() = itemId 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/search/RecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.search 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.squareup.picasso.Picasso 11 | import io.github.teccheck.fastlyrics.R 12 | import io.github.teccheck.fastlyrics.model.SearchResult 13 | import io.github.teccheck.fastlyrics.utils.Utils 14 | 15 | class RecyclerAdapter : RecyclerView.Adapter() { 16 | 17 | private var searchResults: List = listOf() 18 | 19 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 20 | private val imageArt: ImageView = view.findViewById(R.id.image_song_art) 21 | private val textTitle: TextView = view.findViewById(R.id.text_song_title) 22 | private val textArtist: TextView = view.findViewById(R.id.text_song_artist) 23 | private val providerIcon: ImageView = view.findViewById(R.id.provider_icon) 24 | 25 | private var searchResult: SearchResult? = null 26 | 27 | fun bind(searchResult: SearchResult) { 28 | this.searchResult = searchResult 29 | textTitle.text = searchResult.title 30 | textArtist.text = searchResult.artist 31 | Picasso.get().load(searchResult.artUrl).into(imageArt) 32 | providerIcon.setImageResource(Utils.getProviderIconRes(searchResult.provider)) 33 | } 34 | 35 | fun getSearchResult() = searchResult 36 | } 37 | 38 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 39 | val view = LayoutInflater.from(viewGroup.context) 40 | .inflate(R.layout.list_item_song, viewGroup, false) 41 | 42 | return ViewHolder(view) 43 | } 44 | 45 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 46 | val searchResult = searchResults[position] 47 | viewHolder.bind(searchResult) 48 | } 49 | 50 | override fun getItemCount() = searchResults.size 51 | 52 | override fun getItemId(position: Int) = position.toLong() 53 | 54 | @SuppressLint("NotifyDataSetChanged") 55 | fun setSearchResults(searchResults: List) { 56 | this.searchResults = searchResults 57 | notifyDataSetChanged() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/search/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.search 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.MenuItem 8 | import android.view.MotionEvent 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.appcompat.widget.SearchView 12 | import androidx.fragment.app.Fragment 13 | import androidx.lifecycle.ViewModelProvider 14 | import androidx.navigation.fragment.findNavController 15 | import androidx.recyclerview.selection.ItemDetailsLookup 16 | import androidx.recyclerview.selection.SelectionTracker 17 | import androidx.recyclerview.selection.StableIdKeyProvider 18 | import androidx.recyclerview.selection.StorageStrategy 19 | import androidx.recyclerview.widget.LinearLayoutManager 20 | import dev.forkhandles.result4k.Success 21 | import io.github.teccheck.fastlyrics.MainActivity 22 | import io.github.teccheck.fastlyrics.databinding.FragmentSearchBinding 23 | import io.github.teccheck.fastlyrics.model.SearchResult 24 | import io.github.teccheck.fastlyrics.ui.viewlyrics.ViewLyricsActivity 25 | 26 | class SearchFragment : Fragment() { 27 | 28 | private var _binding: FragmentSearchBinding? = null 29 | 30 | // This property is only valid between onCreateView and onDestroyView. 31 | private val binding get() = _binding!! 32 | 33 | private val searchTimer = SearchTimer(this::onQueryTextSubmit) 34 | 35 | private lateinit var viewModel: SearchViewModel 36 | private lateinit var recyclerAdapter: RecyclerAdapter 37 | private lateinit var selectionTracker: SelectionTracker 38 | 39 | private var searchMenuItem: MenuItem? = null 40 | private var searchView: SearchView? = null 41 | 42 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 43 | _binding = FragmentSearchBinding.inflate(inflater, container, false) 44 | viewModel = ViewModelProvider(this)[SearchViewModel::class.java] 45 | 46 | searchMenuItem = (activity as MainActivity).getSearchMenuItem() 47 | searchMenuItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { 48 | override fun onMenuItemActionExpand(item: MenuItem): Boolean = true 49 | 50 | override fun onMenuItemActionCollapse(item: MenuItem): Boolean { 51 | findNavController().navigateUp() 52 | return true 53 | } 54 | }) 55 | 56 | searchView = (searchMenuItem?.actionView as SearchView?) 57 | searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 58 | override fun onQueryTextSubmit(query: String?): Boolean = this@SearchFragment.onQueryTextSubmit(query) 59 | 60 | override fun onQueryTextChange(newText: String?): Boolean = this@SearchFragment.onQueryTextChange(newText) 61 | }) 62 | 63 | recyclerAdapter = RecyclerAdapter() 64 | recyclerAdapter.setHasStableIds(true) 65 | binding.recyclerView.adapter = recyclerAdapter 66 | binding.recyclerView.layoutManager = LinearLayoutManager(context) 67 | 68 | selectionTracker = SelectionTracker.Builder( 69 | SELECTION_ID, 70 | binding.recyclerView, 71 | StableIdKeyProvider(binding.recyclerView), 72 | DetailsLookup(binding.recyclerView), 73 | StorageStrategy.createLongStorage() 74 | ).withOnItemActivatedListener(this::onItemActivated) 75 | .withSelectionPredicate(SelectionPredicate()).build() 76 | 77 | viewModel.searchResults.observe(viewLifecycleOwner) { searchResults -> 78 | binding.progressIndicator.visibility = View.GONE 79 | if (searchResults is Success) recyclerAdapter.setSearchResults(searchResults.value) 80 | } 81 | 82 | return binding.root 83 | } 84 | 85 | override fun onDestroyView() { 86 | super.onDestroyView() 87 | _binding = null 88 | searchView?.setOnQueryTextListener(null) 89 | searchMenuItem?.setOnActionExpandListener(null) 90 | } 91 | 92 | private fun onQueryTextSubmit(query: String?): Boolean { 93 | if (query?.isBlank() != false) { 94 | return true 95 | } 96 | 97 | binding.progressIndicator.visibility = View.VISIBLE 98 | viewModel.search(query) 99 | return true 100 | } 101 | 102 | private fun onQueryTextChange(newText: String?): Boolean { 103 | searchTimer.setQuery(newText) 104 | return true 105 | } 106 | 107 | private fun onItemActivated(item: ItemDetailsLookup.ItemDetails, e: MotionEvent): Boolean { 108 | val viewHolder = item.selectionKey?.let { binding.recyclerView.findViewHolderForItemId(it) } 109 | val searchResult = (viewHolder as RecyclerAdapter.ViewHolder).getSearchResult() 110 | searchResult?.let { viewSearchResult(it) } 111 | return false 112 | } 113 | 114 | private fun viewSearchResult(searchResult: SearchResult) { 115 | Log.d(TAG, "Show search result $searchResult") 116 | val intent = Intent(requireContext(), ViewLyricsActivity::class.java) 117 | intent.putExtra(ViewLyricsActivity.ARG_SEARCH_RESULT, searchResult) 118 | startActivity(intent) 119 | 120 | searchMenuItem?.setOnActionExpandListener(null) 121 | searchMenuItem?.collapseActionView() 122 | } 123 | 124 | companion object { 125 | private const val TAG = "SearchFragment" 126 | private const val SELECTION_ID = "search" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/search/SearchTimer.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.search 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | 6 | class SearchTimer(private val queryDispatcher: QueryDispatcher) { 7 | private val handler = Handler(Looper.getMainLooper()) 8 | private var query: String? = null 9 | private var timerRunning = false 10 | 11 | fun setQuery(query: String?) { 12 | this.query = query 13 | if (!timerRunning && query != null) { 14 | timerRunning = true 15 | handler.postDelayed(this::dispatchQuery, TIMER_DELAY) 16 | } 17 | } 18 | 19 | private fun dispatchQuery() { 20 | timerRunning = false 21 | query?.let { queryDispatcher.dispatch(it) } 22 | } 23 | 24 | fun interface QueryDispatcher { 25 | fun dispatch(query: String) 26 | } 27 | 28 | companion object { 29 | private const val TIMER_DELAY = 800.toLong() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.search 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import dev.forkhandles.result4k.Result 7 | import io.github.teccheck.fastlyrics.api.LyricsApi 8 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 9 | import io.github.teccheck.fastlyrics.model.SearchResult 10 | 11 | class SearchViewModel : ViewModel() { 12 | private val _searchResults = MutableLiveData, LyricsApiException>>() 13 | val searchResults: LiveData, LyricsApiException>> = _searchResults 14 | 15 | fun search(query: String) { 16 | LyricsApi.search(query, _searchResults) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/search/SelectionPredicate.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.search 2 | 3 | import androidx.recyclerview.selection.SelectionTracker 4 | 5 | class SelectionPredicate : SelectionTracker.SelectionPredicate() { 6 | override fun canSetStateForKey(key: Long, nextState: Boolean): Boolean = false 7 | 8 | override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean = false 9 | 10 | override fun canSelectMultiple(): Boolean = false 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/settings/ProviderRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.settings 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.appcompat.widget.SwitchCompat 9 | import androidx.recyclerview.widget.RecyclerView 10 | import io.github.teccheck.fastlyrics.R 11 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 12 | import io.github.teccheck.fastlyrics.utils.ProviderOrder 13 | import io.github.teccheck.fastlyrics.utils.Utils 14 | 15 | class ProviderRecyclerAdapter : RecyclerView.Adapter() { 16 | 17 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 18 | private val icon: ImageView = view.findViewById(R.id.icon) 19 | private val text: TextView = view.findViewById(R.id.text) 20 | private val switch: SwitchCompat = view.findViewById(R.id.provider_switch) 21 | 22 | private var provider: LyricsProvider? = null 23 | 24 | fun bind(provider: LyricsProvider) { 25 | this.provider = provider 26 | 27 | icon.setImageResource(Utils.getProviderIconRes(provider)) 28 | text.setText(Utils.getProviderNameRes(provider)) 29 | switch.isChecked = ProviderOrder.getEnabled(provider) 30 | switch.setOnCheckedChangeListener { _, enabled -> 31 | ProviderOrder.setEnabled( 32 | provider, 33 | enabled 34 | ) 35 | } 36 | } 37 | } 38 | 39 | fun interface ToggleListener { 40 | fun toggle(lyricsProvider: LyricsProvider, enabled: Boolean) 41 | } 42 | 43 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 44 | val view = LayoutInflater.from(parent.context) 45 | .inflate(R.layout.list_item_provider_setting, parent, false) 46 | 47 | return ViewHolder(view) 48 | } 49 | 50 | override fun getItemCount(): Int = ProviderOrder.getOrderCount() 51 | 52 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 53 | holder.bind(ProviderOrder.getProvider(position)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/settings/ProviderSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.settings 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.ItemTouchHelper 9 | import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import androidx.recyclerview.widget.RecyclerView 12 | import io.github.teccheck.fastlyrics.Settings 13 | import io.github.teccheck.fastlyrics.databinding.FragmentProviderSettingsBinding 14 | import io.github.teccheck.fastlyrics.utils.ProviderOrder 15 | 16 | class ProviderSettingsFragment : Fragment() { 17 | 18 | private var _binding: FragmentProviderSettingsBinding? = null 19 | 20 | // This property is only valid between onCreateView and onDestroyView. 21 | private val binding get() = _binding!! 22 | 23 | private lateinit var settings: Settings 24 | 25 | private val callback = object : SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { 26 | override fun onMove( 27 | recyclerView: RecyclerView, 28 | viewHolder: RecyclerView.ViewHolder, 29 | target: RecyclerView.ViewHolder 30 | ): Boolean { 31 | val adapter = recyclerView.adapter 32 | val from = viewHolder.adapterPosition 33 | val to = target.adapterPosition 34 | 35 | ProviderOrder.swap(from, to) 36 | adapter?.notifyItemMoved(from, to) 37 | 38 | return true 39 | } 40 | 41 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} 42 | } 43 | 44 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 45 | _binding = FragmentProviderSettingsBinding.inflate(inflater, container, false) 46 | settings = Settings(requireContext()) 47 | 48 | binding.recycler.adapter = ProviderRecyclerAdapter() 49 | binding.recycler.layoutManager = LinearLayoutManager(requireContext()) 50 | 51 | val helper = ItemTouchHelper(callback) 52 | helper.attachToRecyclerView(binding.recycler) 53 | 54 | return binding.root 55 | } 56 | 57 | override fun onPause() { 58 | super.onPause() 59 | ProviderOrder.save() 60 | } 61 | 62 | companion object { 63 | private val TAG = Companion::class.java.name 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/settings/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.settings 2 | 3 | import android.os.Bundle 4 | import io.github.teccheck.fastlyrics.BaseActivity 5 | import io.github.teccheck.fastlyrics.R 6 | import io.github.teccheck.fastlyrics.databinding.ActivitySettingsBinding 7 | 8 | class SettingsActivity : BaseActivity() { 9 | 10 | private lateinit var binding: ActivitySettingsBinding 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | binding = ActivitySettingsBinding.inflate(layoutInflater) 16 | setContentView(binding.root) 17 | setupToolbar(binding.toolbarLayout.toolbar, R.string.menu_settings) 18 | 19 | if (savedInstanceState == null) { 20 | supportFragmentManager 21 | .beginTransaction() 22 | .replace(R.id.settings_fragment, SettingsFragment()) 23 | .commit() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/settings/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.settings 2 | 3 | import android.os.Bundle 4 | import androidx.preference.PreferenceFragmentCompat 5 | import io.github.teccheck.fastlyrics.R 6 | 7 | class SettingsFragment : PreferenceFragmentCompat() { 8 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 9 | setPreferencesFromResource(R.xml.root_preferences, rootKey) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/viewlyrics/ViewLyricsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.viewlyrics 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.View 7 | import androidx.lifecycle.ViewModelProvider 8 | import com.squareup.picasso.Picasso 9 | import dev.forkhandles.result4k.Success 10 | import io.github.teccheck.fastlyrics.BaseActivity 11 | import io.github.teccheck.fastlyrics.R 12 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 13 | import io.github.teccheck.fastlyrics.databinding.ActivityViewLyricsBinding 14 | import io.github.teccheck.fastlyrics.model.SearchResult 15 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 16 | import io.github.teccheck.fastlyrics.utils.PlaceholderDrawable 17 | import io.github.teccheck.fastlyrics.utils.Utils 18 | import io.github.teccheck.fastlyrics.utils.Utils.copyToClipboard 19 | import io.github.teccheck.fastlyrics.utils.Utils.getLyrics 20 | import io.github.teccheck.fastlyrics.utils.Utils.openLink 21 | import io.github.teccheck.fastlyrics.utils.Utils.share 22 | 23 | class ViewLyricsActivity : BaseActivity() { 24 | 25 | private lateinit var binding: ActivityViewLyricsBinding 26 | private lateinit var lyricsViewModel: ViewLyricsViewModel 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | 31 | binding = ActivityViewLyricsBinding.inflate(layoutInflater) 32 | setContentView(binding.root) 33 | setupToolbar(binding.toolbarLayout.toolbar) 34 | 35 | binding.lyricsView.lyricViewX.visibility = View.GONE 36 | binding.refresher.isEnabled = false 37 | binding.refresher.setColorSchemeResources(R.color.theme_primary, R.color.theme_secondary) 38 | 39 | lyricsViewModel = ViewModelProvider(this)[ViewLyricsViewModel::class.java] 40 | 41 | lyricsViewModel.songWithLyrics.observe(this) { result -> 42 | if (result is Success) displaySongWithLyrics(result.value) 43 | } 44 | 45 | if (intent.hasExtra(ARG_SONG_ID)) { 46 | lyricsViewModel.loadLyricsForSongFromStorage(intent.getLongExtra(ARG_SONG_ID, 0)) 47 | return 48 | } 49 | 50 | if (intent.hasExtra(ARG_SEARCH_RESULT)) { 51 | binding.refresher.isRefreshing = true 52 | val result = getSearchResult(intent) ?: return 53 | lyricsViewModel.loadLyricsForSearchResult(result) 54 | } 55 | } 56 | 57 | private fun displaySongWithLyrics(song: SongWithLyrics) { 58 | binding.header.textSongTitle.text = song.title 59 | binding.header.textSongArtist.text = song.artist 60 | binding.lyricsView.textLyrics.text = song.getLyrics() 61 | 62 | val picasso = Picasso.get().load(song.artUrl) 63 | 64 | LyricsProvider.getProviderByName(song.provider)?.let { 65 | val nameRes = Utils.getProviderNameRes(it) 66 | val providerIconRes = Utils.getProviderIconRes(it) 67 | 68 | picasso.placeholder(PlaceholderDrawable(this, providerIconRes)) 69 | 70 | binding.lyricsView.source.setText(nameRes) 71 | binding.lyricsView.source.setIconResource(providerIconRes) 72 | 73 | binding.lyricsView.textLyricsProvider.setText(nameRes) 74 | binding.lyricsView.textLyricsProvider.setCompoundDrawablesRelativeWithIntrinsicBounds( 75 | providerIconRes, 76 | 0, 77 | 0, 78 | 0 79 | ) 80 | } 81 | 82 | picasso.into(binding.header.imageSongArt) 83 | 84 | binding.lyricsView.source.setOnClickListener { 85 | openLink(this@ViewLyricsActivity, song.sourceUrl) 86 | } 87 | binding.lyricsView.copy.setOnClickListener { 88 | copyToClipboard( 89 | this@ViewLyricsActivity, 90 | getString(R.string.lyrics_clipboard_label), 91 | song.getLyrics() 92 | ) 93 | } 94 | binding.lyricsView.share.setOnClickListener { 95 | share(this@ViewLyricsActivity, song.title, song.artist, song.getLyrics()) 96 | } 97 | } 98 | 99 | private fun getSearchResult(intent: Intent): SearchResult? = 100 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 101 | intent.getSerializableExtra(ARG_SEARCH_RESULT, SearchResult::class.java) 102 | } else { 103 | @Suppress("DEPRECATION") 104 | intent.getSerializableExtra(ARG_SEARCH_RESULT) 105 | as SearchResult 106 | } 107 | 108 | companion object { 109 | private const val TAG = "ViewLyricsFragment" 110 | const val ARG_SONG_ID = "song_id" 111 | const val ARG_SEARCH_RESULT = "search_result" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/ui/viewlyrics/ViewLyricsViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.ui.viewlyrics 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import dev.forkhandles.result4k.Result 7 | import io.github.teccheck.fastlyrics.api.LyricStorage 8 | import io.github.teccheck.fastlyrics.api.LyricsApi 9 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 10 | import io.github.teccheck.fastlyrics.model.SearchResult 11 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 12 | 13 | class ViewLyricsViewModel : ViewModel() { 14 | 15 | private val _songWithLyrics = MutableLiveData>() 16 | 17 | val songWithLyrics: LiveData> = _songWithLyrics 18 | 19 | fun loadLyricsForSongFromStorage(songId: Long) = LyricStorage.getSongAsync(songId, _songWithLyrics) 20 | 21 | fun loadLyricsForSearchResult(searchResult: SearchResult) = LyricsApi.getLyricsAsync(searchResult, _songWithLyrics) 22 | 23 | companion object { 24 | private const val TAG = "ViewLyricsViewModel" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/utils/DebouncedMutableLiveData.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.utils 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | 5 | class DebouncedMutableLiveData : MutableLiveData() { 6 | override fun setValue(value: T) { 7 | if (value == getValue()) return 8 | 9 | super.setValue(value) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/utils/PlaceholderDrawable.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.utils 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.LayerDrawable 5 | import androidx.annotation.DrawableRes 6 | import io.github.teccheck.fastlyrics.R 7 | 8 | class PlaceholderDrawable(context: Context, @DrawableRes drawable: Int) : 9 | LayerDrawable( 10 | arrayOf( 11 | context.resources.getDrawable(R.color.art_placeholder_background), 12 | context.getDrawable(drawable)?.apply { 13 | setTint(context.resources.getColor(R.color.art_placeholder_foreground)) 14 | } 15 | ) 16 | ) { 17 | 18 | init { 19 | val padding = context.resources.getDimension(R.dimen.placeholder_drawable_padding).toInt() 20 | this.setLayerInset(1, padding, padding, padding, padding) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/utils/ProviderOrder.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 6 | import io.github.teccheck.fastlyrics.utils.Utils.swap 7 | 8 | object ProviderOrder { 9 | private const val KEY_PREFIX_PROVIDER_ORDER = "provider_order" 10 | private const val KEY_PREFIX_PROVIDER_ENABLED = "provider_enabled" 11 | 12 | private lateinit var sharedPreferences: SharedPreferences 13 | 14 | private var order = arrayOf() 15 | private var enabled = mutableMapOf() 16 | 17 | val providers: Array get() = order.filter { enabled[it] == true }.toTypedArray() 18 | 19 | fun init(context: Context) { 20 | sharedPreferences = 21 | context.getSharedPreferences(context.packageName + "_providers", Context.MODE_PRIVATE) 22 | load() 23 | } 24 | 25 | fun setEnabled(provider: LyricsProvider, enabled: Boolean) { 26 | this.enabled[provider] = enabled 27 | } 28 | 29 | fun getEnabled(provider: LyricsProvider): Boolean = this.enabled[provider] ?: false 30 | 31 | fun getOrderCount(): Int = order.size 32 | 33 | fun swap(first: Int, second: Int) { 34 | order.swap(first, second) 35 | } 36 | 37 | fun getProvider(index: Int): LyricsProvider = order[index] 38 | 39 | fun load() { 40 | this.order = LyricsProvider.getAllProviders().associateWith { 41 | sharedPreferences.getInt(orderKey(it.getName()), -1) 42 | }.toList().sortedBy { it.second }.map { it.first }.toTypedArray() 43 | 44 | this.enabled = LyricsProvider.getAllProviders().associateWith { 45 | sharedPreferences.getBoolean(enabledKey(it.getName()), true) 46 | }.toMutableMap() 47 | } 48 | 49 | fun save() { 50 | val editor = sharedPreferences.edit() 51 | this.order.forEachIndexed { index, lyricsProvider -> 52 | editor.putInt( 53 | orderKey(lyricsProvider.getName()), 54 | index 55 | ) 56 | } 57 | this.enabled.forEach { editor.putBoolean(enabledKey(it.key.getName()), it.value) } 58 | editor.apply() 59 | } 60 | 61 | private fun orderKey(name: String) = "${KEY_PREFIX_PROVIDER_ORDER}_$name" 62 | private fun enabledKey(name: String) = "${KEY_PREFIX_PROVIDER_ENABLED}_$name" 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/teccheck/fastlyrics/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics.utils 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.view.View 9 | import androidx.annotation.DrawableRes 10 | import androidx.annotation.StringRes 11 | import androidx.core.app.ShareCompat 12 | import androidx.core.content.ContextCompat 13 | import com.google.gson.JsonElement 14 | import dev.forkhandles.result4k.Failure 15 | import dev.forkhandles.result4k.Result 16 | import dev.forkhandles.result4k.Success 17 | import io.github.teccheck.fastlyrics.R 18 | import io.github.teccheck.fastlyrics.api.provider.Deezer 19 | import io.github.teccheck.fastlyrics.api.provider.Genius 20 | import io.github.teccheck.fastlyrics.api.provider.LrcLib 21 | import io.github.teccheck.fastlyrics.api.provider.LyricsProvider 22 | import io.github.teccheck.fastlyrics.api.provider.Netease 23 | import io.github.teccheck.fastlyrics.api.provider.PetitLyrics 24 | import io.github.teccheck.fastlyrics.exceptions.LyricsApiException 25 | import io.github.teccheck.fastlyrics.exceptions.LyricsNotFoundException 26 | import io.github.teccheck.fastlyrics.exceptions.NetworkException 27 | import io.github.teccheck.fastlyrics.exceptions.NoMusicPlayingException 28 | import io.github.teccheck.fastlyrics.exceptions.ParseException 29 | import io.github.teccheck.fastlyrics.model.SongWithLyrics 30 | import io.github.teccheck.fastlyrics.model.SyncedLyrics 31 | 32 | object Utils { 33 | fun result(value: T?, exception: E): Result = if (value == null) { 34 | Failure(exception) 35 | } else { 36 | Success(value) 37 | } 38 | 39 | fun View.setVisible(visible: Boolean) { 40 | visibility = if (visible) View.VISIBLE else View.GONE 41 | } 42 | 43 | fun copyToClipboard(context: Context, title: String, text: String) = 44 | ContextCompat.getSystemService(context, ClipboardManager::class.java) 45 | ?.setPrimaryClip(ClipData.newPlainText(title, text)) 46 | 47 | fun openLink(context: Context, link: String) = context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link))) 48 | 49 | fun share(context: Context, songTitle: String, artist: String, text: String) { 50 | val title = context.getString(R.string.share_title, songTitle, artist) 51 | ShareCompat.IntentBuilder(context).setText(text).setType("text/plain") 52 | .setChooserTitle(title).setSubject(title).startChooser() 53 | } 54 | 55 | fun SongWithLyrics.getLyrics(preferSynced: Boolean = false): String { 56 | if (preferSynced && lyricsSynced != null) return lyricsSynced 57 | 58 | if (lyricsPlain != null) return lyricsPlain 59 | 60 | if (lyricsSynced != null) { 61 | return SyncedLyrics.parseLrcToList(lyricsSynced) 62 | .joinToString("\n") { it.text } 63 | } 64 | 65 | return "" 66 | } 67 | 68 | fun JsonElement.asStringOrNull(): String? { 69 | if (this.isJsonNull) return null 70 | return this.asString 71 | } 72 | 73 | fun Array.swap(first: Int, second: Int) { 74 | val tmp = this[second] 75 | this[second] = this[first] 76 | this[first] = tmp 77 | } 78 | 79 | @DrawableRes 80 | fun getProviderIconRes(provider: LyricsProvider) = when (provider) { 81 | Genius -> R.drawable.genius 82 | Deezer -> R.drawable.deezer 83 | LrcLib -> R.drawable.lrclib 84 | PetitLyrics -> R.drawable.petitlyrics 85 | Netease -> R.drawable.netease 86 | else -> R.drawable.fastlyrics 87 | } 88 | 89 | @StringRes 90 | fun getProviderNameRes(provider: LyricsProvider) = when (provider) { 91 | Genius -> R.string.source_genius 92 | Deezer -> R.string.source_deezer 93 | LrcLib -> R.string.source_lrclib 94 | PetitLyrics -> R.string.source_petitlyrics 95 | Netease -> R.string.source_netease 96 | else -> R.string.app_name 97 | } 98 | 99 | @StringRes 100 | fun getProviderUrlRes(provider: LyricsProvider) = when (provider) { 101 | Genius -> R.string.source_url_genius 102 | Deezer -> R.string.source_url_deezer 103 | LrcLib -> R.string.source_url_lrclib 104 | PetitLyrics -> R.string.source_url_petitlyrics 105 | Netease -> R.string.source_url_netease 106 | else -> null 107 | } 108 | 109 | @StringRes 110 | fun getErrorTextRes(exception: LyricsApiException) = when (exception) { 111 | is LyricsNotFoundException -> R.string.lyrics_not_found 112 | is NetworkException -> R.string.lyrics_network_exception 113 | is ParseException -> R.string.lyrics_parse_exception 114 | is NoMusicPlayingException -> R.string.no_song_playing 115 | else -> R.string.lyrics_unknown_error 116 | } 117 | 118 | @DrawableRes 119 | fun getErrorIconRes(exception: LyricsApiException) = when (exception) { 120 | is NoMusicPlayingException -> R.drawable.outline_music_off_24 121 | else -> R.drawable.baseline_error_outline_24 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_arrow_forward_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_check_circle_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_close_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_code_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_drag_handle_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_error_outline_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_open_in_new_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/deezer.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fastlyrics.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/genius.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lrclib.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/netease.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_cloud_download_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_delete_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_info_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_music_off_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_notifications_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_queue_music_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_search_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/petitlyrics.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_music_note_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_permission.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 18 | 19 | 22 | 23 | 35 | 36 | 48 | 49 | 62 | 63 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_saved.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_view_lyrics.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 19 | 20 | 24 | 25 | 29 | 30 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_bar_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_fast_lyrics.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 21 | 22 | 26 | 27 | 32 | 33 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_provider_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 22 | 23 | 29 | 30 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_provider.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 21 | 22 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_provider_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 24 | 25 | 33 | 34 | 40 | 41 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_song.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 20 | 21 | 28 | 29 | 40 | 41 | 42 | 48 | 49 | 55 | 56 | 62 | 63 | 64 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/preference_widget_switch_m3.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 15 | 19 | 20 | 24 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/menu/fragment_saved_contextual_appbar_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/navigation/mobile_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastLyrics 3 | 4 | 5 | Songtext 6 | Gespeichert 7 | Suche 8 | Benachrichtigungszugriff 9 | Einstellungen 10 | Über 11 | 12 | 13 | Es läuft derzeit keine Musik 14 | Konnte keinen Songtext für diesen Song finden 15 | Songtext konnte nicht gelesen werden 16 | Es gab ein Netzwerkproblem beim Laden des Textes 17 | Unbekannter Fehler 18 | 19 | Synchronisierter Songtext verfügbar 20 | 21 | Songtext von 22 | 23 | Kopieren 24 | Songtext 25 | 26 | Teilen 27 | Songtext für %1$s - %2$s 28 | 29 | 30 | Löschen 31 | 32 | %d ausgewählt 33 | 34 | 35 | 36 | FastLyrics benötigt Zugriff auf die Benachrichtungen deines Gerätes, um zu wissen, welcher Song gerade läuft. Mit dem Knopf unten gelangst du zu den passenden Einstellungen 37 | Tip: In neueren Android Versionen kannst du einstellen, was FastLyrics einsehen darf. Da FastLyrics eigentlich nicht wirklich Benachrichtigungen lesen muss, kannst du alles deaktivieren 38 | Zu den Einstellungen 39 | Um Zugriff ein und auszuschalten kannst du jederzeit hier her zurück, falls du die Einstellungen nicht findest 40 | 41 | 42 | 43 | Aussehen 44 | Verhalten 45 | Material Style 46 | App Thema 47 | Songtext automatisch neu laden 48 | Automatically refresh lyrics when the music changes 49 | Textgröße für Songtexte 50 | Synchronisierte Songtexte standardmäßig anzeigen 51 | 52 | 53 | 54 | Quellcode 55 | Songtext Quellen 56 | 57 | 58 | Fastlyrics Song Metadaten Zugriff 59 | -------------------------------------------------------------------------------- /app/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastLyrics 3 | 4 | 5 | Testo 6 | Salvati 7 | Cerca 8 | Permesso per notifiche 9 | Impostazioni 10 | Informazioni 11 | 12 | 13 | Non stai ascoltando niente in questo momento 14 | Impossibile trovare il testo per questa canzone 15 | Impossibile processare il testo 16 | C\'è stato un problema di rete mentre si stava caricando il testo 17 | C\'è stato un errore sconosciuto 18 | 19 | Testo da 20 | 21 | Copia 22 | Testo 23 | 24 | Condividi 25 | Testo di %1$s - %2$s 26 | 27 | 28 | Elimina 29 | 30 | %d selezionato 31 | %d selezionati 32 | 33 | 34 | 35 | FastLyrics ha bisogno dell\'accesso alle notifiche del tuo dispositivo per scoprire qual è il brano in riproduzione. Puoi concedere il permesso nelle impostazioni cliccando sul bottone sottostante 36 | Suggerimento: nelle nuove versioni di Android puoi fare le singole selezioni a cosa FastLyrics può avere accesso. Puoi deselezionare tutto in quanto attualmente FastLyrics non ha accesso ad alcuna notifica 37 | Vai nelle impostazioni 38 | Puoi sempre ritornare qui e concedere/negare l\'accesso alle notifiche se non riesci a trovare il posto giusto nelle impostazioni di sistema 39 | 40 | 41 | Aspetto 42 | Stile Material 43 | Tema dell\'app 44 | Auto-aggiornamento dei testi 45 | Aggiorna automaticamente il testo quando cambia la canzone 46 | 47 | 48 | Codice sorgente 49 | Fonti del testo 50 | 51 | 52 | Accesso ai metadata dei brani per Fastlyrics 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastLyrics 3 | 4 | 5 | 歌詞 6 | 保存済み 7 | 検索 8 | 通知の許可 9 | 設定 10 | 詳細 11 | 12 | 13 | 現在音楽を聴いていません 14 | この曲の歌詞が見つかりませんでした 15 | 歌詞を解析できませんでした 16 | 歌詞の読み込み中にネットワーク問題が発生しました 17 | 不明なエラーが発生しました 18 | 19 | 同期された歌詞が利用可能 20 | 21 | からの歌詞 22 | 23 | コピー 24 | 歌詞 25 | 26 | 共有 27 | %1$s - %2$sの歌詞 28 | 29 | 30 | 消去 31 | 32 | %d個選択済み 33 | 34 | 35 | 36 | FastLyricsは、現在再生中の曲を確認するためにデバイスの通知にアクセスする必要があります。 下のボタンをタップして設定で許可できます 37 | ヒント:新しいAndroidのバージョンでは、FastLyricsがアクセスできる対象を個別に選択できます。 FastLyricsは実際には通知にアクセスしないため、全ての選択を解除できます。 38 | 設定に移動 39 | システム設定で適切な場所が見つからない場合は、いつでも通知へのアクセスを有効または無効にすることができます。 40 | 41 | 42 | 外観 43 | Materialスタイル 44 | アプリのテーマ 45 | 歌詞を自動更新 46 | 音楽が変更されると自動的に歌詞を更新します 47 | 48 | 49 | ソースコード 50 | 歌詞の出典 51 | 52 | 53 | Fastlyricsのメタデータへのアクセス 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #111111 4 | #7C7C7C 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 16 | 17 | 21 | 22 | 26 | 27 | 30 | 31 | 32 | 36 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastLyrics 3 | 4 | 5 | Tekst 6 | Zapisane 7 | Szukaj 8 | Dostęp do powiadomień 9 | Ustawienia 10 | O aplikacji 11 | 12 | 13 | Nie odtwarzany jest żaden utwór 14 | Nie udało się znaleźć tekstu tego utworu 15 | Nie udało się poprawnie przetworzyć tekstu 16 | Błąd w połączeniu sieciowym podczas ładowania tekstu 17 | Wystąpił nieznany błąd 18 | 19 | Dostawcą tekstu jest 20 | 21 | Kopiuj 22 | Tekst 23 | 24 | Udostępnij 25 | Tekst %1$s - %2$s 26 | 27 | 28 | Usuń 29 | 30 | %d wybrano 31 | 32 | 33 | 34 | W celu uzyskania informacji o obecnie granym utworze należy udzielić FastLyrics dostępu do powiadomień. Możesz to zrobić za pomocą menu ustawień, dotykając poniższego przycisku 35 | Porada: W nowszych wersjach systemu Android możesz dokonać selektywnego wyboru funkcji, do których FastLyrics ma dostęp. Możesz odznaczyć wszystkie opcje, ponieważ FastLyrics w rzeczywistości z nich nie korzysta 36 | Przejdź do ustawień 37 | Zawsze możesz tu wrócić, aby zezwolić/odmówić dostępu do powiadomień, jeżeli nie jesteś w stanie znaleźć odpowiedniej sekcji w menu ustawień 38 | 39 | 40 | Wygląd 41 | Material Style 42 | Motyw aplikacji 43 | Automatyczne odświeżanie 44 | Automatycznie odśwież tekst po zmianie odtwarzanego utworu 45 | 46 | 47 | Kod źródłowy 48 | Dostawcy tekstów 49 | 50 | 51 | Dostęp do metadanych utwórów dla FastLyrics 52 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Material 1 4 | Material 2 5 | Material 3 (experimental) 6 | 7 | 8 | 9 | 1 10 | 2 11 | 3 12 | 13 | 14 | 15 | Follow system 16 | Light 17 | Dark 18 | 19 | 20 | 21 | -1 22 | 1 23 | 2 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #47b278 4 | #396f52 5 | #E54D4D 6 | #9A0000 7 | #ffffff 8 | #ffffff 9 | 10 | #202124 11 | 12 | #E2E2E2 13 | #454545 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 5 | 12dp 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastLyrics 3 | 4 | 5 | Lyrics 6 | Saved 7 | Search 8 | Notification Permission 9 | Settings 10 | About 11 | 12 | 13 | You\'re not listening to any music right now 14 | Could not find lyrics for this song 15 | Could not parse lyrics 16 | There was a network problem while loading lyrics 17 | An unknown error occurred 18 | 19 | Synced lyrics available 20 | 21 | Lyrics from 22 | 23 | Copy 24 | Lyrics 25 | 26 | Share 27 | Lyrics for %1$s - %2$s 28 | 29 | 30 | Delete 31 | 32 | %d selected 33 | 34 | 35 | 36 | FastLyrics needs access to your device\'s notifications in order to find out what song is currently playing. You can allow this in the settings by clicking the button below 37 | Tip: In newer versions of Android you can individually select what FastLyrics has access to. You can deselect everything because FastLyrics doesn\'t actually access any notifications 38 | Go to settings 39 | You can always come back here and enable/disable notification access if you can\'t find the right place in your system settings 40 | 41 | 42 | Appearance 43 | Behavior 44 | Material Style 45 | App theme 46 | Auto refresh lyrics 47 | Automatically refresh lyrics when the music changes 48 | Lyrics text size 49 | Synced lyrics by default 50 | 51 | 52 | Source code 53 | Lyrics sources 54 | 55 | Genius 56 | Deezer 57 | LRCLIB 58 | PetitLyrics 59 | Netease 60 | 61 | https://github.com/TecCheck/FastLyrics 62 | https://genius.com/ 63 | https://deezer.com/ 64 | https://lrclib.net/ 65 | https://petitlyrics.com/ 66 | https://music.163.com/ 67 | 68 | 69 | Fastlyrics song metadata access 70 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 14 | 15 | 18 | 19 | 21 | 22 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml/root_preferences.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 10 | 11 | 12 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 39 | 40 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/test/java/io/github/teccheck/fastlyrics/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.teccheck.fastlyrics 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | Assert.assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:8.7.3' 9 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0' 10 | 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | task clean(type: Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Grundlegende Funktionalität 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Automatische Aktualiserung des Songtextes 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Kleine Bugfixes und Design Verbesserungen 2 | Klick auf Quellen banner öffnet Quelle 3 | Suchfunktion 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Italienische Übersetzung 2 | Deutsche Übersetzung 3 | Bessere Suchfunktion 4 | Kopieren und Teilen Knopf 5 | Material 3 Design (experimentell) 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Polnische Übersetzung 2 | Deezer Songtexte 3 | Syncronisierte Songtexte 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Versuche einen Crash zu beheben, wenn die App keinen Zugriff auf die Benachrichtigungen hat 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/full_description.txt: -------------------------------------------------------------------------------- 1 | FastLyrics läd automatisch Songtexte zum aktuell laufenden Song herunter. 2 | Außerdem werden die Songtexte gespeichert, um sie offline anzuschauen 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/short_description.txt: -------------------------------------------------------------------------------- 1 | Lädt automatisch Songtexte zum aktuell laufenden Song herunter und zeigt sie an 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de-DE/title.txt: -------------------------------------------------------------------------------- 1 | FastLyrics 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Added basic functionality 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Added automatic refresh 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Fixed a few bugs, odd behavior and styling issues 2 | Added the ability to click on the footer to get to the lyrics' source 3 | Added a search function 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Italian translation 2 | German translation 3 | Better search 4 | Add copy and share button 5 | Add Material 3 design (experimental) 6 | 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Polish translation 2 | Deezer lyrics 3 | Synced lyrics 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Tried fixing a crash, if the app doesn't have notification access 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | FastLyrics is an app that downloads lyrics for the song, you're listening to. 2 | It saves lyrics for offline use and even faster access. 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | An app that downloads lyrics for the song, you're listening to 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | FastLyrics 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/it/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Aggiunte funzioni base 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/it/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Aggiunto aggiornamento automatico 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/it/full_description.txt: -------------------------------------------------------------------------------- 1 | FastLyrics è un'app che scarica i testi delle canzoni che stai ascoltando. 2 | L'app salva i testi per un uso offline e per un accesso più rapido. 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/it/short_description.txt: -------------------------------------------------------------------------------- 1 | Un'app che scarica i testi delle canzoni, anche di quelle che stai ascoltando 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/it/title.txt: -------------------------------------------------------------------------------- 1 | FastLyrics 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Dodano podstawowe funkcje 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Dodano automatyczne odświeżanie 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Naprawiono kilka błędów, niepożądanych zachowań oraz problemów ze stylem 2 | Dodano możliwość dotknięcia stopki aby przejść do oryginalnej strony z tekstem utworu 3 | Dodano funkcję wyszukiwania 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Dodano Włoskie tłumaczenie 2 | Dodano Niemieckie tłumaczenie 3 | Poprawono funkcję wyszukiwania 4 | Dodano przyciski Kopiuj i Udostępnij 5 | Dodano design Material 3 (eksperymentalne) -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/full_description.txt: -------------------------------------------------------------------------------- 1 | FastLyrics wyświetla teksty słuchanych przez ciebie utworów oraz zapisuje je w celu szybszego dostępu, bez potrzeby połączenia z internetem. -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/short_description.txt: -------------------------------------------------------------------------------- 1 | Aplikacja wyświetlająca i pobierająca teksty słuchanych przez ciebie utworów 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/pl/title.txt: -------------------------------------------------------------------------------- 1 | FastLyrics 2 | -------------------------------------------------------------------------------- /fastlane/svg/AppIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 44 | 48 | 49 | 51 | 61 | 65 | 70 | 73 | 76 | 80 | 85 | 91 | 92 | 93 | 97 | 107 | 117 | 122 | 128 | 134 | 140 | 146 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.nonTransitiveRClass=true 23 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teccheck/FastLyrics/4222406e3132a456774cf3c02ea6d08ed6ea4bb4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 20 22:04:32 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 6 | networkTimeout=10000 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | maven { url 'https://www.jitpack.io' } 7 | // Warning: this repository is going to shut down soon 8 | } 9 | } 10 | rootProject.name = "FastLyrics" 11 | include ':app' 12 | --------------------------------------------------------------------------------