├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ ├── librespot-dacp-1.6.4-20231011.152351-33.jar │ ├── librespot-decoder-api-1.6.4-20231011.152351-32.jar │ ├── librespot-lib-1.6.4-20231011.152351-32.jar │ ├── librespot-player-1.6.4-20231011.152351-29-thin.jar │ └── librespot-sink-api-1.6.4-20231011.152351-35.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── v │ │ └── blade │ │ └── SpotifyConnectionTest.java │ ├── debug │ └── AndroidManifest.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── v │ │ └── blade │ │ ├── BladeApplication.java │ │ ├── library │ │ ├── Album.java │ │ ├── Artist.java │ │ ├── Library.java │ │ ├── LibraryObject.java │ │ ├── Playlist.java │ │ ├── Separator.java │ │ └── Song.java │ │ ├── player │ │ ├── MediaBrowserService.java │ │ ├── MediaSessionCallback.java │ │ └── PlayerNotification.java │ │ ├── sources │ │ ├── Source.java │ │ ├── SourceInformation.java │ │ ├── deezer │ │ │ ├── Deezer.java │ │ │ ├── DeezerPlayer.java │ │ │ ├── DeezerService.java │ │ │ └── DeezerSettingsFragment.java │ │ ├── local │ │ │ ├── Local.java │ │ │ └── LocalPlayer.java │ │ └── spotify │ │ │ ├── Spotify.java │ │ │ ├── SpotifyExploreAdapter.java │ │ │ ├── SpotifyPlayer.java │ │ │ └── SpotifyService.java │ │ └── ui │ │ ├── Dialogs.java │ │ ├── ExploreFragment.java │ │ ├── GeniusService.java │ │ ├── LibraryFragment.java │ │ ├── LibraryObjectAdapter.java │ │ ├── LyricsActivity.java │ │ ├── MainActivity.java │ │ ├── PlayActivity.java │ │ ├── SettingsActivity.java │ │ └── TouchHelperCallback.java │ └── res │ ├── drawable-anydpi-v24 │ ├── ic_pause_notification.xml │ ├── ic_play_notification.xml │ ├── ic_skip_next_notification.xml │ └── ic_skip_previous_notification.xml │ ├── drawable-hdpi │ ├── ic_album_notification.png │ ├── ic_blade_notification.png │ ├── ic_pause_notification.png │ ├── ic_play_notification.png │ ├── ic_skip_next_notification.png │ └── ic_skip_previous_notification.png │ ├── drawable-mdpi │ ├── ic_album_notification.png │ ├── ic_blade_notification.png │ ├── ic_pause_notification.png │ ├── ic_play_notification.png │ ├── ic_skip_next_notification.png │ └── ic_skip_previous_notification.png │ ├── drawable-xhdpi │ ├── ic_album_notification.png │ ├── ic_blade_notification.png │ ├── ic_pause_notification.png │ ├── ic_play_notification.png │ ├── ic_skip_next_notification.png │ └── ic_skip_previous_notification.png │ ├── drawable-xxhdpi │ ├── ic_album_notification.png │ ├── ic_blade_notification.png │ ├── ic_pause_notification.png │ ├── ic_play_notification.png │ ├── ic_skip_next_notification.png │ └── ic_skip_previous_notification.png │ ├── drawable-xxxhdpi │ ├── ic_album_notification.png │ └── ic_blade_notification.png │ ├── drawable │ ├── fastscroll_thumb.xml │ ├── fastscroll_track.xml │ ├── ic_add.xml │ ├── ic_album.xml │ ├── ic_album_artist_24px.xml │ ├── ic_artist.xml │ ├── ic_dark_theme.xml │ ├── ic_deezer.xml │ ├── ic_deezer_logo.xml │ ├── ic_explore.xml │ ├── ic_hourglass.xml │ ├── ic_info_24px.xml │ ├── ic_local.xml │ ├── ic_more_vert.xml │ ├── ic_pause.xml │ ├── ic_pause_circle.xml │ ├── ic_play_arrow.xml │ ├── ic_play_circle.xml │ ├── ic_playlist.xml │ ├── ic_playlist_add.xml │ ├── ic_reorder_24px.xml │ ├── ic_repeat.xml │ ├── ic_repeat_one.xml │ ├── ic_search_24px.xml │ ├── ic_settings_24px.xml │ ├── ic_shuffle.xml │ ├── ic_skip_next.xml │ ├── ic_skip_previous.xml │ ├── ic_song.xml │ ├── ic_sources_24px.xml │ ├── ic_spotify.xml │ ├── ic_spotify_logo.xml │ ├── ic_sync_24px.xml │ ├── thumb.xml │ └── track.xml │ ├── layout-land │ └── activity_play.xml │ ├── layout │ ├── activity_lyrics.xml │ ├── activity_main.xml │ ├── activity_play.xml │ ├── app_bar_main.xml │ ├── content_main.xml │ ├── dialog_create_playlist.xml │ ├── fragment_explore.xml │ ├── fragment_library.xml │ ├── item_label_layout.xml │ ├── item_layout.xml │ ├── item_simple_layout.xml │ ├── item_switch.xml │ ├── nav_header_main.xml │ ├── settings_activity.xml │ ├── settings_fragment_about.xml │ ├── settings_fragment_deezer.xml │ ├── settings_fragment_local.xml │ ├── settings_fragment_sources.xml │ └── settings_fragment_spotify.xml │ ├── menu │ ├── activity_main_drawer.xml │ ├── currentplay_more.xml │ ├── item_more.xml │ └── main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── navigation │ └── mobile_navigation.xml │ ├── values-de │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values-pl │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── spotify_audio_quality.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── header_preferences.xml │ ├── network_security_config.xml │ └── searchable.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup JDK17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Restore Blade keystore (for app signing) 24 | run: | 25 | echo "${{ secrets.BLADE_KEYSTORE }}" > blade-keystore.b64 26 | base64 -d blade-keystore.b64 > blade-keystore.jks 27 | 28 | - name: Restore debug keystore (for debug configurations) 29 | run: | 30 | echo "${{ secrets.DEBUG_KEYSTORE }}" > debug.b64 31 | mkdir ~/.android 32 | base64 -d debug.b64 > ~/.android/debug.keystore 33 | 34 | - name: Grant execute permission for gradlew 35 | run: chmod +x gradlew 36 | - name: Build with Gradle 37 | run: ./gradlew build -PreleaseStorePassword=${{ secrets.RELEASE_STORE_PASSWORD }} -PreleaseKeyPassword=${{ secrets.RELEASE_KEY_PASSWORD }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | blade-keystore.jks 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Blade Player -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 132 | 133 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome ! 4 | There are many ways you can contribute to Blade : report bugs, suggest features, translate it to 5 | your language, fixing a bug, or implementing a new feature. 6 | 7 | ## Translating Blade 8 | 9 | Like any (i think ?) Android app, Blade uses strings in ressource files : you can find the default ( 10 | english) one in [app/src/main/res/values/strings.xml](app/src/main/res/values/strings.xml). To 11 | translate Blade, just get this file and replace every string in it with their translation ; you can 12 | then send the file to me, and i'll update the language files. 13 | 14 | ## Contributing code 15 | 16 | Blade is entirely programmed in Java for Android, using the AndroidX library, with xml layouts and 17 | ressources. Adding code relying on Kotlin, Jetpack Compose, or other libraries doing 'core' Android 18 | job is obviously bad ; please try to keep the design decisions that were made. 19 | 20 | ### Code style 21 | 22 | Blade has a code style that might be considered unusual for Java. Basically, i write like this : 23 | 24 | ``` 25 | void thisIsFunction(String arg0, int arg1) 26 | { 27 | if(arg1 == 3) return; 28 | else if(arg1 == 4) 29 | { 30 | System.out.println("4 !"); 31 | doSomething(); 32 | } 33 | 34 | System.out.println(arg1 + ": " + arg0); 35 | } 36 | ``` 37 | 38 | and not like this : 39 | 40 | ``` 41 | void thisIsFunction(String arg0, int arg1) { 42 | if(arg1 == 3) { 43 | return; 44 | } 45 | else if(arg1 == 4) { 46 | System.out.println("4 !"); 47 | doSomething(); 48 | } 49 | 50 | System.out.println(arg1 + ": " + arg0); 51 | } 52 | ``` 53 | 54 | Please try to stick to this style ; if you are using Android Studio or IntelliJ Idea, the project is 55 | configured with this code style, you can just code the way you want and autoformat before commiting. 56 | 57 | ### Adding new features 58 | 59 | If you want to add a new feature to Blade, go ahead. However, know that this is a personal project, 60 | and if i don't like your feature, i won't add it to mainline. It does not mean i hate you or your 61 | code, it is just that i have no interest in that feature, so i don't want it in my Blade version ; 62 | but feel free to keep a fork of the project with your feature. 63 | 64 | ## Blade internals : global architecture 65 | 66 | Blade consists of 4 components : 67 | - UI (User Interface) with activities/settings/... 68 | - Library, handling library and Blade internals Song, Album, Artist, Playlist types 69 | - Source, handling source submodules 70 | - Player, that handles the music playing 71 | 72 | ## Blade internals : adding a new source 73 | 74 | A Source component has to : 75 | - Register songs in library 76 | - Provide a way to play songs of such source in a SourcePlayer 77 | - Have an explore/search adapter 78 | - Provide ways of interacting with it (adding songs to playlists, removing elements, ...) 79 | 80 | All of this is done extending the Source class. 81 | 82 | ## GitHub actions setup 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blade Player 2 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/vhaudiquet/BladePlayer?style=flat-square) 3 | ![license](https://img.shields.io/github/license/vhaudiquet/BladePlayer?style=flat-square) 4 | ![GitHub last commit](https://img.shields.io/github/last-commit/vhaudiquet/BladePlayer?style=flat-square) 5 | ![GitHub commits since latest release (by date)](https://img.shields.io/github/commits-since/vhaudiquet/BladePlayer/latest?style=flat-square) 6 | 7 | 8 | 9 | Blade is an open source music player for Android, allowing you to play music from multiple 10 | services : files on your phone, [Spotify], and more. 11 | 12 | Blade is available on [Google Play], or [here on GitHub]. 13 | It seems to also be on [IzzySoft F-Droid], and to stay up to date there. 14 | 15 | Other repos like ~~[Aptoide]~~ or even ~~[ApkPure]~~, ~~[ApkCombo]~~ are not recommended because 16 | they don't seem 17 | to stay up to date fast, and i can't check if they modified the `apk` before publishing it. 18 | 19 |
20 | 21 | 22 |
23 | 24 | ## Feature overview 25 | 26 | Blade Player is developed by me ([@vhaudiquet]) alone, so the project cannot be tested on many 27 | devices and scenarios ; if you find an issue, open one here. 28 | 29 | - The app will open on your library, categorized as Artists, Albums, Songs, and Playlists (available 30 | in the navigation drawer). 31 | - It allows you to manage libraries for songs (add/remove from source library) and playlists ( 32 | add/remove from playlist, create/remove playlists) 33 | - It supports Android 'dark theme' (the screenshots above are done on a dark themed system). 34 | - It is completely free (there are no ads, no limited version) 35 | - It caches the library locally, so launching Blade requires virtually no data (only refreshing 36 | tokens and status of sources servers) 37 | - **(TODO)** It has a 'data saving' mode that allows you to listen to music while consuming very low 38 | mobile data (by not loading album arts) 39 | - The search feature allows you to search the local library instantly 40 | - The "explore" mode allows you to search and browse sources for new music (for example 41 | search all Spotify and look for new releases) 42 | - It has a layout that can adapt for tablet users (landscape layout) 43 | - It is available in different languages : English, French, German, Turkish ([and you can easily help me traduce it](CONTRIBUTING.md)) 44 | 45 |
46 | 47 | 48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 | ### Supported services (music sources) 56 | 57 | - Local (i.e. files on your phone) 58 | - Spotify 59 | 60 | ### About Spotify 61 | 62 | You will need a **Spotify Premium** account to play music from [Spotify], but you can use Blade 63 | without a premium account (to play your Spotify playlists from other sources, for example) 64 | 65 | Blade is using the official [Spotify Android Auth] library and [Retrofit] to access 66 | the [Spotify Web API], i.e. to obtain user library and playlists. In order to play music from 67 | Spotify, Blade uses the [librespot-java] library. 68 | 69 | For now, Blade will act as a Spotify Connect peripheral. **Please do not try to control Blade with 70 | Spotify Connect**. It won't work, and it will glitch the app. 71 | 72 | When connecting Blade and Spotify, i am for now obligated to ask directly for your username and 73 | password, 2 times : one for [librespot-java], which requires them directly (Spotify does not allow 74 | streaming music from their API authentification), and one for [Spotify Android Auth], which uses the 75 | secure OAuth2 protocol. I can only promise you that i am not stealing your credentials ; if you are 76 | paranoid, you may build Blade from sources or use a network analyzer to see that all network traffic 77 | goes to [Spotify] servers. 78 | 79 | Special thanks to the people at [librespot-org] and [librespot-java] ; without them, Spotify support 80 | would not have been possible. 81 | 82 | ## Future updates 83 | 84 | New features and bug fixes or improvements are coming. Here is a list of what i want to implement : 85 | 86 | - SPOTIFY: Use Android Native Decoders, and/or libtremolo if native decoder not present 87 | - UI: Show albums in an 'album view' instead of a list 88 | - Add new services : SoundCloud, YouTube Music, Amazon Music, Tidal, WebDAV/FTP Servers... 89 | - CORE: 'Blade' playlists, that can contain song from all sources 90 | 91 | ## Contributing 92 | 93 | See [CONTRIBUTING.md] 94 | 95 | ## Older versions 96 | 97 | If you want older (i.e. < 2.0) versions of Blade, you can check the [old repository]. 98 | 99 | [Google Play]:https://play.google.com/store/apps/details?id=v.blade 100 | 101 | [here on GitHub]:https://github.com/vhaudiquet/BladePlayer/releases 102 | 103 | [IzzySoft F-Droid]:https://apt.izzysoft.de/fdroid/index/apk/v.blade 104 | 105 | [Aptoide]:https://blade-blade.en.aptoide.com/app 106 | 107 | [ApkPure]:https://apkpure.com/blade-player/v.blade/ 108 | 109 | [ApkCombo]:https://apkcombo.com/blade-player/v.blade/ 110 | 111 | [Spotify]:https://www.spotify.com 112 | 113 | [old repository]:https://github.com/vhaudiquet/blade-player 114 | 115 | [Spotify Android Auth]:https://github.com/spotify/android-auth 116 | 117 | [Retrofit]:https://github.com/square/retrofit 118 | 119 | [Spotify Web API]:https://developer.spotify.com/documentation/web-api/ 120 | 121 | [librespot-java]:https://github.com/librespot-org/librespot-java 122 | 123 | [librespot-org]:https://github.com/librespot-org 124 | 125 | [@vhaudiquet]:https://github.com/vhaudiquet 126 | 127 | [CONTRIBUTING.md]:CONTRIBUTING.md -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'kotlin-android' 3 | id 'com.android.application' 4 | } 5 | 6 | android { 7 | compileSdk 34 8 | 9 | defaultConfig { 10 | applicationId "v.blade" 11 | minSdk 21 12 | targetSdk 34 13 | versionCode 31 14 | versionName "2.2.2" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | android.buildFeatures.buildConfig true 18 | manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"] 19 | } 20 | 21 | signingConfigs { 22 | release { 23 | storeFile file("../blade-keystore.jks") 24 | keyAlias "blade-release" 25 | if (project.hasProperty("releaseStorePassword")) 26 | storePassword project.getProperty("releaseStorePassword") 27 | if (project.hasProperty("releaseKeyPassword")) 28 | keyPassword project.getProperty("releaseKeyPassword") 29 | } 30 | } 31 | 32 | buildTypes { 33 | release { 34 | buildConfigField 'String', 'SPOTIFY_CLIENT_ID', '\"2f95bc7168584e7aa67697418a684bae\"' 35 | buildConfigField 'String', 'SPOTIFY_CLIENT_SECRET', '\"3166d3b40ff74582b03cb23d6701c297\"' 36 | 37 | buildConfigField 'String', 'DEEZER_CLIENT_ID', '\"172365\"' 38 | buildConfigField 'String', 'DEEZER_CLIENT_SECRET', '\"fb0bec7ccc063dab0417eb7b0d847f34\"' 39 | 40 | signingConfig signingConfigs.release 41 | minifyEnabled true 42 | shrinkResources true 43 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 44 | } 45 | 46 | debug { 47 | /* Note : these are MY Spotify debug app settings, you should change that if you want debug builds to work with Spotify 48 | * Later, if more people work on the project, those 2 variables should be passed via command-line for debug builds */ 49 | buildConfigField 'String', 'SPOTIFY_CLIENT_ID', '\"048adc76814146e7bb049d89813bd6e0\"' 50 | buildConfigField 'String', 'SPOTIFY_CLIENT_SECRET', '\"854982b95dc04100a2009ebb8a8df758\"' 51 | 52 | buildConfigField 'String', 'DEEZER_CLIENT_ID', '\"\"' 53 | buildConfigField 'String', 'DEEZER_CLIENT_SECRET', '\"\"' 54 | 55 | debuggable true 56 | } 57 | } 58 | compileOptions { 59 | sourceCompatibility 17 60 | targetCompatibility 17 61 | } 62 | buildFeatures { 63 | viewBinding true 64 | } 65 | packagingOptions { 66 | resources { 67 | excludes += ['log4j2.xml', 'META-INF/DEPENDENCIES'] 68 | } 69 | } 70 | namespace 'v.blade' 71 | } 72 | 73 | dependencies { 74 | /* stdlib */ 75 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-common:1.9.21' 76 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.21' 77 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.21' 78 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21' 79 | 80 | /* AndroidX (Jetpack) : compat libraries, and Material */ 81 | implementation "androidx.exifinterface:exifinterface:1.3.7" 82 | implementation 'androidx.appcompat:appcompat:1.6.1' 83 | implementation 'com.google.android.material:material:1.11.0' 84 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 85 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' 86 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' 87 | implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6' 88 | implementation 'androidx.navigation:navigation-ui-ktx:2.7.6' 89 | implementation 'androidx.preference:preference-ktx:1.2.1' 90 | implementation 'androidx.media:media:1.7.0' 91 | 92 | /* Image loading : Picasso */ 93 | implementation 'com.squareup.picasso:picasso:2.8' 94 | 95 | /* Spotify : librespot + auth (+ retrofit) */ 96 | /*implementation('xyz.gianlu.librespot:librespot-player:1.6.2:thin') { 97 | exclude group: 'xyz.gianlu.librespot', module: 'librespot-sink' 98 | exclude group: 'com.lmax', module: 'disruptor' 99 | exclude group: 'org.apache.logging.log4j' 100 | }*/ 101 | implementation files('libs/librespot-player-1.6.4-20231011.152351-29-thin.jar') 102 | implementation files('libs/librespot-lib-1.6.4-20231011.152351-32.jar') 103 | implementation files('libs/librespot-sink-api-1.6.4-20231011.152351-35.jar') 104 | implementation files('libs/librespot-dacp-1.6.4-20231011.152351-33.jar') 105 | implementation files('libs/librespot-decoder-api-1.6.4-20231011.152351-32.jar') 106 | implementation 'org.jcraft:jorbis:0.0.17' 107 | implementation 'com.badlogicgames.jlayer:jlayer:1.0.2-gdx' 108 | implementation 'com.google.protobuf:protobuf-java:3.25.1' 109 | implementation 'commons-net:commons-net:3.10.0' 110 | implementation 'com.google.code.gson:gson:2.10.1' 111 | 112 | implementation 'uk.uuid.slf4j:slf4j-android:2.0.9-0' //Needed to log librespot-player 113 | 114 | implementation 'com.spotify.android:auth:2.1.0' 115 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 116 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 117 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 118 | 119 | testImplementation 'junit:junit:4.13.2' 120 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 121 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 122 | androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' 123 | androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1' 124 | } 125 | -------------------------------------------------------------------------------- /app/libs/librespot-dacp-1.6.4-20231011.152351-33.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/libs/librespot-dacp-1.6.4-20231011.152351-33.jar -------------------------------------------------------------------------------- /app/libs/librespot-decoder-api-1.6.4-20231011.152351-32.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/libs/librespot-decoder-api-1.6.4-20231011.152351-32.jar -------------------------------------------------------------------------------- /app/libs/librespot-lib-1.6.4-20231011.152351-32.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/libs/librespot-lib-1.6.4-20231011.152351-32.jar -------------------------------------------------------------------------------- /app/libs/librespot-player-1.6.4-20231011.152351-29-thin.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/libs/librespot-player-1.6.4-20231011.152351-29-thin.jar -------------------------------------------------------------------------------- /app/libs/librespot-sink-api-1.6.4-20231011.152351-35.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/libs/librespot-sink-api-1.6.4-20231011.152351-35.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Project-specific ProGuard rules 2 | 3 | # We optimize, so we obfuscate ; to be able to read 4 | # debug stack trace, we need this mapping 5 | -keepattributes LineNumberTable,SourceFile 6 | -renamesourcefileattribute SourceFile 7 | 8 | # We save sources using their class, and instanciate 9 | # them using reflection : we need their class to stay 10 | # the same 11 | -keep public class * extends v.blade.sources.Source 12 | -keepclassmembers public class * extends v.blade.sources.Source 13 | { 14 | public static final int NAME_RESOURCE; 15 | public static final int DESCRIPTION_RESOURCE; 16 | public static final int IMAGE_RESOURCE; 17 | } 18 | -keep class v.blade.sources.spotify.Spotify$SpotifyTokenResponse {*;} 19 | -keep class v.blade.sources.spotify.SpotifyService$* {*;} 20 | -keep class v.blade.sources.deezer.Deezer$DeezerTokenResponse {*;} 21 | -keep class v.blade.sources.deezer.Deezer$DeezerErrorObject {*;} 22 | -keep class v.blade.sources.deezer.DeezerService$* {*;} 23 | 24 | # The spotify librespot player needs an 'output class' 25 | # for it's audio output ; we need to keep that 26 | -keep class v.blade.sources.spotify.SpotifyPlayer$BladeSinkOutput 27 | 28 | # librespot needs reflexion to work internally 29 | # Here we keep the classes it needs 30 | -keep class xyz.gianlu.librespot.mercury.MercuryRequests$GenericJson 31 | { 32 | (com.google.gson.JsonObject); # method i.e. constructor 33 | } 34 | -keep class xyz.gianlu.librespot.json.GenericJson 35 | { 36 | (com.google.gson.JsonObject); # method i.e. constructor 37 | } 38 | -keep class xyz.gianlu.librespot.mercury.MercuryRequests$ResolvedContextWrapper 39 | { 40 | (com.google.gson.JsonObject); # method i.e. constructor 41 | } 42 | -keep class xyz.gianlu.librespot.json.ResolvedContextWrapper 43 | { 44 | (com.google.gson.JsonObject); # method i.e. constructor 45 | } 46 | -keep class com.spotify.** {*;} 47 | -keep class xyz.gianlu.librespot.audio.decoders.** {*;} 48 | 49 | # We load settings fragment using reflexion 50 | # We need to keep the SettingsActivity class for that 51 | -keep class v.blade.ui.SettingsActivity$AboutFragment 52 | -keep class v.blade.ui.SettingsActivity$SourcesFragment 53 | 54 | # For lyrics, we use Genius API, as a retrofit service : reflexion 55 | -keep class v.blade.ui.GeniusService$* {*;} 56 | 57 | # OkHttp version < 5.0 warns for this missing ; this is 58 | # a bug (https://github.com/square/okhttp/issues/6258) 59 | # We can safely ignore (those classes are not needed on Android) 60 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 61 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 62 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 63 | -dontwarn org.conscrypt.Conscrypt$Version 64 | -dontwarn org.conscrypt.Conscrypt 65 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 66 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 67 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 68 | -dontwarn org.openjsse.net.ssl.OpenJSSE 69 | 70 | # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and 71 | # EnclosingMethod is required to use InnerClasses. 72 | -keepattributes Signature, InnerClasses, EnclosingMethod 73 | 74 | # Retrofit does reflection on method and parameter annotations. 75 | -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations 76 | 77 | # Keep annotation default values (e.g., retrofit2.http.Field.encoded). 78 | -keepattributes AnnotationDefault 79 | 80 | # Retain service method parameters when optimizing. 81 | -keepclassmembers,allowshrinking,allowobfuscation interface * { 82 | @retrofit2.http.* ; 83 | } 84 | 85 | # Ignore annotation used for build tooling. 86 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 87 | 88 | # Ignore JSR 305 annotations for embedding nullability information. 89 | -dontwarn javax.annotation.** 90 | 91 | # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. 92 | -dontwarn kotlin.Unit 93 | 94 | # Top-level functions that can only be used by Kotlin. 95 | -dontwarn retrofit2.KotlinExtensions 96 | -dontwarn retrofit2.KotlinExtensions$* 97 | 98 | # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy 99 | # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. 100 | -if interface * { @retrofit2.http.* ; } 101 | -keep,allowobfuscation interface <1> 102 | 103 | # Keep inherited services. 104 | -if interface * { @retrofit2.http.* ; } 105 | -keep,allowobfuscation interface * extends <1> 106 | 107 | # With R8 full mode generic signatures are stripped for classes that are not 108 | # kept. Suspend functions are wrapped in continuations where the type argument 109 | # is used. 110 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 111 | 112 | # R8 full mode strips generic signatures from return types if not kept. 113 | -if interface * { @retrofit2.http.* public *** *(...); } 114 | -keep,allowoptimization,allowshrinking,allowobfuscation class <3> -------------------------------------------------------------------------------- /app/src/androidTest/java/v/blade/SpotifyConnectionTest.java: -------------------------------------------------------------------------------- 1 | package v.blade; 2 | 3 | import static androidx.test.espresso.Espresso.closeSoftKeyboard; 4 | import static androidx.test.espresso.Espresso.onView; 5 | import static androidx.test.espresso.action.ViewActions.click; 6 | import static androidx.test.espresso.action.ViewActions.typeText; 7 | import static androidx.test.espresso.matcher.ViewMatchers.withId; 8 | import static androidx.test.espresso.matcher.ViewMatchers.withResourceName; 9 | import static androidx.test.espresso.matcher.ViewMatchers.withText; 10 | import static androidx.test.espresso.web.sugar.Web.onWebView; 11 | import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement; 12 | import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick; 13 | import static androidx.test.espresso.web.webdriver.DriverAtoms.webKeys; 14 | 15 | import androidx.test.espresso.contrib.RecyclerViewActions; 16 | import androidx.test.espresso.web.webdriver.Locator; 17 | import androidx.test.ext.junit.rules.ActivityScenarioRule; 18 | import androidx.test.ext.junit.runners.AndroidJUnit4; 19 | import androidx.test.filters.LargeTest; 20 | import androidx.test.platform.app.InstrumentationRegistry; 21 | 22 | import org.junit.Before; 23 | import org.junit.Rule; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | 27 | import v.blade.sources.Source; 28 | import v.blade.sources.spotify.Spotify; 29 | import v.blade.ui.SettingsActivity; 30 | 31 | @RunWith(AndroidJUnit4.class) 32 | @LargeTest 33 | public class SpotifyConnectionTest 34 | { 35 | /* 36 | * This is an Espresso test, testing that connecting to a Spotify account works as intended 37 | * 38 | * It is also useful to automate screenshots, as once Spotify connected it can populate the 39 | * library and thus we generate nice screenshots :)) 40 | */ 41 | 42 | @Rule 43 | public ActivityScenarioRule settingsActivityRule = new ActivityScenarioRule<>(SettingsActivity.class); 44 | 45 | @Before 46 | public void setup() 47 | { 48 | 49 | } 50 | 51 | private int getSpotifyAmount() 52 | { 53 | int amount = 0; 54 | for(Source source : Source.SOURCES) 55 | if(source instanceof Spotify) 56 | amount++; 57 | return amount; 58 | } 59 | 60 | @Test 61 | public void addSpotifyConnection() throws InterruptedException 62 | { 63 | //Check Spotify amount 64 | int beforeSpotifyAmount = getSpotifyAmount(); 65 | 66 | //Go to source 67 | onView(withText(R.string.sources)).perform(click()); 68 | //Click on button 69 | onView(withId(R.id.settings_source_add)).perform(click()); 70 | //Click on 'Spotify' 71 | onView(withText(R.string.spotify)).perform(click()); 72 | 73 | //Check if Spotify source was added 74 | int afterSpotifyAmount = getSpotifyAmount(); 75 | assert afterSpotifyAmount == beforeSpotifyAmount + 1; 76 | 77 | //Open the added Spotify settings fragment 78 | int newSpotifyIndex = Source.SOURCES.size() - 1; 79 | onView(withId(R.id.settings_sources_listview)).perform(RecyclerViewActions 80 | .actionOnItemAtPosition(newSpotifyIndex, click())); 81 | 82 | //Fill login informations 83 | String spotifyUser = InstrumentationRegistry.getArguments().getString("spotify_user"); 84 | String spotifyPass = InstrumentationRegistry.getArguments().getString("spotify_password"); 85 | onView(withId(R.id.settings_spotify_user)).perform(typeText(spotifyUser)); 86 | closeSoftKeyboard(); 87 | onView(withId(R.id.settings_spotify_password)).perform(typeText(spotifyPass)); 88 | closeSoftKeyboard(); 89 | 90 | //Start connection procedure 91 | onView(withId(R.id.settings_spotify_sign_in)).perform(click()); 92 | 93 | /* In theory, there are now multiple cases : 94 | * - The user does not have Spotify application installed 95 | * - It's first launch : enter credentials (again) and accept 96 | * - Authcode accepted before : login is done after a short amount of time 97 | * - The user does have Spotify app installed : 98 | * - It's first launch : accept 99 | * - Authcode accepted before : login is done after a (not so short) amount of time 100 | * 101 | * We will handle only 'no spotify app' case for now 102 | */ 103 | 104 | //Wait for window showup 105 | boolean windowOn = false; 106 | while(!windowOn) 107 | { 108 | try 109 | { 110 | onWebView(withResourceName("com_spotify_sdk_login_webview")) 111 | .withElement(findElement(Locator.ID, "login-username")) 112 | .perform(webKeys(spotifyUser)); 113 | closeSoftKeyboard(); 114 | windowOn = true; 115 | } 116 | catch(RuntimeException e) 117 | { 118 | Thread.sleep(1000); 119 | } 120 | } 121 | 122 | //Fill webview login infos and click on connect 123 | onWebView(withResourceName("com_spotify_sdk_login_webview")) 124 | .withElement(findElement(Locator.ID, "login-password")) 125 | .perform(webKeys(spotifyPass)); 126 | closeSoftKeyboard(); 127 | 128 | onWebView(withResourceName("com_spotify_sdk_login_webview")) 129 | .withElement(findElement(Locator.ID, "login-button")) 130 | .perform(webClick()); 131 | 132 | //Now there are 2 cases : either login is done, or webview is still here 133 | // and we have to click accept 134 | try 135 | { 136 | onWebView(withResourceName("com_spotify_sdk_login_webview")) 137 | .withElement(findElement(Locator.ID, "auth-accept")) 138 | .perform(webClick()); 139 | } 140 | catch(RuntimeException ignored) 141 | { 142 | } 143 | 144 | Thread.sleep(1000); 145 | 146 | //Done : check that it went well 147 | assert Source.SOURCES.get(newSpotifyIndex).getStatus() == Source.SourceStatus.STATUS_READY; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 30 | 35 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/v/blade/BladeApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2022 Valentin HAUDIQUET 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | package v.blade; 16 | 17 | import android.annotation.SuppressLint; 18 | import android.app.Activity; 19 | import android.app.Application; 20 | import android.content.ComponentName; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.ServiceConnection; 24 | import android.os.Bundle; 25 | import android.os.IBinder; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | import androidx.appcompat.app.AppCompatDelegate; 30 | import androidx.preference.PreferenceManager; 31 | 32 | import java.util.concurrent.ExecutorService; 33 | import java.util.concurrent.LinkedBlockingQueue; 34 | import java.util.concurrent.ThreadPoolExecutor; 35 | import java.util.concurrent.TimeUnit; 36 | 37 | import v.blade.library.Library; 38 | import v.blade.player.MediaBrowserService; 39 | import v.blade.sources.Source; 40 | 41 | public class BladeApplication extends Application 42 | { 43 | public static abstract class Callback 44 | { 45 | public abstract void run(T arg0); 46 | } 47 | 48 | private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); 49 | private static final ExecutorService executorService = threadPoolExecutor; 50 | public static Context appContext; 51 | public static boolean shouldDisplayFirstLaunchDialog = false; 52 | 53 | @SuppressLint("StaticFieldLeak") 54 | public static Activity currentActivity = null; 55 | 56 | @Override 57 | protected void attachBaseContext(Context base) 58 | { 59 | super.attachBaseContext(base); 60 | 61 | this.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() 62 | { 63 | @Override 64 | public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) 65 | { 66 | } 67 | 68 | @Override 69 | public void onActivityStarted(@NonNull Activity activity) 70 | { 71 | currentActivity = activity; 72 | } 73 | 74 | @Override 75 | public void onActivityResumed(@NonNull Activity activity) 76 | { 77 | currentActivity = activity; 78 | } 79 | 80 | @Override 81 | public void onActivityPaused(@NonNull Activity activity) 82 | { 83 | if(currentActivity == activity) 84 | currentActivity = null; 85 | 86 | // NOTE: This happens even when switching between Main/Play activities 87 | // TODO: Maybe find a better place to save playlist ? (this will cost disk usage...) 88 | // (it's not as bad as it seems, we have kernel cache...) 89 | System.out.println("BLADE: onActivityPaused...."); 90 | if(MediaBrowserService.getInstance() != null) 91 | MediaBrowserService.getInstance().savePlaylist(); 92 | } 93 | 94 | @Override 95 | public void onActivityStopped(@NonNull Activity activity) 96 | { 97 | if(currentActivity == activity) 98 | currentActivity = null; 99 | } 100 | 101 | @Override 102 | public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) 103 | { 104 | } 105 | 106 | @Override 107 | public void onActivityDestroyed(@NonNull Activity activity) 108 | { 109 | if(currentActivity == activity) 110 | currentActivity = null; 111 | } 112 | }); 113 | 114 | //Restore theme from preferences 115 | String dark_theme = PreferenceManager.getDefaultSharedPreferences(base).getString("dark_theme", "system_default"); 116 | switch(dark_theme) 117 | { 118 | case "system_default": 119 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); 120 | break; 121 | case "dark_theme": 122 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); 123 | break; 124 | case "light_theme": 125 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); 126 | break; 127 | } 128 | 129 | //Provide static access to application context (eg. for 'Local' source, needing ContentProvider) 130 | appContext = base; 131 | 132 | //Load sources 133 | executorService.execute(() -> 134 | { 135 | Source.loadSourcesFromSave(); 136 | Source.initSources(); 137 | 138 | // Bind MediaBrowserService to application 139 | Intent serviceIntent = new Intent(this, MediaBrowserService.class); 140 | bindService(serviceIntent, new ServiceConnection() 141 | { 142 | @Override 143 | public void onServiceConnected(ComponentName name, IBinder service) 144 | { 145 | } 146 | 147 | @Override 148 | public void onServiceDisconnected(ComponentName name) 149 | { 150 | } 151 | }, 0); 152 | 153 | Library.loadFromCache(); 154 | 155 | if(Source.SOURCES.size() == 0) 156 | shouldDisplayFirstLaunchDialog = true; 157 | }); 158 | } 159 | 160 | public static ExecutorService obtainExecutorService() 161 | { 162 | return executorService; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/library/Album.java: -------------------------------------------------------------------------------- 1 | package v.blade.library; 2 | 3 | import com.squareup.picasso.Picasso; 4 | import com.squareup.picasso.RequestCreator; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class Album extends LibraryObject 10 | { 11 | Artist[] artists; 12 | List songList; 13 | String imageBigStr; 14 | RequestCreator imageBig; 15 | int imageLevel; 16 | 17 | public Album(String name, Artist[] artists, String imageMiniature, String imageBig, int imageLevel) 18 | { 19 | this.name = name; 20 | this.songList = new ArrayList<>(); 21 | this.artists = artists; 22 | setImage(imageMiniature, imageBig, imageLevel); 23 | } 24 | 25 | protected void addSong(Song s) 26 | { 27 | this.songList.add(s); 28 | } 29 | 30 | public Artist[] getArtists() 31 | { 32 | return artists; 33 | } 34 | 35 | public String getArtistsString() 36 | { 37 | StringBuilder sb = new StringBuilder(); 38 | for(int i = 0; i < artists.length; i++) 39 | { 40 | sb = sb.append(artists[i].name); 41 | if(i != artists.length - 1) sb = sb.append(", "); 42 | } 43 | return sb.toString(); 44 | } 45 | 46 | public List getSongs() 47 | { 48 | return songList; 49 | } 50 | 51 | public String getImageBigStr() 52 | { 53 | return imageBigStr; 54 | } 55 | 56 | public void setImage(String imageMiniature, String imageBig, int imageLevel) 57 | { 58 | if(this.imageLevel > imageLevel) return; 59 | 60 | this.imageStr = imageMiniature; 61 | this.imageBigStr = imageBig; 62 | this.imageLevel = imageLevel; 63 | this.imageBig = (imageBig == null || imageBig.equals("")) ? null : Picasso.get().load(imageBig); 64 | this.imageRequest = (imageMiniature == null || imageMiniature.equals("")) ? null : Picasso.get().load(imageMiniature); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/library/Artist.java: -------------------------------------------------------------------------------- 1 | package v.blade.library; 2 | 3 | import com.squareup.picasso.Picasso; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class Artist extends LibraryObject 9 | { 10 | List albums; 11 | int track_count; 12 | 13 | public Artist(String name, String image) 14 | { 15 | this.albums = new ArrayList<>(); 16 | this.name = name; 17 | this.imageRequest = (image == null || image.equals("")) ? null : Picasso.get().load(image); 18 | this.imageStr = image; 19 | this.track_count = 0; 20 | } 21 | 22 | protected void addAlbum(Album album) 23 | { 24 | this.albums.add(album); 25 | } 26 | 27 | public int getTrackCount() 28 | { 29 | return track_count; 30 | } 31 | 32 | public List getAlbums() 33 | { 34 | return albums; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/library/LibraryObject.java: -------------------------------------------------------------------------------- 1 | package v.blade.library; 2 | 3 | import com.squareup.picasso.RequestCreator; 4 | 5 | import java.util.ArrayList; 6 | 7 | import v.blade.sources.SourceInformation; 8 | 9 | public abstract class LibraryObject 10 | { 11 | protected String name; 12 | protected ArrayList sources; 13 | protected RequestCreator imageRequest = null; 14 | protected String imageStr = null; 15 | 16 | public String getName() 17 | { 18 | return name; 19 | } 20 | 21 | public RequestCreator getImageRequest() 22 | { 23 | return imageRequest; 24 | } 25 | 26 | public void setImageRequest(RequestCreator request) 27 | { 28 | this.imageRequest = request; 29 | } 30 | 31 | public String getImageStr() 32 | { 33 | return imageStr; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/library/Playlist.java: -------------------------------------------------------------------------------- 1 | package v.blade.library; 2 | 3 | import com.squareup.picasso.Picasso; 4 | 5 | import java.util.List; 6 | 7 | import v.blade.sources.SourceInformation; 8 | 9 | public class Playlist extends LibraryObject 10 | { 11 | final List songs; 12 | private final SourceInformation sourceInformation; 13 | private final String playlistSubtitle; 14 | 15 | public Playlist(String name, List songList, String image, String subtitle, SourceInformation sourceInformation) 16 | { 17 | this.name = name; 18 | this.imageStr = image; 19 | this.imageRequest = (image == null || image.equals("")) ? null : Picasso.get().load(image); 20 | this.songs = songList; 21 | this.sourceInformation = sourceInformation; 22 | this.playlistSubtitle = subtitle; 23 | } 24 | 25 | public SourceInformation getSource() 26 | { 27 | return sourceInformation; 28 | } 29 | 30 | public List getSongs() 31 | { 32 | return songs; 33 | } 34 | 35 | public String getSubtitle() 36 | { 37 | return playlistSubtitle; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/library/Separator.java: -------------------------------------------------------------------------------- 1 | package v.blade.library; 2 | 3 | public class Separator extends LibraryObject 4 | { 5 | public Separator(String name) 6 | { 7 | this.name = name; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/library/Song.java: -------------------------------------------------------------------------------- 1 | package v.blade.library; 2 | 3 | import com.squareup.picasso.RequestCreator; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import v.blade.sources.Source; 9 | import v.blade.sources.SourceInformation; 10 | 11 | public class Song extends LibraryObject 12 | { 13 | List sources; 14 | Artist[] artists; 15 | Album album; 16 | int track_number; 17 | 18 | protected Song(String name, Album album, Artist[] artists, int track_number) 19 | { 20 | this.name = name; 21 | this.artists = artists; 22 | this.album = album; 23 | this.track_number = track_number; 24 | this.sources = new ArrayList<>(); 25 | } 26 | 27 | protected void addSource(Source source, Object id, boolean handled) 28 | { 29 | if(source == null || id == null) return; 30 | 31 | //check if song contains same source 32 | for(SourceInformation si : sources) if(si.source == source) return; 33 | 34 | sources.add(new SourceInformation(source, id, handled)); 35 | } 36 | 37 | public Artist[] getArtists() 38 | { 39 | return artists; 40 | } 41 | 42 | public String getArtistsString() 43 | { 44 | StringBuilder sb = new StringBuilder(); 45 | for(int i = 0; i < artists.length; i++) 46 | { 47 | sb = sb.append(artists[i].name); 48 | if(i != artists.length - 1) sb = sb.append(", "); 49 | } 50 | return sb.toString(); 51 | } 52 | 53 | public SourceInformation getBestSource() 54 | { 55 | if(sources.size() == 0) return null; 56 | 57 | SourceInformation best = null; 58 | int min = Source.SOURCES.size(); 59 | for(int i = 0; i < sources.size(); i++) 60 | { 61 | if(sources.get(i).source.getIndex() < min 62 | && sources.get(i).source.getStatus() == Source.SourceStatus.STATUS_READY) 63 | { 64 | best = sources.get(i); 65 | min = best.source.getIndex(); 66 | } 67 | } 68 | return best; 69 | } 70 | 71 | public int getTrackNumber() 72 | { 73 | return track_number; 74 | } 75 | 76 | public Album getAlbum() 77 | { 78 | return album; 79 | } 80 | 81 | public List getSources() 82 | { 83 | return sources; 84 | } 85 | 86 | @Override 87 | public RequestCreator getImageRequest() 88 | { 89 | return album.imageRequest; 90 | } 91 | 92 | public RequestCreator getBigImageRequest() 93 | { 94 | return album.imageBig; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/sources/SourceInformation.java: -------------------------------------------------------------------------------- 1 | package v.blade.sources; 2 | 3 | public class SourceInformation 4 | { 5 | public Source source; 6 | public Object id; 7 | //Whether or not this is a 'handle' for the song, i.e. not in source library 8 | public boolean handled; 9 | 10 | public SourceInformation(Source source, Object id, boolean handled) 11 | { 12 | this.source = source; 13 | this.id = id; 14 | this.handled = handled; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/sources/deezer/DeezerPlayer.java: -------------------------------------------------------------------------------- 1 | package v.blade.sources.deezer; 2 | 3 | import v.blade.library.Song; 4 | import v.blade.sources.Source; 5 | 6 | public class DeezerPlayer extends Source.Player 7 | { 8 | @Override 9 | public void init() 10 | { 11 | 12 | } 13 | 14 | @Override 15 | public void play() 16 | { 17 | 18 | } 19 | 20 | @Override 21 | public void pause() 22 | { 23 | 24 | } 25 | 26 | @Override 27 | public void playSong(Song song) 28 | { 29 | 30 | } 31 | 32 | @Override 33 | public void seekTo(long millis) 34 | { 35 | 36 | } 37 | 38 | @Override 39 | public long getCurrentPosition() 40 | { 41 | return 0; 42 | } 43 | 44 | @Override 45 | public long getDuration() 46 | { 47 | return 0; 48 | } 49 | 50 | @Override 51 | public boolean isPaused() 52 | { 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/sources/deezer/DeezerSettingsFragment.java: -------------------------------------------------------------------------------- 1 | package v.blade.sources.deezer; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.fragment.app.Fragment; 10 | 11 | import v.blade.BladeApplication; 12 | import v.blade.R; 13 | import v.blade.databinding.SettingsFragmentDeezerBinding; 14 | import v.blade.sources.Source; 15 | 16 | public class DeezerSettingsFragment extends Fragment 17 | { 18 | private final Deezer deezer; 19 | private SettingsFragmentDeezerBinding binding; 20 | 21 | public DeezerSettingsFragment(Deezer deezer) 22 | { 23 | this.deezer = deezer; 24 | } 25 | 26 | public void refreshStatus() 27 | { 28 | Source.SourceStatus status = deezer.getStatus(); 29 | 30 | // Set status label 31 | switch(status) 32 | { 33 | case STATUS_DOWN: 34 | binding.settingsDeezerStatus.setText(R.string.source_down_desc); 35 | break; 36 | case STATUS_NEED_INIT: 37 | binding.settingsDeezerStatus.setText(R.string.source_need_init_desc); 38 | break; 39 | case STATUS_CONNECTING: 40 | binding.settingsDeezerStatus.setText(R.string.source_connecting_desc); 41 | break; 42 | case STATUS_READY: 43 | binding.settingsDeezerStatus.setText(R.string.source_ready_desc); 44 | break; 45 | } 46 | 47 | // Set account text 48 | if(status == Source.SourceStatus.STATUS_DOWN) 49 | { 50 | binding.settingsDeezerAccount.setText(R.string.disconnected); 51 | binding.settingsDeezerAccount.setTextColor(getResources().getColor(R.color.errorRed)); 52 | 53 | // Hide audio quality and force init buttons 54 | binding.settingsDeezerInit.setVisibility(View.GONE); 55 | } 56 | else 57 | { 58 | binding.settingsDeezerAccount.setText(deezer.account_name); 59 | binding.settingsDeezerAccount.setTextColor(getResources().getColor(R.color.okGreen)); 60 | 61 | // Hide login buttons (we are already connected) 62 | binding.settingsDeezerUser.setVisibility(View.GONE); 63 | binding.settingsDeezerPassword.setVisibility(View.GONE); 64 | binding.settingsDeezerSignIn.setVisibility(View.GONE); 65 | } 66 | } 67 | 68 | @Override 69 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 70 | { 71 | binding = SettingsFragmentDeezerBinding.inflate(inflater, container, false); 72 | 73 | refreshStatus(); 74 | 75 | // Set 'sign in' button action 76 | binding.settingsDeezerSignIn.setOnClickListener(v -> 77 | { 78 | // Set deezer parameters 79 | deezer.account_login = binding.settingsDeezerUser.getText().toString(); 80 | deezer.account_password = binding.settingsDeezerPassword.getText().toString(); 81 | 82 | deezer.setStatus(Source.SourceStatus.STATUS_NEED_INIT); 83 | deezer.initSource(); 84 | }); 85 | 86 | // Set 'force init' button action 87 | binding.settingsDeezerInit.setOnClickListener(v -> 88 | { 89 | //tODO CHANGE 90 | BladeApplication.obtainExecutorService().execute(deezer::synchronizeLibrary); 91 | //deezer.setStatus(Source.SourceStatus.STATUS_NEED_INIT); 92 | //deezer.initSource(); 93 | }); 94 | 95 | return binding.getRoot(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/sources/local/LocalPlayer.java: -------------------------------------------------------------------------------- 1 | package v.blade.sources.local; 2 | 3 | import android.content.ContentUris; 4 | import android.media.MediaPlayer; 5 | import android.net.Uri; 6 | import android.provider.MediaStore; 7 | 8 | import androidx.core.content.ContextCompat; 9 | 10 | import java.io.IOException; 11 | 12 | import v.blade.BladeApplication; 13 | import v.blade.library.Song; 14 | import v.blade.player.MediaBrowserService; 15 | import v.blade.sources.Source; 16 | import v.blade.sources.SourceInformation; 17 | 18 | public class LocalPlayer extends Source.Player 19 | { 20 | private final Local local; 21 | 22 | private final MediaPlayer mediaPlayer; 23 | 24 | protected LocalPlayer(Local local) 25 | { 26 | this.local = local; 27 | mediaPlayer = new MediaPlayer(); 28 | init(); 29 | } 30 | 31 | @Override 32 | public void init() 33 | { 34 | mediaPlayer.setOnCompletionListener(mp -> 35 | ContextCompat.getMainExecutor(MediaBrowserService.getInstance()) 36 | .execute(MediaBrowserService.getInstance()::notifyPlaybackEnd)); 37 | } 38 | 39 | @Override 40 | public void play() 41 | { 42 | mediaPlayer.start(); 43 | } 44 | 45 | @Override 46 | public void pause() 47 | { 48 | mediaPlayer.pause(); 49 | } 50 | 51 | @Override 52 | public void playSong(Song song) 53 | { 54 | //Obtain id 55 | SourceInformation current = null; 56 | for(SourceInformation si : song.getSources()) 57 | { 58 | if(si.source == local) 59 | { 60 | current = si; 61 | break; 62 | } 63 | } 64 | if(current == null) return; 65 | 66 | Uri songUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ((Number) current.id).longValue()); 67 | 68 | try 69 | { 70 | mediaPlayer.reset(); 71 | mediaPlayer.setDataSource(BladeApplication.appContext, songUri); 72 | mediaPlayer.prepare(); 73 | play(); 74 | } 75 | catch(IOException | RuntimeException e) 76 | { 77 | e.printStackTrace(); 78 | } 79 | } 80 | 81 | @Override 82 | public void seekTo(long millis) 83 | { 84 | mediaPlayer.seekTo((int) millis); 85 | } 86 | 87 | @Override 88 | public long getCurrentPosition() 89 | { 90 | return mediaPlayer.getCurrentPosition(); 91 | } 92 | 93 | @Override 94 | public long getDuration() 95 | { 96 | try 97 | { 98 | return mediaPlayer.getDuration(); 99 | } 100 | catch(IllegalStateException e) 101 | { 102 | return 0; 103 | } 104 | } 105 | 106 | @Override 107 | public boolean isPaused() 108 | { 109 | return !mediaPlayer.isPlaying(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/ui/ExploreFragment.java: -------------------------------------------------------------------------------- 1 | package v.blade.ui; 2 | 3 | import android.os.Bundle; 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 android.widget.Toast; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.fragment.app.Fragment; 13 | import androidx.recyclerview.widget.LinearLayoutManager; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import java.util.Stack; 17 | 18 | import v.blade.R; 19 | import v.blade.databinding.FragmentExploreBinding; 20 | import v.blade.sources.Source; 21 | 22 | public class ExploreFragment extends Fragment 23 | { 24 | private static class BackInformation 25 | { 26 | final RecyclerView.Adapter adapter; 27 | final String title; 28 | 29 | BackInformation(String title, RecyclerView.Adapter adapter) 30 | { 31 | this.adapter = adapter; 32 | this.title = title; 33 | } 34 | } 35 | 36 | public FragmentExploreBinding binding; 37 | private Stack backStack; 38 | public Source current; 39 | 40 | private class SourceAdapter extends RecyclerView.Adapter 41 | { 42 | class ViewHolder extends RecyclerView.ViewHolder 43 | { 44 | ImageView elementImage; 45 | TextView elementTitle; 46 | 47 | public ViewHolder(@NonNull View itemView) 48 | { 49 | super(itemView); 50 | 51 | elementImage = itemView.findViewById(R.id.item_element_image); 52 | elementTitle = itemView.findViewById(R.id.item_element_title); 53 | } 54 | } 55 | 56 | @NonNull 57 | @Override 58 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) 59 | { 60 | View view = LayoutInflater.from(parent.getContext()) 61 | .inflate(R.layout.item_layout, parent, false); 62 | ViewHolder viewHolder = new ViewHolder(view); 63 | 64 | view.setOnClickListener(v -> 65 | { 66 | int pos = binding.exploreSourcesListview.getChildAdapterPosition(v); 67 | Source current = Source.SOURCES.get(pos); 68 | 69 | ExploreFragment.this.current = current; 70 | current.explore(ExploreFragment.this); 71 | }); 72 | 73 | return viewHolder; 74 | } 75 | 76 | @Override 77 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) 78 | { 79 | Source current = Source.SOURCES.get(position); 80 | holder.elementTitle.setText(current.getName()); 81 | holder.elementImage.setImageResource(current.getImageResource()); 82 | } 83 | 84 | @Override 85 | public int getItemCount() 86 | { 87 | return Source.SOURCES.size(); 88 | } 89 | } 90 | 91 | public String getTitle() 92 | { 93 | return ((MainActivity) requireActivity()).binding == null ? "" : ((MainActivity) requireActivity()).binding.appBarMain.toolbar.getTitle().toString(); 94 | } 95 | 96 | @Override 97 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 98 | Bundle savedInstanceState) 99 | { 100 | binding = FragmentExploreBinding.inflate(inflater, container, false); 101 | 102 | binding.exploreSourcesListview.setLayoutManager(new LinearLayoutManager(requireContext())); 103 | binding.exploreSourcesListview.setAdapter(new SourceAdapter()); 104 | 105 | current = null; 106 | backStack = new Stack<>(); 107 | 108 | return binding.getRoot(); 109 | } 110 | 111 | boolean lastSearched = false; 112 | 113 | protected void onSearch(String query) 114 | { 115 | if(current == null) 116 | { 117 | Toast.makeText(requireContext(), getString(R.string.cant_search_here), Toast.LENGTH_SHORT).show(); 118 | return; 119 | } 120 | else if(lastSearched) 121 | { 122 | //TODO : this temp fixes '2 intent receiving' ; find a better way 123 | lastSearched = false; 124 | return; 125 | } 126 | 127 | current.exploreSearch(query, this); 128 | lastSearched = true; 129 | } 130 | 131 | public void updateContent(RecyclerView.Adapter adapter, String title, boolean shouldSaveBackInformation) 132 | { 133 | if(shouldSaveBackInformation) 134 | backStack.push(new BackInformation(getTitle(), binding.exploreSourcesListview.getAdapter())); 135 | 136 | binding.exploreSourcesListview.setAdapter(adapter); 137 | if(((MainActivity) requireActivity()).binding != null) 138 | ((MainActivity) requireActivity()).binding.appBarMain.toolbar.setTitle(title); 139 | } 140 | 141 | private void updateContent(BackInformation backInformation) 142 | { 143 | updateContent(backInformation.adapter, backInformation.title, false); 144 | } 145 | 146 | public void onBackPressed() 147 | { 148 | if(backStack.empty()) 149 | { 150 | requireActivity().finish(); 151 | return; 152 | } 153 | 154 | updateContent(backStack.pop()); 155 | 156 | //if we went back to root, current is null 157 | if(backStack.empty()) current = null; 158 | } 159 | } -------------------------------------------------------------------------------- /app/src/main/java/v/blade/ui/GeniusService.java: -------------------------------------------------------------------------------- 1 | package v.blade.ui; 2 | 3 | import retrofit2.Call; 4 | import retrofit2.http.GET; 5 | import retrofit2.http.Header; 6 | import retrofit2.http.Query; 7 | 8 | public interface GeniusService 9 | { 10 | class SearchResultObject 11 | { 12 | int annotation_count; 13 | String api_path; 14 | String artist_names; 15 | String full_title; 16 | long id; 17 | String lyrics_state; 18 | String title; 19 | String title_with_featured; 20 | String path; 21 | } 22 | 23 | class SearchHitObject 24 | { 25 | String index; 26 | String type; 27 | SearchResultObject result; 28 | } 29 | 30 | class SearchResponse 31 | { 32 | SearchHitObject[] hits; 33 | } 34 | 35 | class MetaObject 36 | { 37 | int status; 38 | } 39 | 40 | class SearchApiResponse 41 | { 42 | MetaObject meta; 43 | SearchResponse response; 44 | } 45 | 46 | 47 | @GET("search") 48 | Call search(@Header("Authorization") String authorization, @Query("q") String query); 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/v/blade/ui/LyricsActivity.java: -------------------------------------------------------------------------------- 1 | package v.blade.ui; 2 | 3 | import android.os.Bundle; 4 | import android.webkit.WebView; 5 | 6 | import androidx.appcompat.app.AppCompatActivity; 7 | 8 | import java.io.IOException; 9 | 10 | import retrofit2.Call; 11 | import retrofit2.Response; 12 | import retrofit2.Retrofit; 13 | import retrofit2.converter.gson.GsonConverterFactory; 14 | import v.blade.BladeApplication; 15 | import v.blade.R; 16 | import v.blade.library.Song; 17 | import v.blade.player.MediaBrowserService; 18 | 19 | public class LyricsActivity extends AppCompatActivity 20 | { 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) 23 | { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.activity_lyrics); 26 | 27 | WebView webView = findViewById(R.id.lyrics_webview); 28 | 29 | Song song = MediaBrowserService.getInstance().getPlaylist().get(MediaBrowserService.getInstance().getIndex()); 30 | BladeApplication.obtainExecutorService().execute(() -> 31 | { 32 | Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.genius.com") 33 | .addConverterFactory(GsonConverterFactory.create()).build(); 34 | GeniusService service = retrofit.create(GeniusService.class); 35 | 36 | Call search = 37 | service.search("Bearer wTGF45NZElaOrhC1LIEhdBq9ISwX7SgNLBkp_74fjUo-uwUJNrENnCJ2Uj4tJeVo", 38 | song.getName() + " " + song.getArtists()[0].getName()); 39 | 40 | 41 | try 42 | { 43 | Response response = search.execute(); 44 | 45 | if(response.code() != 200) 46 | { 47 | return; 48 | } 49 | 50 | GeniusService.SearchApiResponse r = response.body(); 51 | if(r == null || r.response.hits.length == 0) 52 | { 53 | return; 54 | } 55 | 56 | runOnUiThread(() -> 57 | webView.loadUrl("https://genius.com" + r.response.hits[0].result.path)); 58 | } 59 | catch(IOException e) 60 | { 61 | e.printStackTrace(); 62 | } 63 | }); 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/v/blade/ui/TouchHelperCallback.java: -------------------------------------------------------------------------------- 1 | package v.blade.ui; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.recyclerview.widget.ItemTouchHelper; 5 | import androidx.recyclerview.widget.RecyclerView; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | @SuppressWarnings("rawtypes") 11 | public class TouchHelperCallback extends ItemTouchHelper.Callback 12 | { 13 | private Object[] toOrderObject; 14 | private List toOrderList; 15 | 16 | public TouchHelperCallback(Object[] toOrder) 17 | { 18 | this.toOrderObject = toOrder; 19 | } 20 | 21 | public TouchHelperCallback(List toOrder) 22 | { 23 | this.toOrderList = toOrder; 24 | } 25 | 26 | @Override 27 | public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) 28 | { 29 | int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; 30 | return makeMovementFlags(dragFlags, 0); 31 | } 32 | 33 | @Override 34 | public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) 35 | { 36 | if(toOrderObject == null && toOrderList == null) return false; 37 | if(recyclerView.getAdapter() == null) return false; 38 | 39 | int from = viewHolder.getAdapterPosition(); 40 | int to = target.getAdapterPosition(); 41 | 42 | if(toOrderObject != null) 43 | { 44 | Object toMove = toOrderObject[from]; 45 | if(to - from >= 0) 46 | System.arraycopy(toOrderObject, from + 1, toOrderObject, from, to - from); 47 | else if(from - to >= 0) 48 | System.arraycopy(toOrderObject, to, toOrderObject, to + 1, from - to); 49 | toOrderObject[to] = toMove; 50 | } 51 | else Collections.swap(toOrderList, from, to); 52 | 53 | recyclerView.getAdapter().notifyItemMoved(from, to); 54 | return true; 55 | } 56 | 57 | @Override 58 | public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) 59 | { 60 | if(toOrderObject == null && toOrderList == null) return; 61 | 62 | int position = viewHolder.getAdapterPosition(); 63 | if(toOrderObject != null) 64 | { 65 | System.arraycopy(toOrderObject, position + 1, toOrderObject, position, toOrderObject.length - position); 66 | toOrderObject[toOrderObject.length - 1] = null; 67 | } 68 | else toOrderList.remove(position); 69 | 70 | //NOTE: Adapter still needs to be notified ; this method needs to be overloaded 71 | } 72 | 73 | @Override 74 | public boolean isLongPressDragEnabled() 75 | { 76 | //NOTE : we disable long press drag; i find that not intuitive, and it is bugged anyway 77 | return false; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_pause_notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_play_notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_skip_next_notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_skip_previous_notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_album_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-hdpi/ic_album_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_blade_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-hdpi/ic_blade_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_pause_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-hdpi/ic_pause_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_play_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-hdpi/ic_play_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_skip_next_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-hdpi/ic_skip_next_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_skip_previous_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-hdpi/ic_skip_previous_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_album_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-mdpi/ic_album_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_blade_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-mdpi/ic_blade_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_pause_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-mdpi/ic_pause_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_play_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-mdpi/ic_play_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_skip_next_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-mdpi/ic_skip_next_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_skip_previous_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-mdpi/ic_skip_previous_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_album_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xhdpi/ic_album_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_blade_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xhdpi/ic_blade_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_pause_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xhdpi/ic_pause_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_play_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xhdpi/ic_play_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_skip_next_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xhdpi/ic_skip_next_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_skip_previous_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xhdpi/ic_skip_previous_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_album_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxhdpi/ic_album_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_blade_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxhdpi/ic_blade_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_pause_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxhdpi/ic_pause_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_play_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxhdpi/ic_play_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_skip_next_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxhdpi/ic_skip_next_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_skip_previous_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxhdpi/ic_skip_previous_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_album_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxxhdpi/ic_album_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_blade_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/drawable-xxxhdpi/ic_blade_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/fastscroll_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fastscroll_track.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_album.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_album_artist_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_artist.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dark_theme.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_deezer.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 12 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 61 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 78 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 98 | 101 | 102 | 103 | 104 | 105 | 106 | 112 | 115 | 118 | 119 | 120 | 121 | 122 | 123 | 129 | 132 | 135 | 136 | 137 | 138 | 139 | 140 | 146 | 149 | 152 | 153 | 154 | 155 | 156 | 157 | 163 | 166 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_explore.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_hourglass.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_local.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_vert.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_playlist.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_playlist_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reorder_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repeat.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repeat_one.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shuffle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_next.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_previous.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_song.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sources_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spotify.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spotify_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sync_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/track.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_lyrics.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_bar_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 27 | 28 | 34 | 35 | 47 | 48 | 59 | 60 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_create_playlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_explore.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_label_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 26 | 27 | 37 | 38 | 49 | 50 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_simple_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_activity.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_fragment_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 27 | 28 | 34 | 35 | 42 | 43 | 49 | 50 | 56 | 57 | 63 | 64 | 70 | 71 | 79 | 80 | 88 | 89 | 97 | 98 | 106 | 107 | 115 | 116 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_fragment_deezer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 18 | 19 | 24 | 25 | 30 | 31 | 38 | 39 | 46 | 47 | 54 | 55 | 63 | 64 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_fragment_local.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 16 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_fragment_sources.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_fragment_spotify.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | 29 | 36 | 37 | 44 | 45 | 52 | 53 | 59 | 60 | 68 | 69 | 73 | 74 | 79 | 80 | 81 | 82 | 90 | 91 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /app/src/main/res/menu/activity_main_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/menu/currentplay_more.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/item_more.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 16 | 20 | 24 | 28 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 15 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhaudiquet/BladePlayer/d8762be6b8c2d3737630e3fc26393b9638a3b130/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/mobile_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 18 | 23 | 28 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Blade Player 3 | 4 | Otwórz panel nawigacji 5 | Zamknij panel nawigacji 6 | 7 | Zsynchronizuj 8 | Ustawienia 9 | 10 | Biblioteka 11 | Artyści 12 | Albumy 13 | Piosenki 14 | Playlisty 15 | 16 | Źródła 17 | Informacje 18 | 19 | Dodaj źródło… 20 | 21 | Spotify 22 | Odtwarzaj muzykę z konta Spotify 23 | Lokalne 24 | Odtwarzaj muzykę z plików na telefonie 25 | Deezer 26 | Odtwarzaj muzykę z konta Deezer 27 | 28 | Zdjęcie elementu 29 | Więcej… 30 | 31 | Niedostępne - Proszę, sprawdź konfigurację 32 | Potrzebna inicjalizacja - Wymuś lub sprawdź konfigurację 33 | Łączenie… 34 | Gotowe 35 | 36 | Zaloguj się 37 | Rozłączono 38 | Problem uwierzytelniania 39 | Nie udało się zainicjować źródła 40 | Wymuś inicjalizację 41 | Usuń 42 | Wersja 43 | Blade Player to Androidowy odtwarzacz muzyki (stworzony przez @vhaudiquet), aby odtwarzać muzykę z wielu źródeł. 44 | Licencja : Apache 2.0 45 | Logo: @zularizal 46 | Źródło: github.com/vhaudiquet/BladePlayer 47 | Użyte biblioteki: 48 | Nazwa użytkownika 49 | Hasło 50 | (Może być konieczne ponowne wprowadzenie danych uwierzytelniających, dla API i odtwarzacza) 51 | Proszę zsynchronizuj bibliotekę, aby zaaplikować zmiany 52 | Graj 53 | Zatrzymaj 54 | Dalej 55 | Cofnij 56 | Odtwarzanie multimediów 57 | Powiadomienia i sterowanie multimediami 58 | Obecnie gra 59 | Odtwórz po tej piosence 60 | Dodaj do aktualnej playlisty 61 | Losowo 62 | Powtórz 63 | Ta piosenka nie ma gotowego źródła. Coś musi być nie tak z konfiguracją… 64 | Szukaj 65 | Dodaj do playlisty… 66 | Zarządzaj bibliotekami 67 | Usuń z playlisty 68 | Dodano %1$s do listy %2$s 69 | Nie można dodać %1$s do listy %2$s 70 | Nowa playlista 71 | Nazwa 72 | Nie można utworzyć playlisty %1$s 73 | Jesteś pewien, że chcesz usunąć playlistę %1$s ? 74 | Tak 75 | Nie 76 | Usunięto playlistę %1$s 77 | Usunięto playlistę 78 | Nie można było usunąć playlisty %1$s 79 | Piosenka %1$s została dodana do biblioteki 80 | Piosenka %1$s nie mogła zostać dodana do biblioteki 81 | Piosenka %1$s została usunięta z biblioteki 82 | Piosenka %1$s nie mogła zostać usunięta z biblioteki 83 | Jesteś pewien, że chcesz usunąć %1$s z playlisty %2$s ? 84 | %1$s usunięto z playlisty %2$s 85 | %1$s nie można było usunąć z playlisty %2$s 86 | Błąd odtwarzacza Spotify, coś musi być nie tak (internet/konfiguracja?) 87 | Udziel uprawnień dostępu do pamięci masowej 88 | Uprawnienia już udzielone 89 | Uprawnienia udzielone! 90 | Współpraca 91 | %1$s odtwarzacz nie jest gotowy 92 | Witamy w Blade Player! 93 | Wygląda na to, że nie skonfigurowałeś żadnych \'źródeł\' muzyki. Proszę, wejdź w ustawienia (ikona w prawym górnym rogu), \'Źródła\', dodaj źródło używając przycisku + i kliknij na nie, aby je skonfigurować. Po zakończeniu, wróć, naciśnij ikonę \'synchronizacji\' obok ustawień i czekaj na synchronizację. Potem delektuj się swoją muzyką! 94 | Czarny motyw 95 | Jasny motyw 96 | Motyw systemowy 97 | Odkryj 98 | Odkryj źródła 99 | Nie ma czego tutaj szukać 100 | Nie udało się szukać %1$s 101 | Nie można przeglądać albumu %1$s 102 | Nie można przeglądać artysty %1$s 103 | Nie można wyświetlić zakładki Odkryj 104 | Nie można przeglądać playlisty %1$s 105 | Zaznaczenie jest za duże 106 | Tekst 107 | Jakość dźwięku 108 | Normalna 109 | Wysoka 110 | Bardzo wysoka 111 | Pokaż tylko twórców albumu 112 | Ukrywaj artystów, którzy nie są twórcami albumów w bibliotece 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | Blade Player 4 | Gezinti çubuğunu aç 5 | Gezinti çubuğunu kapat 6 | Senkronize et 7 | Ayarlar 8 | Kitaplık 9 | Sanatçılar 10 | Albümler 11 | Şarkılar 12 | Çalma Listeleri 13 | Kaynaklar 14 | Hakkında 15 | Kaynak ekle… 16 | Spotify 17 | Spotify hesabından müzik çal 18 | Yerel 19 | Telefonundaki dosyalardan müzik çal 20 | Deezer 21 | Deezer hesabından müzik çal 22 | Ürün resmi 23 | Daha… 24 | Aşağı - Lütfen yapılandırmayı kontrol edin 25 | Başlatma gerek - Ya zorlayın ya da yapılandırmayı kontrol edin 26 | Bağlanıyor… 27 | Hazır 28 | Giriş yap 29 | Bağlantı kesildi 30 | Doğrulama hatası 31 | Kaynak başlatılamadı 32 | Başlatmaya zorla 33 | Kaldır 34 | Sürüm 35 | Blade Player bir Android müzik oynatıcısıdır (@vhaudiquet tarafından) birden fazla hizmetten şarkı çalmak için yapılmıştır. 36 | Lisans : Apache 2.0 37 | Logo : @zularizal 38 | Kaynaklar : github.com/vhaudiquet/BladePlayer 39 | Kullanılan kütüphaneler : 40 | Kullanıcı adı 41 | Parola 42 | (API ve Oynatıcı için kimlik bilgilerinizi tekrar girmeniz gerekebilir) 43 | Değişikliklerinizi uygulamak için lütfen kitaplığı senkronize edin 44 | Oynat 45 | Durdur 46 | Sonrakine atla 47 | 48 | Öncekine atla 49 | Medya oynatma 50 | Medya bildirimi ve kontroller 51 | Şu anda çalıyor 52 | Geçerli şarkıdan sonra çal 53 | Geçerli çalma listesine ekle 54 | Karıştır 55 | Tekrarla 56 | Bu şarkının hazır bir kaynağı yok. Kaynak yapılandırmasında bir sorun olmalı… 57 | Ara 58 | Çalma listesine ekle… 59 | Kitaplıkları yönet 60 | Çalma listesinden kaldır 61 | %2$s listesine %1$s eklendi 62 | %2$s listesine %1$s eklenemedi 63 | Yeni çalma listesi 64 | İsim 65 | %1$s çalma listesi oluşturulamadı 66 | %1$s çalma listesini silmek istediğinizden emin misiniz? 67 | Evet 68 | Hayır 69 | %1$s çalma listesi kaldırıldı 70 | Çalma listesini sil 71 | %1$s çalma listesi kaldırılamadı 72 | %1$s şarkısı kitaplığa eklendi 73 | %1$s şarkısı kitaplığa eklenemedi 74 | 75 | %1$s şarkısı kitaplıktan kaldırıldı 76 | 77 | %1$s şarkısı kitaplıktan kaldırılamadı 78 | %1$s şarkısını %2$s çalma listesinden kaldırmak istediğinizden emin misiniz? 79 | 80 | %1$s, %2$s çalma listesinden kaldırıldı 81 | %1$s, %2$s çalma listesinden kaldırılamadı 82 | Spotify oynatıcı hatası, bir sorun olmalı (internet/ayarlar ?) 83 | Depolama alanına erişim izni verin 84 | İzin zaten verildi 85 | 86 | İzin verildi ! 87 | 88 | Ortak 89 | %1$s oynatıcısı hazır değildi 90 | 91 | Blade Player\'a hoş geldiniz! 92 | Görünüşe göre herhangi bir müzik "kaynağı" yapılandırmamışsınız (Blade\'in müziğinizi alacağı hizmet). Lütfen ayarlara gidin (sağ üstteki simgeyi kullanarak), \'Kaynaklar\', + düğmesini kullanarak bir kaynak ekleyin ve yapılandırmak için üzerine tıklayın. İşiniz bittiğinde geri dönün, ayarların yanındaki \'senkronizasyon\' simgesine basın ve senkronizasyonu bekleyin. Her şey hazır, müziğinizi dinleyin! 93 | 94 | Koyu tema 95 | Açık tema 96 | Sistem varsayılanı 97 | Keşfet 98 | Kaynakları keşfet 99 | Burada aranacak bir şey yok 100 | %1$s aranamadı 101 | 102 | %1$s albümüne göz atılamadı 103 | 104 | %1$s sanatçısına göz atılamadı 105 | Keşfet sekmesi gösterilemedi 106 | %1$s çalma listesine göz atılamadı 107 | Seçim çok büyük 108 | Sözler 109 | Ses kalitesi 110 | Normal 111 | Yüksek 112 | Çok yüksek 113 | Yalnızca albüm sanatçılarını göster 114 | Yalnızca kitaplığınızdaki diğer sanatçıların albümlerinde yer alan sanatçıları gizleyin 115 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Blade 播放器 3 | 4 | 打开导航栏 5 | 关闭导航栏 6 | 7 | 同步 8 | 设置 9 | 10 | 11 | 歌手 12 | 专辑 13 | 歌曲 14 | 播放列表 15 | 16 | 来源 17 | 关于 18 | 19 | 添加源… 20 | 21 | Spotify 22 | 播放来自 Spotify 账户的音乐 23 | 本地 24 | 播放手机上存储的音乐 25 | 26 | Deezer 27 | 播放来自 Deezer 账户的音乐 28 | 项目图片 29 | 更多… 30 | 31 | 已离线 - 请检查配置文件 32 | 需初始化 - 强制或检查配置 33 | 连接中… 34 | Ready 35 | 36 | 登入 37 | 已断开 38 | 身份验证错误 39 | 初始化失败 40 | 强制初始化 41 | 删除 42 | 版本 43 | Blade Player 是一个安卓音乐播放器(由 @vhaudiquet 制作),用于播放来自多个服务的歌曲。 44 | 许可证 : Apache 2.0 45 | 图标 : @zularizal 46 | 源码 : github.com/vhaudiquet/BladePlayer 47 | 使用的库 : 48 | 用户名 49 | 密码 50 | (当使用 API 或播放器时,你也许需要重新登录) 51 | 请同步库以应用您的更改 52 | 播放 53 | 暂停 54 | 下一个 55 | 上一个 56 | 媒体播放 57 | 媒体通知与控制 58 | 当前播放 59 | 下一首播放 60 | 加入当前播放列表 61 | Shuffle 62 | 单曲循环 63 | 这首歌没有任何可用来源,难道是来源配置出了点问题? 64 | 搜索 65 | 加入播放列表… 66 | 管理库 67 | 从播放列表移除 68 | 已添加 %1$s 至 %2$s 69 | 无法将 %1$s 添加至 %2$s 70 | 新建播放列表 71 | 名字 72 | 无法创建 %1$s 73 | 你确定要删除 %1$s 吗? 74 | 好的 75 | 不要 76 | 已删除 %1$s 77 | 删除播放列表 78 | 无法删除 %1$s 79 | %1$s 已加入库 80 | %1$s 无法被加入库 81 | %1$s 已从库中移除 82 | %1$s 无法从库中移除 83 | 你确定要将 %1$s 从 %2$s 中删除吗? 84 | %1$s 已从 %2$s 中移除 85 | %1$s 无法从 %2$s 中移除 86 | Spotify 播放出错,难道 互联网 / 设置 出了问题? 87 | 授予存储空间权限 88 | 权限已授予 89 | 拿到权限啦! 90 | Collaborative 91 | %1$s 播放器还没准备好呢 92 | 欢迎使用 Blade 播放器! 93 | 看来你没有配置任何音乐来源(Blade 将播放其中的音乐)。请进入设置(右上角图标),来源,使用 + 按钮添加一个来源,并配置它。完成后回到主页,点击设置旁边的 "同步" 图标,等待片刻,享受你的音乐吧! 94 | 深色模式 95 | 亮色模式 96 | 系统默认 97 | 探索 98 | 探索来源 99 | 这里…好像什么都没有? 100 | 无法搜索 %1$s 101 | 无法查看专辑 %1$s 102 | 无法查看歌手 %1$s 103 | 无法显示探索页 104 | 无法查看播放列表 %1$s 105 | 你选的太多啦 106 | 歌词 107 | 音质 108 | 普通的 109 | 高的 110 | 很高 111 | 仅显示专辑艺术家 112 | 隐藏仅出现在您资料库中其他艺术家专辑中的艺术家 113 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Blade 播放器 3 | 4 | 開啟導航欄 5 | 關閉導航欄 6 | 7 | 同步 8 | 設定 9 | 10 | 11 | 歌手 12 | 專輯 13 | 歌曲 14 | 播放列表 15 | 16 | 來源 17 | 關於 18 | 19 | 新增源… 20 | 21 | Spotify 22 | 播放來自 Spotify 賬戶的音樂 23 | 本地 24 | 播放手機上儲存的音樂 25 | 26 | Deezer 27 | 播放來自 Deezer 賬戶的音樂 28 | 專案圖片 29 | 更多… 30 | 31 | 已離線 - 請檢查配置檔案 32 | 需初始化 - 強制或檢查配置 33 | 連線中… 34 | Ready 35 | 36 | 登入 37 | 已斷開 38 | 身份驗證錯誤 39 | 初始化失敗 40 | 強制初始化 41 | 刪除 42 | 版本 43 | Blade Player 是一個安卓音樂播放器(由 @vhaudiquet 製作),用於播放來自多個服務的歌曲。 44 | 許可證 : Apache 2.0 45 | 圖示 : @zularizal 46 | 原始碼 : github.com/vhaudiquet/BladePlayer 47 | 使用的庫 : 48 | 使用者名稱 49 | 密碼 50 | (當使用 API 或播放器時,你也許需要重新登入) 51 | 請同步庫以應用您的更改 52 | 播放 53 | 暫停 54 | 下一個 55 | 上一個 56 | 媒體播放 57 | 媒體通知與控制 58 | 當前播放 59 | 下一首播放 60 | 加入當前播放列表 61 | Shuffle 62 | 單曲迴圈 63 | 這首歌沒有任何可用來源,難道是來源配置出了點問題? 64 | 搜尋 65 | 加入播放列表… 66 | 管理庫 67 | 從播放列表移除 68 | 已新增 %1$s 至 %2$s 69 | 無法將 %1$s 新增至 %2$s 70 | 新建播放列表 71 | 名字 72 | 無法建立 %1$s 73 | 你確定要刪除 %1$s 嗎? 74 | 好的 75 | 不要 76 | 已刪除 %1$s 77 | 刪除播放列表 78 | 無法刪除 %1$s 79 | %1$s 已加入庫 80 | %1$s 無法被加入庫 81 | %1$s 已從庫中移除 82 | %1$s 無法從庫中移除 83 | 你確定要將 %1$s 從 %2$s 中刪除嗎? 84 | %1$s 已從 %2$s 中移除 85 | %1$s 無法從 %2$s 中移除 86 | Spotify 播放出錯,難道 網際網路 / 設定 出了問題? 87 | 授予儲存空間許可權 88 | 許可權已授予 89 | 拿到許可權啦! 90 | Collaborative 91 | %1$s 播放器還沒準備好呢 92 | 歡迎使用 Blade 播放器! 93 | 看來你沒有配置任何音樂來源(Blade 將播放其中的音樂)。請進入設定(右上角圖示),來源,使用 + 按鈕新增一個來源,並配置它。完成後回到主頁,點選設定旁邊的 "同步" 圖示,等待片刻,享受你的音樂吧! 94 | 深色模式 95 | 亮色模式 96 | 系統預設 97 | 探索 98 | 探索來源 99 | 這裡…好像什麼都沒有? 100 | 無法搜尋 %1$s 101 | 無法檢視專輯 %1$s 102 | 無法檢視歌手 %1$s 103 | 無法顯示探索頁 104 | 無法檢視播放列表 %1$s 105 | 你選的太多啦 106 | 歌詞 107 | 音质 108 | 普通的 109 | 高的 110 | 很高 111 | 仅显示专辑艺术家 112 | 隐藏仅出现在您资料库中其他艺术家专辑中的艺术家 113 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/light_theme 5 | @string/dark_theme 6 | @string/system_default 7 | 8 | 9 | light_theme 10 | dark_theme 11 | system_default 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF2C3D4F 4 | #FF616161 5 | #FF212121 6 | #FFbdbdbd 7 | #FFe0e0e0 8 | #FFffffff 9 | #FF000000 10 | #FF424242 11 | #FFf44336 12 | #FF76ff03 13 | #FFb3e5fc 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 12dp 6 | 120dp 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2C3D4F 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/spotify_audio_quality.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/quality_normal 5 | @string/quality_high 6 | @string/quality_very_high 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Blade Player 3 | 4 | Open navigation drawer 5 | Close navigation drawer 6 | 7 | Synchronize 8 | Settings 9 | 10 | Library 11 | Artists 12 | Albums 13 | Songs 14 | Playlists 15 | 16 | Sources 17 | About 18 | 19 | Add source… 20 | 21 | Spotify 22 | Play music from a Spotify account 23 | Local 24 | Play music from files on your phone 25 | Deezer 26 | Play music from a Deezer account 27 | 28 | Item image 29 | More… 30 | 31 | Down - Please check configuration 32 | Need initialization - Either force or check config 33 | Connecting… 34 | Ready 35 | 36 | Sign in 37 | Disconnected 38 | Authentication error 39 | Could not init source 40 | Force initialization 41 | Remove 42 | Version 43 | Blade Player is an Android music player (by @vhaudiquet) made to play songs from multiple services. 44 | License : Apache 2.0 45 | Logo : @zularizal 46 | Sources : github.com/vhaudiquet/BladePlayer 47 | Libraries used : 48 | Username 49 | Password 50 | (You might need to enter your credentials again, for API and Player) 51 | Please sync library to apply your changes 52 | Play 53 | Pause 54 | Skip to next 55 | Skip to previous 56 | Media playback 57 | Media notification and controls 58 | Currently playing 59 | Play after current song 60 | Add to current playlist 61 | Shuffle 62 | Repeat 63 | Build type 64 | This song has no ready source. Something must be wrong in sources configuration… 65 | Search 66 | Add to playlist… 67 | Manage libraries 68 | Remove from playlist 69 | Added %1$s to list %2$s 70 | Could not add %1$s to list %2$s 71 | New playlist 72 | Name 73 | OK 74 | Could not create playlist %1$s 75 | Are you sure that you want to delete playlist %1$s ? 76 | Yes 77 | No 78 | Removed playlist %1$s 79 | Delete playlist 80 | Could not remove playlist %1$s 81 | Song %1$s added to library 82 | Song %1$s could not be added to library 83 | Song %1$s removed from library 84 | Song %1$s could not be removed from library 85 | Are you sure that you want to remove %1$s from playlist %2$s ? 86 | %1$s removed from playlist %2$s 87 | %1$s could not be removed from playlist %2$s 88 | Spotify player error, something must be wrong (internet/settings ?) 89 | Grant permission to access storage 90 | Permission already granted 91 | Permission granted ! 92 | Collaborative 93 | %1$s player was not ready 94 | Welcome to Blade player ! 95 | It seems like you did not configure any \'source\' of music (service from which Blade will get your music). Please go to settings (using the top right icon), \'Sources\', add a source using the + button, and click on it to configure it. When you\'re done, go back, hit the \'sync\' icon next to settings, and wait for synchronization. You\'re all set, listen to your music ! 96 | Dark theme 97 | Light theme 98 | System default 99 | Explore 100 | Explore sources 101 | There is nothing to search here 102 | Could not search for %1$s 103 | Could not browse album %1$s 104 | Could not browse artist %1$s 105 | Could not show explore tab 106 | Could not browse playlist %1$s 107 | Selection is too big 108 | Lyrics 109 | Audio quality 110 | Normal 111 | High 112 | Very high 113 | Show album artists only 114 | Hide artists that are only featured in other artists albums in your library 115 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 25 | 26 |