├── .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 | [](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 | [
](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 | 
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 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/fragment_saved_contextual_appbar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------