Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a17e91690 | ||
|
|
1036829186 | ||
|
|
becfc1d589 | ||
|
|
44bf346332 | ||
|
|
896e5fb3bd | ||
|
|
3086a8b9f9 | ||
|
|
10c2172be0 | ||
|
|
918bf6928e | ||
|
|
c9cf86acb5 | ||
|
|
0487f3bb9b | ||
|
|
c7f2524085 | ||
|
|
88c2129cd4 | ||
|
|
aa5d0f92db | ||
|
|
3ba2255205 | ||
|
|
145bb82eb0 | ||
|
|
932d1aaa8c | ||
|
|
4f8212d491 | ||
|
|
b403d69982 | ||
|
|
a49f2b97a2 | ||
|
|
c44e60c0e5 | ||
|
|
4cd15b4284 | ||
|
|
72d7aea6e3 | ||
|
|
9adaf8c013 | ||
|
|
661346ca3a | ||
|
|
dbd32baa12 | ||
|
|
3958cbcc1c | ||
|
|
fb568d1d74 | ||
|
|
e06a168350 | ||
|
|
b8dc985279 | ||
|
|
090701b92b | ||
|
|
7767a66fb8 | ||
|
|
d1122bef4e | ||
|
|
72d4495582 | ||
|
|
499644d041 |
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,8 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## Pending release
|
||||
## What's Changed
|
||||
## [4.12.6](https://github.com/eddyizm/tempo/releases/tag/v4.12.6) (2026-03-06)
|
||||
* doc: update USAGE with android auto configuration by @MaFo-28 in https://github.com/eddyizm/tempus/pull/481
|
||||
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/483
|
||||
* fix: remove material you dynamic theming by @tvillega in https://github.com/eddyizm/tempus/pull/484
|
||||
* fix: collapse sheet on navitation change by @tvillega in https://github.com/eddyizm/tempus/pull/482
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.4...v4.12.5
|
||||
|
||||
## What's Changed
|
||||
## [4.12.4](https://github.com/eddyizm/tempo/releases/tag/v4.12.4) (2026-03-01)
|
||||
* feat: advertise existing long press to refresh per section on library page by @tvillega in https://github.com/eddyizm/tempus/pull/467
|
||||
* fix: playlist filter returns properly filtered list and reset correctly by @eddyizm in https://github.com/eddyizm/tempus/pull/476
|
||||
* feat: toggle player bitrate visibility on touch by @tvillega in https://github.com/eddyizm/tempus/pull/466
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.0...v4.12.3
|
||||
|
||||
## What's Changed
|
||||
## [4.12.0](https://github.com/eddyizm/tempo/releases/tag/v4.12.0) (2026-02-28)
|
||||
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/441
|
||||
* feat: radio logos support for AndroidAuto by @dmachard in https://github.com/eddyizm/tempus/pull/435
|
||||
* feat: Port remove song of playlist from tempus ng by @tvillega in https://github.com/eddyizm/tempus/pull/457
|
||||
* fix: artist sort by name case sensitive by @tvillega in https://github.com/eddyizm/tempus/pull/462
|
||||
* feat: added slide out enhanced navigation for tab mode and optionally portrait mode by @tvillega in https://github.com/eddyizm/tempus/pull/450
|
||||
* feat: Android Auto: improve media service browsing by @MaFo-28 in https://github.com/eddyizm/tempus/pull/437
|
||||
* feat: Support specifying a client certificate for mTLS auth by @tinsukE in https://github.com/eddyizm/tempus/pull/458
|
||||
|
||||
## New Contributors
|
||||
* @MaFo-28 made their first contribution in https://github.com/eddyizm/tempus/pull/437
|
||||
* @tinsukE made their first contribution in https://github.com/eddyizm/tempus/pull/458
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.11.0...v4.12.0
|
||||
|
||||
## What's Changed
|
||||
## [4.11.0](https://github.com/eddyizm/tempo/releases/tag/v4.11.0) (2026-02-15)
|
||||
* fix: added dynamic application id from gradle variant by @eddyizm in https://github.com/eddyizm/tempus/pull/425
|
||||
* fix: Use Bluetooth tethering connection by @jaime-grj in https://github.com/eddyizm/tempus/pull/428
|
||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/427
|
||||
* fix: visual glitches on landscape navbar by @tvillega in https://github.com/eddyizm/tempus/pull/429
|
||||
* fix: radio playback "source error" on android auto by @dmachard in https://github.com/eddyizm/tempus/pull/426
|
||||
* fix: speed button overlaps with shuffle on landscape by @tvillega in https://github.com/eddyizm/tempus/pull/430
|
||||
* fix: local url used in share link instead of server url by @tvillega in https://github.com/eddyizm/tempus/pull/431
|
||||
* Feat :prefer downloaded files by @eddyizm in https://github.com/eddyizm/tempus/pull/433
|
||||
* fix: radio metadata displayed by @TrackArcher in https://github.com/eddyizm/tempus/pull/352
|
||||
* feat: improve playlist chooser dialog UI by @tvillega in https://github.com/eddyizm/tempus/pull/439
|
||||
|
||||
## New Contributors
|
||||
* @dmachard made their first contribution in https://github.com/eddyizm/tempus/pull/426
|
||||
* @TrackArcher made their first contribution in https://github.com/eddyizm/tempus/pull/352
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.10.1...v4.11.0
|
||||
|
||||
## What's Changed
|
||||
## [4.10.1](https://github.com/eddyizm/tempo/releases/tag/v4.10.1) (2026-02-08)
|
||||
* fix: Addressing some UI/UX quirks by @tiltshiftfocus in https://github.com/eddyizm/tempus/pull/413
|
||||
* fix: keep observer until data is received on continuousPlay bug by @eddyizm in https://github.com/eddyizm/tempus/pull/421
|
||||
* fix: album art now displays on android auto by @trobinson in https://github.com/eddyizm/tempus/pull/414
|
||||
@@ -12,7 +63,7 @@
|
||||
* @tiltshiftfocus made their first contribution in https://github.com/eddyizm/tempus/pull/413
|
||||
* @trobinson made their first contribution in https://github.com/eddyizm/tempus/pull/414
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.8...v4.10.0
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.8...v4.10.1
|
||||
|
||||
## What's Changed
|
||||
## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02)
|
||||
|
||||
57
USAGE.md
57
USAGE.md
@@ -158,7 +158,8 @@ If your server supports it - add a internet radio station feed
|
||||
|
||||
## Android Auto
|
||||
|
||||
### Enabling on your head unit
|
||||
**Enabling on your head unit**
|
||||
|
||||
To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options".
|
||||
1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission.
|
||||
<p align="left">
|
||||
@@ -177,6 +178,60 @@ To allow the Tempus app on your car's head unit, "Unknown sources" needs to be e
|
||||
<img width="270" height="600" alt="3" src="https://github.com/user-attachments/assets/37db88e9-1b76-417f-9c47-da9f3a750fff" />
|
||||
</p>
|
||||
|
||||
**Interface Configuration**
|
||||
|
||||
The Android Auto interface can be configured by user to best suit their preferences.
|
||||
|
||||
<p align="left">
|
||||
<img src="mockup/usage/aa_preferences.png" width=317 style="margin-right:16px;">
|
||||
<img src="mockup/usage/aa_functions.png" width=317>
|
||||
</p>
|
||||
|
||||
4 tabs can be configured with the following functions:
|
||||
- Do not display : This tab is not used
|
||||
- Home : Displays all functions not used in other tabs
|
||||
- Recent : The 15 most recently listened-to albums
|
||||
- Albums : Albums sorted by name
|
||||
- Artists : Albums sorted by artist
|
||||
- Playlists
|
||||
- Podcast : The 100 podcasts recently added
|
||||
- Radio
|
||||
- Folder : Navigation through music directories
|
||||
- Albums most played : The 15 most played albums
|
||||
- Albums added : The 15 recently added albums
|
||||
- Star tracks
|
||||
- Star albums
|
||||
- Star artists
|
||||
- Random : 100 random songs
|
||||
|
||||
If all tabs are set to "Do not display", then "Home" tab will be created with all functions inside.
|
||||
|
||||
If "Home" is selected after another tab, it becomes "More"
|
||||
|
||||
In addition, you can choose to display the following functions as thumbnails or lists:
|
||||
- Home
|
||||
- Albums (Last played, Most played, Recently added, Artists, Star tracks, Star albums, Star artists, Random)
|
||||
- Playlists
|
||||
- Radio
|
||||
- Podcast
|
||||
|
||||
<p align="left">
|
||||
<img src="mockup/usage/aa_thumbnails.jpg" width=317 style="margin-right:16px;">
|
||||
<img src="mockup/usage/aa_list.jpg" width=317>
|
||||
</p>
|
||||
|
||||
The A-Z button allows you to jump to items starting with the chosen letter.
|
||||
|
||||
Search button returns albums or artists, even if they are not displayed by the selected function.
|
||||
|
||||
Results of the A-Z jump or search will always be displayed as a list.
|
||||
|
||||
<p align="left">
|
||||
<img src="mockup/usage/aa_AZ.jpg" width=317 style="margin-right:16px;">
|
||||
<img src="mockup/usage/aa_search.jpg" width=317>
|
||||
</p>
|
||||
|
||||
Display of albums and artists is limited to 500. For large libraries, it's preferable to use star albums or star artists.
|
||||
|
||||
### Server Settings
|
||||
**IN PROGRESS**
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
minSdkVersion 24
|
||||
targetSdk 35
|
||||
|
||||
versionCode 18
|
||||
versionName '4.10.0'
|
||||
versionCode 23
|
||||
versionName '4.12.6'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
@@ -101,6 +101,7 @@ dependencies {
|
||||
implementation 'androidx.room:room-runtime:2.6.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0"
|
||||
|
||||
// Android Material
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
|
||||
1164
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
Normal file
1164
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@
|
||||
|
||||
<provider
|
||||
android:name=".provider.AlbumArtContentProvider"
|
||||
android:authorities="com.cappielloantonio.tempo.provider"
|
||||
android:authorities="${applicationId}.albumart.provider"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.cappielloantonio.tempo.github.Github;
|
||||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
||||
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
|
||||
import com.cappielloantonio.tempo.util.ClientCertManager;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
public class App extends Application {
|
||||
@@ -31,6 +32,8 @@ public class App extends Application {
|
||||
instance = new App();
|
||||
context = getApplicationContext();
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
ClientCertManager.setupSslSocketFactory(context);
|
||||
}
|
||||
|
||||
public static App getInstance() {
|
||||
@@ -55,6 +58,48 @@ public class App extends Application {
|
||||
}
|
||||
return subsonic;
|
||||
}
|
||||
|
||||
public static Subsonic getSubsonicPublicClientInstance(boolean override) {
|
||||
|
||||
/*
|
||||
If I do the shortcut that the IDE suggests:
|
||||
SubsonicPreferences preferences = getSubsonicPreferences1();
|
||||
During the chain of calls it will run the following:
|
||||
String server = Preferences.getInUseServerAddress();
|
||||
Which could return Local URL, causing issues like generating public shares with Local URL
|
||||
|
||||
To prevent this I just replicated the entire chain of functions here,
|
||||
if you need a call to Subsonic using the Server (Public) URL use this function.
|
||||
*/
|
||||
|
||||
String server = Preferences.getServer();
|
||||
String username = Preferences.getUser();
|
||||
String password = Preferences.getPassword();
|
||||
String token = Preferences.getToken();
|
||||
String salt = Preferences.getSalt();
|
||||
boolean isLowSecurity = Preferences.isLowScurity();
|
||||
|
||||
SubsonicPreferences preferences = new SubsonicPreferences();
|
||||
preferences.setServerUrl(server);
|
||||
preferences.setUsername(username);
|
||||
preferences.setAuthentication(password, token, salt, isLowSecurity);
|
||||
|
||||
if (subsonic == null || override) {
|
||||
|
||||
if (preferences.getAuthentication() != null) {
|
||||
if (preferences.getAuthentication().getPassword() != null)
|
||||
Preferences.setPassword(preferences.getAuthentication().getPassword());
|
||||
if (preferences.getAuthentication().getToken() != null)
|
||||
Preferences.setToken(preferences.getAuthentication().getToken());
|
||||
if (preferences.getAuthentication().getSalt() != null)
|
||||
Preferences.setSalt(preferences.getAuthentication().getSalt());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return new Subsonic(preferences);
|
||||
}
|
||||
|
||||
public static Github getGithubClientInstance() {
|
||||
if (github == null) {
|
||||
|
||||
@@ -30,9 +30,13 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||
|
||||
@UnstableApi
|
||||
@Database(
|
||||
version = 13,
|
||||
version = 14,
|
||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
||||
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
||||
autoMigrations = {
|
||||
@AutoMigration(from = 10, to = 11),
|
||||
@AutoMigration(from = 11, to = 12),
|
||||
@AutoMigration(from = 13, to = 14),
|
||||
}
|
||||
)
|
||||
@TypeConverters({DateConverters.class})
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
@@ -19,6 +19,9 @@ public interface PlaylistDao {
|
||||
@Query("SELECT * FROM playlist")
|
||||
LiveData<List<Playlist>> getAll();
|
||||
|
||||
@Query("SELECT * FROM playlist")
|
||||
List<Playlist> getAllSync();
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
void insert(Playlist playlist);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -35,5 +34,8 @@ data class Server(
|
||||
val timestamp: Long,
|
||||
|
||||
@ColumnInfo(name = "low_security", defaultValue = "false")
|
||||
val isLowSecurity: Boolean
|
||||
val isLowSecurity: Boolean,
|
||||
|
||||
@ColumnInfo(name = "client_cert")
|
||||
val clientCert: String?,
|
||||
) : Parcelable
|
||||
@@ -195,11 +195,20 @@ class SessionMediaItem() {
|
||||
title = internetRadioStation.name
|
||||
streamUrl = internetRadioStation.streamUrl
|
||||
type = Constants.MEDIA_TYPE_RADIO
|
||||
|
||||
val homePageUrl = internetRadioStation.homePageUrl
|
||||
if (homePageUrl != null && homePageUrl.isNotEmpty() && MusicUtil.isImageUrl(homePageUrl)) {
|
||||
val encodedUrl = android.util.Base64.encodeToString(
|
||||
homePageUrl.toByteArray(java.nio.charset.StandardCharsets.UTF_8),
|
||||
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP
|
||||
)
|
||||
coverArtId = "ir_$encodedUrl"
|
||||
}
|
||||
}
|
||||
|
||||
fun getMediaItem(): MediaItem {
|
||||
val uri: Uri = getStreamUri()
|
||||
val artworkUri = AlbumArtContentProvider.contentUri(coverArtId)
|
||||
val artworkUri = if (coverArtId != null) AlbumArtContentProvider.contentUri(coverArtId!!) else null
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putString("id", id)
|
||||
@@ -229,7 +238,7 @@ class SessionMediaItem() {
|
||||
bundle.putLong("starred", starred?.time ?: 0)
|
||||
bundle.putString("albumId", albumId)
|
||||
bundle.putString("artistId", artistId)
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC)
|
||||
bundle.putString("type", type)
|
||||
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
|
||||
bundle.putInt("originalWidth", originalWidth ?: 0)
|
||||
bundle.putInt("originalHeight", originalHeight ?: 0)
|
||||
|
||||
@@ -8,12 +8,14 @@ import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.cappielloantonio.tempo.BuildConfig;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
@@ -28,7 +30,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AlbumArtContentProvider extends ContentProvider {
|
||||
public static final String AUTHORITY = "com.cappielloantonio.tempo.provider";
|
||||
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".albumart.provider";
|
||||
public static final String ALBUM_ART = "albumArt";
|
||||
private ExecutorService executor;
|
||||
|
||||
@@ -52,7 +54,15 @@ public class AlbumArtContentProvider extends ContentProvider {
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
Context context = getContext();
|
||||
String albumId = uri.getLastPathSegment();
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
|
||||
Uri artworkUri;
|
||||
|
||||
if (albumId != null && albumId.startsWith("ir_")) {
|
||||
String encodedUrl = albumId.substring("ir_".length());
|
||||
String decodedUrl = new String(Base64.decode(encodedUrl, Base64.URL_SAFE | Base64.NO_WRAP));
|
||||
artworkUri = Uri.parse(decodedUrl);
|
||||
} else {
|
||||
artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
|
||||
}
|
||||
|
||||
try {
|
||||
// use pipe to communicate between background thread and caller of openFile()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
@@ -69,6 +68,16 @@ public class AutomotiveRepository {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
||||
List<AlbumID3> albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums();
|
||||
|
||||
// add by MFO
|
||||
// Hack for artist view
|
||||
if("alphabeticalByArtist".equals(type))for(AlbumID3 album : albums){
|
||||
String artistName = album.getArtist();
|
||||
String albumName = album.getName();
|
||||
album.setName(artistName);
|
||||
album.setArtist(albumName);
|
||||
}
|
||||
// end add by MFO
|
||||
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
@@ -606,20 +615,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (InternetRadioStation radioStation : radioStations) {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(radioStation.getName())
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
||||
.build();
|
||||
|
||||
MediaItem mediaItem = new MediaItem.Builder()
|
||||
.setMediaId(radioStation.getId())
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
.setUri(radioStation.getStreamUrl())
|
||||
.build();
|
||||
|
||||
mediaItems.add(mediaItem);
|
||||
mediaItems.add(MappingUtil.mapInternetRadioStation(radioStation));
|
||||
}
|
||||
|
||||
setInternetRadioStationsMetadata(radioStations);
|
||||
|
||||
@@ -3,8 +3,11 @@ package com.cappielloantonio.tempo.repository;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
@@ -23,8 +26,45 @@ import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class PlaylistRepository {
|
||||
private static final MutableLiveData<Boolean> playlistUpdateTrigger = new MutableLiveData<>();
|
||||
|
||||
public LiveData<Boolean> getPlaylistUpdateTrigger() {
|
||||
return playlistUpdateTrigger;
|
||||
}
|
||||
|
||||
public void notifyPlaylistChanged() {
|
||||
playlistUpdateTrigger.postValue(true);
|
||||
refreshAllPlaylists();
|
||||
}
|
||||
|
||||
@androidx.media3.common.util.UnstableApi
|
||||
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
||||
private static final MutableLiveData<List<Playlist>> allPlaylistsLiveData = new MutableLiveData<>();
|
||||
|
||||
public LiveData<List<Playlist>> getAllPlaylists(LifecycleOwner owner) {
|
||||
refreshAllPlaylists();
|
||||
return allPlaylistsLiveData;
|
||||
}
|
||||
|
||||
public void refreshAllPlaylists() {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.getPlaylists()
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null) {
|
||||
List<Playlist> playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||
allPlaylistsLiveData.postValue(playlists);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
||||
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
||||
|
||||
@@ -104,9 +144,16 @@ public class PlaylistRepository {
|
||||
return playlistLiveData;
|
||||
}
|
||||
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||
public interface AddToPlaylistCallback {
|
||||
void onSuccess();
|
||||
void onFailure();
|
||||
void onAllSkipped();
|
||||
}
|
||||
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic, AddToPlaylistCallback callback) {
|
||||
android.util.Log.d("PlaylistRepository", "addSongToPlaylist: id=" + playlistId + ", songs=" + songsId);
|
||||
if (songsId.isEmpty()) {
|
||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||
if (callback != null) callback.onAllSkipped();
|
||||
} else{
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
@@ -114,17 +161,45 @@ public class PlaylistRepository {
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
|
||||
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||
if (callback != null) callback.onSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
|
||||
if (callback != null) callback.onFailure();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSongFromPlaylist(String playlistId, int index, AddToPlaylistCallback callback) {
|
||||
ArrayList<Integer> indexes = new ArrayList<>();
|
||||
indexes.add(index);
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.updatePlaylist(playlistId, null, true, null, indexes)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||
if (callback != null) {
|
||||
if (response.isSuccessful()) callback.onSuccess();
|
||||
else callback.onFailure();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
if (callback != null) callback.onFailure();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||
addSongToPlaylist(playlistId, songsId, playlistVisibilityIsPublic, null);
|
||||
}
|
||||
|
||||
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
@@ -132,7 +207,7 @@ public class PlaylistRepository {
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -145,20 +220,45 @@ public class PlaylistRepository {
|
||||
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.deletePlaylist(playlistId)
|
||||
.updatePlaylist(playlistId, name, true, null, null)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
createPlaylist(null, name, songsId);
|
||||
if (response.isSuccessful()) {
|
||||
// After renaming, we need to handle the song list update.
|
||||
// Subsonic doesn't have a "replace all songs" in updatePlaylist.
|
||||
// So we might still need to recreate if the songs changed significantly,
|
||||
// but if we just renamed, we should update the local pinned database.
|
||||
updateLocalPinnedPlaylistName(playlistId, name);
|
||||
notifyPlaylistChanged();
|
||||
}
|
||||
|
||||
// If songsId is provided, we might want to re-sync them.
|
||||
// For now, let's at least fix the name duplication issue.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void updateLocalPinnedPlaylistName(String id, String newName) {
|
||||
new Thread(() -> {
|
||||
List<Playlist> pinned = playlistDao.getAllSync();
|
||||
if (pinned != null) {
|
||||
for (Playlist p : pinned) {
|
||||
if (p.getId().equals(id)) {
|
||||
p.setName(newName);
|
||||
playlistDao.insert(p); // Replace strategy will update it
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void deletePlaylist(String playlistId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
@@ -166,7 +266,7 @@ public class PlaylistRepository {
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -194,6 +294,49 @@ public class PlaylistRepository {
|
||||
thread.start();
|
||||
}
|
||||
|
||||
@androidx.media3.common.util.UnstableApi
|
||||
public void updatePinnedPlaylists() {
|
||||
updatePinnedPlaylists(null);
|
||||
}
|
||||
|
||||
@androidx.media3.common.util.UnstableApi
|
||||
public void updatePinnedPlaylists(List<String> forceIds) {
|
||||
new Thread(() -> {
|
||||
List<Playlist> pinned = playlistDao.getAllSync();
|
||||
if (pinned != null && !pinned.isEmpty()) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.getPlaylists()
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null) {
|
||||
List<Playlist> remotes = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||
new Thread(() -> {
|
||||
for (Playlist p : pinned) {
|
||||
for (Playlist r : remotes) {
|
||||
if (p.getId().equals(r.getId())) {
|
||||
p.setName(r.getName());
|
||||
p.setSongCount(r.getSongCount());
|
||||
p.setDuration(r.getDuration());
|
||||
p.setCoverArtId(r.getCoverArtId());
|
||||
playlistDao.insert(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static class InsertThreadSafe implements Runnable {
|
||||
private final PlaylistDao playlistDao;
|
||||
private final Playlist playlist;
|
||||
|
||||
@@ -41,7 +41,7 @@ public class SharingRepository {
|
||||
public MutableLiveData<Share> createShare(String id, String description, Long expires) {
|
||||
MutableLiveData<Share> share = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
App.getSubsonicPublicClientInstance(false)
|
||||
.getSharingClient()
|
||||
.createShare(id, description, expires)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@@ -64,7 +64,7 @@ public class SharingRepository {
|
||||
}
|
||||
|
||||
public void updateShare(String id, String description, Long expires) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
App.getSubsonicPublicClientInstance(false)
|
||||
.getSharingClient()
|
||||
.updateShare(id, description, expires)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
|
||||
@@ -24,6 +24,9 @@ import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
||||
import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
@@ -32,6 +35,12 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val TAG = "BaseMediaService"
|
||||
|
||||
@@ -70,6 +79,13 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
}
|
||||
|
||||
private val radioHeaderCheckExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||
private var radioHeaderCheckScheduled = false
|
||||
private var radioHeaderCheckFuture: ScheduledFuture<*>? = null
|
||||
private val radioHeaderCheckRunnable = Runnable {
|
||||
checkRadioHttpHeaders()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
open fun playerInitHook() {
|
||||
@@ -120,6 +136,9 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
private var lastRadioArtist: String? = null
|
||||
private var lastRadioTitle: String? = null
|
||||
|
||||
fun initializePlayerListener(player: Player) {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
@@ -129,6 +148,16 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
|
||||
// Restart header checks for radio streams when media item changes
|
||||
val mediaType = mediaItem.mediaMetadata.extras?.getString("type")
|
||||
if (mediaType == Constants.MEDIA_TYPE_RADIO && player.isPlaying) {
|
||||
stopRadioHeaderChecks()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else if (mediaType != Constants.MEDIA_TYPE_RADIO) {
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
@@ -170,6 +199,96 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMetadata(metadata: Metadata) {
|
||||
// Handle streaming metadata (ICY, ID3) for radio / streaming content
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val extras = currentItem.mediaMetadata.extras
|
||||
if (extras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
var artist: String? = null
|
||||
var title: String? = null
|
||||
|
||||
// Extract metadata from ICY/ID3/Vorbis
|
||||
for (i in 0 until metadata.length()) {
|
||||
when (val entry = metadata[i]) {
|
||||
is IcyInfo -> {
|
||||
entry.title?.let { icyTitle ->
|
||||
val parts = icyTitle.split(" - ", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
artist = parts[0].trim().ifEmpty { null }
|
||||
title = parts[1].trim().ifEmpty { null }
|
||||
} else {
|
||||
title = icyTitle.trim().ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextInformationFrame -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.id) {
|
||||
"TPE1" -> if (!value.isNullOrBlank()) artist = value
|
||||
"TIT2" -> if (!value.isNullOrBlank()) title = value
|
||||
}
|
||||
}
|
||||
is VorbisComment -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.key) {
|
||||
"ARTIST" -> if (!value.isNullOrBlank()) artist = value
|
||||
"TITLE" -> if (!value.isNullOrBlank()) title = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
// Stop HTTP header checks since we have embedded metadata
|
||||
stopRadioHeaderChecks()
|
||||
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return
|
||||
|
||||
val metadataBuilder = currentItem.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(extras ?: Bundle())
|
||||
|
||||
// Store individual values in extras for UI
|
||||
artist?.let { newExtras.putString("radioArtist", it) }
|
||||
title?.let { newExtras.putString("radioTitle", it) }
|
||||
|
||||
// Get station name (preserve if already set)
|
||||
val stationName = extras?.getString("stationName")
|
||||
?: currentItem.mediaMetadata.title?.toString()
|
||||
?: ""
|
||||
if (stationName.isNotBlank()) {
|
||||
newExtras.putString("stationName", stationName)
|
||||
}
|
||||
|
||||
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||
val formattedTitle = when {
|
||||
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||
!title.isNullOrBlank() -> title
|
||||
!artist.isNullOrBlank() -> artist
|
||||
else -> stationName
|
||||
}
|
||||
|
||||
metadataBuilder.setTitle(formattedTitle)
|
||||
if (stationName.isNotBlank()) {
|
||||
metadataBuilder.setArtist(stationName)
|
||||
}
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, currentItem.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.setExtras(newExtras).build())
|
||||
.build())
|
||||
updateWidget(exo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||
if (!isPlaying) {
|
||||
@@ -182,8 +301,10 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
@@ -287,6 +408,8 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
radioHeaderCheckExecutor.shutdown()
|
||||
releasePlayers()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
@@ -405,6 +528,148 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun scheduleRadioHeaderChecks() {
|
||||
val player = mediaLibrarySession.player
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val mediaType = currentItem.mediaMetadata.extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
if (radioHeaderCheckScheduled) return
|
||||
|
||||
// Check immediately, then periodically
|
||||
checkRadioHttpHeaders()
|
||||
radioHeaderCheckFuture = radioHeaderCheckExecutor.scheduleWithFixedDelay(
|
||||
radioHeaderCheckRunnable,
|
||||
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
radioHeaderCheckScheduled = true
|
||||
}
|
||||
|
||||
private fun stopRadioHeaderChecks() {
|
||||
if (!radioHeaderCheckScheduled) return
|
||||
radioHeaderCheckFuture?.cancel(false)
|
||||
radioHeaderCheckFuture = null
|
||||
radioHeaderCheckScheduled = false
|
||||
}
|
||||
|
||||
private fun checkRadioHttpHeaders() {
|
||||
val player = mediaLibrarySession.player
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val extras = currentItem.mediaMetadata.extras
|
||||
val mediaType = extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
// Skip if we already have embedded metadata (ICY/ID3) - HTTP headers are only fallback
|
||||
val hasEmbeddedMetadata = !currentItem.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItem.mediaMetadata.title.isNullOrBlank() ||
|
||||
(extras != null && !extras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(extras != null && !extras.getString("radioTitle").isNullOrBlank())
|
||||
if (hasEmbeddedMetadata) return
|
||||
|
||||
val streamUrl = extras?.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
|
||||
if (streamUrl.isNullOrBlank()) return
|
||||
|
||||
try {
|
||||
val url = URL(streamUrl)
|
||||
val connection = url.openConnection() as? HttpURLConnection ?: return
|
||||
|
||||
// Only try HEAD request (lightweight) - skip GET fallback as it's unreliable
|
||||
connection.requestMethod = "HEAD"
|
||||
connection.setRequestProperty("Icy-MetaData", "1")
|
||||
connection.setRequestProperty("User-Agent", "Tempus/1.0")
|
||||
connection.connectTimeout = 3000 // Reduced timeout
|
||||
connection.readTimeout = 3000
|
||||
|
||||
connection.connect()
|
||||
|
||||
if (connection.responseCode >= 400) {
|
||||
connection.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for metadata in HTTP headers
|
||||
val streamTitle = connection.getHeaderField("icy-name")
|
||||
?: connection.getHeaderField("StreamTitle")
|
||||
?: connection.getHeaderField("stream-title")
|
||||
|
||||
connection.disconnect()
|
||||
|
||||
if (!streamTitle.isNullOrBlank()) {
|
||||
processStreamTitle(streamTitle, player)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Silently fail - this is a fallback mechanism, ICY metadata is primary
|
||||
}
|
||||
}
|
||||
|
||||
private fun processStreamTitle(streamTitle: String, player: Player) {
|
||||
// Parse "Artist - Title" format
|
||||
val parts = streamTitle.split(" - ", limit = 2)
|
||||
val artist = if (parts.size == 2) parts[0].trim().ifEmpty { null } else null
|
||||
val title = if (parts.size == 2) parts[1].trim().ifEmpty { null } else streamTitle.trim().ifEmpty { null }
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
// Update on main thread
|
||||
widgetUpdateHandler.post {
|
||||
val currentItemNow = player.currentMediaItem ?: return@post
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return@post
|
||||
|
||||
val currentExtras = currentItemNow.mediaMetadata.extras
|
||||
if (currentExtras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return@post
|
||||
|
||||
// Double-check we still don't have embedded metadata (might have arrived since check)
|
||||
val hasEmbeddedMetadata = !currentItemNow.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItemNow.mediaMetadata.title.isNullOrBlank() ||
|
||||
(currentExtras != null && !currentExtras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(currentExtras != null && !currentExtras.getString("radioTitle").isNullOrBlank())
|
||||
if (hasEmbeddedMetadata) return@post
|
||||
|
||||
val metadataBuilder = currentItemNow.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(currentExtras ?: Bundle())
|
||||
|
||||
// Store individual values in extras for UI
|
||||
artist?.let { newExtras.putString("radioArtist", it) }
|
||||
title?.let { newExtras.putString("radioTitle", it) }
|
||||
|
||||
// Get station name (preserve if already set)
|
||||
val stationName = currentExtras?.getString("stationName")
|
||||
?: currentItemNow.mediaMetadata.title?.toString()
|
||||
?: ""
|
||||
if (stationName.isNotBlank()) {
|
||||
newExtras.putString("stationName", stationName)
|
||||
}
|
||||
|
||||
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||
val formattedTitle = when {
|
||||
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||
!title.isNullOrBlank() -> title
|
||||
!artist.isNullOrBlank() -> artist
|
||||
else -> stationName
|
||||
}
|
||||
|
||||
metadataBuilder.setTitle(formattedTitle)
|
||||
if (stationName.isNotBlank()) {
|
||||
metadataBuilder.setArtist(stationName)
|
||||
}
|
||||
metadataBuilder.setExtras(newExtras)
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, currentItemNow.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build())
|
||||
updateWidget(exo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
@@ -595,4 +860,5 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 30L // Reduced frequency - only fallback when ICY fails
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.subsonic
|
||||
import com.cappielloantonio.tempo.App
|
||||
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
||||
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
||||
import com.cappielloantonio.tempo.util.ClientCertManager
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -13,7 +14,7 @@ import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RetrofitClient(subsonic: Subsonic) {
|
||||
var retrofit: Retrofit
|
||||
val retrofit: Retrofit
|
||||
|
||||
init {
|
||||
val gson = GsonBuilder()
|
||||
@@ -50,6 +51,7 @@ class RetrofitClient(subsonic: Subsonic) {
|
||||
.addInterceptor(cacheUtil.offlineInterceptor)
|
||||
// .addNetworkInterceptor(cacheUtil.onlineInterceptor)
|
||||
.cache(getCache())
|
||||
.setupSsl()
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -63,4 +65,11 @@ class RetrofitClient(subsonic: Subsonic) {
|
||||
val cacheSize = 10 * 1024 * 1024
|
||||
return Cache(App.getContext().cacheDir, cacheSize.toLong())
|
||||
}
|
||||
|
||||
private fun OkHttpClient.Builder.setupSsl(): OkHttpClient.Builder {
|
||||
ClientCertManager.sslSocketFactory?.let { sslSocketFactory ->
|
||||
sslSocketFactory(sslSocketFactory, ClientCertManager.trustManager)
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.subsonic.models
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
@@ -10,5 +11,6 @@ class InternetRadioStation(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var streamUrl: String? = null,
|
||||
@SerializedName("homePageUrl", alternate = ["homepageUrl"])
|
||||
var homePageUrl: String? = null,
|
||||
) : Parcelable
|
||||
@@ -62,7 +62,8 @@ public class CacheUtil {
|
||||
|
||||
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH);
|
||||
if (!hasAppropriateTransport) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.content.IntentFilter;
|
||||
@@ -11,18 +12,22 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
@@ -48,6 +53,7 @@ import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -63,9 +69,12 @@ public class MainActivity extends BaseActivity {
|
||||
private FragmentManager fragmentManager;
|
||||
private NavHostFragment navHostFragment;
|
||||
private BottomNavigationView bottomNavigationView;
|
||||
private FrameLayout bottomNavigationViewFrame;
|
||||
public NavController navController;
|
||||
private DrawerLayout drawerLayout;
|
||||
private NavigationView navigationView;
|
||||
private BottomSheetBehavior bottomSheetBehavior;
|
||||
private boolean isLandscape = false;
|
||||
public boolean isLandscape = false;
|
||||
private AssetLinkNavigator assetLinkNavigator;
|
||||
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||
|
||||
@@ -111,6 +120,7 @@ public class MainActivity extends BaseActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
pingServer();
|
||||
toggleNavigationDrawerLockOnOrientationChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,14 +158,8 @@ public class MainActivity extends BaseActivity {
|
||||
goToLogin();
|
||||
}
|
||||
|
||||
// Set bottom navigation height
|
||||
if (isLandscape) {
|
||||
ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams();
|
||||
Rect windowRect = new Rect();
|
||||
bottomNavigationView.getWindowVisibleDisplayFrame(windowRect);
|
||||
layoutParams.width = windowRect.height();
|
||||
bottomNavigationView.setLayoutParams(layoutParams);
|
||||
}
|
||||
toggleNavigationDrawerLockOnOrientationChange();
|
||||
|
||||
}
|
||||
|
||||
// BOTTOM SHEET/NAVIGATION
|
||||
@@ -259,8 +263,12 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
private void initNavigation() {
|
||||
bottomNavigationView = findViewById(R.id.bottom_navigation);
|
||||
bottomNavigationViewFrame = findViewById(R.id.bottom_navigation_frame);
|
||||
navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment);
|
||||
navController = Objects.requireNonNull(navHostFragment).getNavController();
|
||||
// This is the lateral slide-in drawer
|
||||
drawerLayout = findViewById(R.id.drawer_layout);
|
||||
navigationView = findViewById(R.id.nav_view);
|
||||
|
||||
/*
|
||||
* In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet è aperto,
|
||||
@@ -270,23 +278,101 @@ public class MainActivity extends BaseActivity {
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && (
|
||||
destination.getId() == R.id.homeFragment ||
|
||||
destination.getId() == R.id.libraryFragment ||
|
||||
destination.getId() == R.id.downloadFragment)
|
||||
destination.getId() == R.id.downloadFragment ||
|
||||
destination.getId() == R.id.albumCatalogueFragment ||
|
||||
destination.getId() == R.id.artistCatalogueFragment ||
|
||||
destination.getId() == R.id.genreCatalogueFragment ||
|
||||
destination.getId() == R.id.playlistCatalogueFragment)
|
||||
) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
});
|
||||
|
||||
NavigationUI.setupWithNavController(bottomNavigationView, navController);
|
||||
NavigationUI.setupWithNavController(navigationView, navController);
|
||||
}
|
||||
|
||||
public void setBottomNavigationBarVisibility(boolean visibility) {
|
||||
if (visibility) {
|
||||
bottomNavigationView.setVisibility(View.VISIBLE);
|
||||
bottomNavigationViewFrame.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
bottomNavigationView.setVisibility(View.GONE);
|
||||
bottomNavigationViewFrame.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleBottomNavigationBarVisibilityOnOrientationChange() {
|
||||
// Ignore orientation change, bottom navbar always hidden
|
||||
if (Preferences.getHideBottomNavbarOnPortrait()) {
|
||||
setBottomNavigationBarVisibility(false);
|
||||
setPortraitPlayerBottomSheetPeekHeight(56);
|
||||
setSystemBarsVisibility(!isLandscape);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLandscape) {
|
||||
// Show app navbar + show system bars
|
||||
setPortraitPlayerBottomSheetPeekHeight(136);
|
||||
setBottomNavigationBarVisibility(true);
|
||||
setSystemBarsVisibility(true);
|
||||
} else {
|
||||
// Hide app navbar + hide system bars
|
||||
setPortraitPlayerBottomSheetPeekHeight(56);
|
||||
setBottomNavigationBarVisibility(false);
|
||||
setSystemBarsVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void setNavigationDrawerLock(boolean locked) {
|
||||
int mode = locked
|
||||
? DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||
: DrawerLayout.LOCK_MODE_UNLOCKED;
|
||||
drawerLayout.setDrawerLockMode(mode);
|
||||
}
|
||||
|
||||
public void toggleNavigationDrawerLockOnOrientationChange() {
|
||||
// Ignore orientation check, drawer always unlocked
|
||||
if (Preferences.getEnableDrawerOnPortrait()) {
|
||||
setNavigationDrawerLock(false);
|
||||
return;
|
||||
}
|
||||
if (!isLandscape) {
|
||||
setNavigationDrawerLock(true);
|
||||
} else {
|
||||
setNavigationDrawerLock(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSystemBarsVisibility(boolean visibility) {
|
||||
WindowInsetsControllerCompat insetsController;
|
||||
View decorView = getWindow().getDecorView();
|
||||
insetsController = new WindowInsetsControllerCompat(getWindow(), decorView);
|
||||
|
||||
if (visibility) {
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
|
||||
insetsController.show(WindowInsetsCompat.Type.navigationBars());
|
||||
insetsController.show(WindowInsetsCompat.Type.statusBars());
|
||||
insetsController.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT);
|
||||
} else {
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
insetsController.hide(WindowInsetsCompat.Type.navigationBars());
|
||||
insetsController.hide(WindowInsetsCompat.Type.statusBars());
|
||||
insetsController.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPortraitPlayerBottomSheetPeekHeight(int peekHeight) {
|
||||
FrameLayout bottomSheet = findViewById(R.id.player_bottom_sheet);
|
||||
BottomSheetBehavior<FrameLayout> behavior =
|
||||
BottomSheetBehavior.from(bottomSheet);
|
||||
|
||||
int newPeekPx = (int) (peekHeight * getResources().getDisplayMetrics().density);
|
||||
behavior.setPeekHeight(newPeekPx);
|
||||
}
|
||||
|
||||
private void initService() {
|
||||
MediaManager.check(getMediaBrowserListenableFuture());
|
||||
|
||||
@@ -368,6 +454,7 @@ public class MainActivity extends BaseActivity {
|
||||
Preferences.setServer(null);
|
||||
Preferences.setLocalAddress(null);
|
||||
Preferences.setUser(null);
|
||||
Preferences.setClientCert(null);
|
||||
|
||||
// TODO Enter all settings to be reset
|
||||
Preferences.setOpenSubsonic(false);
|
||||
@@ -570,4 +657,4 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
|
||||
public void sort(String order) {
|
||||
switch (order) {
|
||||
case Constants.ARTIST_ORDER_BY_NAME:
|
||||
artists.sort(Comparator.comparing(ArtistID3::getName));
|
||||
artists.sort(Comparator.comparing(ArtistID3::getName,String.CASE_INSENSITIVE_ORDER));
|
||||
break;
|
||||
case Constants.ARTIST_ORDER_BY_RANDOM:
|
||||
Collections.shuffle(artists);
|
||||
|
||||
@@ -42,8 +42,13 @@ public class InternetRadioStationAdapter extends RecyclerView.Adapter<InternetRa
|
||||
holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName());
|
||||
holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl());
|
||||
|
||||
String imageId = internetRadioStation.getHomePageUrl();
|
||||
if (imageId == null || imageId.isEmpty()) {
|
||||
imageId = internetRadioStation.getStreamUrl();
|
||||
}
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(holder.itemView.getContext(), internetRadioStation.getStreamUrl(), CustomGlideRequest.ResourceType.Radio)
|
||||
.from(holder.itemView.getContext(), imageId, CustomGlideRequest.ResourceType.Radio)
|
||||
.build()
|
||||
.into(holder.item.internetRadioStationCoverImageView);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
|
||||
|
||||
FilterResults results = new FilterResults();
|
||||
results.values = filteredList;
|
||||
results.count = filteredList.size();
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -54,7 +55,9 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
playlists.clear();
|
||||
if (results.count > 0) playlists.addAll((List) results.values);
|
||||
if (results.values != null) {
|
||||
playlists.addAll((List<Playlist>) results.values);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -359,6 +359,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
private boolean onLongClick() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
||||
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
|
||||
|
||||
click.onMediaLongClick(bundle);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -20,41 +19,30 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
||||
private DialogPlaylistChooserBinding bind;
|
||||
private PlaylistChooserViewModel playlistChooserViewModel;
|
||||
|
||||
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
||||
bind = DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
||||
|
||||
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
||||
|
||||
String[] playlistVisibilityChoice = {
|
||||
getString(R.string.playlist_chooser_dialog_visibility_public),
|
||||
getString(R.string.playlist_chooser_dialog_visibility_private)
|
||||
};
|
||||
bind.playlistDialogChooserVisibilitySwitch.setOnCheckedChangeListener(
|
||||
(buttonView,
|
||||
isChecked) -> playlistChooserViewModel.setIsPlaylistPublic(isChecked)
|
||||
);
|
||||
bind.playlistChooserDialogCreateButton.setOnClickListener(v -> launchPlaylistEditor());
|
||||
bind.playlistChooserDialogCancelButton.setOnClickListener(v -> dismiss());
|
||||
|
||||
return new MaterialAlertDialogBuilder(getActivity())
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.playlist_chooser_dialog_title)
|
||||
.setSingleChoiceItems(
|
||||
playlistVisibilityChoice,
|
||||
0,
|
||||
(dialog, which) -> {
|
||||
boolean isPublic = (which == 0);
|
||||
playlistChooserViewModel.setIsPlaylistPublic(isPublic);
|
||||
})
|
||||
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
||||
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
||||
.create();
|
||||
.setTitle(R.string.playlist_chooser_dialog_title);
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,25 +57,26 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||
|
||||
initPlaylistView();
|
||||
setSongInfo();
|
||||
setButtonAction();
|
||||
}
|
||||
|
||||
private void setSongInfo() {
|
||||
playlistChooserViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT));
|
||||
}
|
||||
|
||||
private void setButtonAction() {
|
||||
androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog());
|
||||
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, playlistChooserViewModel.getSongsToAdd());
|
||||
private void launchPlaylistEditor() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(
|
||||
Constants.TRACKS_OBJECT,
|
||||
playlistChooserViewModel.getSongsToAdd()
|
||||
);
|
||||
|
||||
PlaylistEditorDialog dialog = new PlaylistEditorDialog(null);
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
PlaylistEditorDialog editorDialog = new PlaylistEditorDialog(null);
|
||||
editorDialog.setArguments(bundle);
|
||||
editorDialog.show(
|
||||
requireActivity().getSupportFragmentManager(),
|
||||
null);
|
||||
|
||||
Objects.requireNonNull(getDialog()).dismiss();
|
||||
});
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private void initPlaylistView() {
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.security.KeyChain;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -32,11 +32,21 @@ public class ServerSignupDialog extends DialogFragment {
|
||||
private String server;
|
||||
private String localAddress;
|
||||
private boolean lowSecurity = false;
|
||||
private String clientCertAlias;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
bind = DialogServerSignupBinding.inflate(getLayoutInflater());
|
||||
bind.clientCertTextView.setOnClickListener(v -> {
|
||||
if (TextUtils.isEmpty(bind.clientCertTextView.getText())) {
|
||||
KeyChain.choosePrivateKeyAlias(requireActivity(), alias -> {
|
||||
bind.clientCertTextView.setText(alias);
|
||||
}, null, null, null, null);
|
||||
} else {
|
||||
bind.clientCertTextView.setText(null);
|
||||
}
|
||||
});
|
||||
|
||||
loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
|
||||
|
||||
@@ -74,6 +84,7 @@ public class ServerSignupDialog extends DialogFragment {
|
||||
bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress());
|
||||
bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress());
|
||||
bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity());
|
||||
bind.clientCertTextView.setText(loginViewModel.getServerToEdit().getClientCert());
|
||||
}
|
||||
} else {
|
||||
loginViewModel.setServerToEdit(null);
|
||||
@@ -106,6 +117,7 @@ public class ServerSignupDialog extends DialogFragment {
|
||||
server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null;
|
||||
localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null;
|
||||
lowSecurity = bind.lowSecurityCheckbox.isChecked();
|
||||
clientCertAlias = bind.clientCertTextView.getText() != null && !bind.clientCertTextView.getText().toString().trim().isBlank() ? bind.clientCertTextView.getText().toString().trim() : null;
|
||||
|
||||
if (TextUtils.isEmpty(serverName)) {
|
||||
bind.serverNameTextView.setError(getString(R.string.error_required));
|
||||
@@ -137,6 +149,6 @@ public class ServerSignupDialog extends DialogFragment {
|
||||
|
||||
private void saveServerPreference() {
|
||||
String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString();
|
||||
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity));
|
||||
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity, this.clientCertAlias));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,47 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
private void setTrackInfo() {
|
||||
genreLink = null;
|
||||
yearLink = null;
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
boolean isRadio = Objects.equals(type, Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
if (isRadio) {
|
||||
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||
// MediaMetadata.title/artist are formatted for notification
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||
|
||||
String artist = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
// Format: "Artist - Song" or fallback to title or station name
|
||||
String mainTitle;
|
||||
if (!android.text.TextUtils.isEmpty(artist) && !android.text.TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!android.text.TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!android.text.TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.trakTitleInfoTextView.setText(mainTitle);
|
||||
bind.trakArtistInfoTextView.setText(stationName);
|
||||
} else {
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: "");
|
||||
}
|
||||
|
||||
if (mediaMetadata.extras != null) {
|
||||
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||
@@ -90,6 +124,27 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
|
||||
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
|
||||
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
||||
|
||||
// Handle radio-specific metadata
|
||||
if (isRadio) {
|
||||
String stationName = mediaMetadata.extras.getString("stationName", getString(R.string.label_placeholder));
|
||||
String radioArtist = mediaMetadata.extras.getString("radioArtist", "");
|
||||
String radioTitle = mediaMetadata.extras.getString("radioTitle", "");
|
||||
|
||||
// Show station name in station section
|
||||
bind.stationInfoSector.setVisibility(android.view.View.VISIBLE);
|
||||
bind.stationValueSector.setText(stationName);
|
||||
|
||||
// Use radio metadata for title/artist if available
|
||||
if (!android.text.TextUtils.isEmpty(radioTitle)) {
|
||||
titleValue = radioTitle;
|
||||
}
|
||||
if (!android.text.TextUtils.isEmpty(radioArtist)) {
|
||||
artistValue = radioArtist;
|
||||
}
|
||||
} else {
|
||||
bind.stationInfoSector.setVisibility(android.view.View.GONE);
|
||||
}
|
||||
|
||||
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
||||
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
||||
|
||||
@@ -83,7 +83,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
activity.setBottomNavigationBarVisibility(true);
|
||||
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||
activity.setBottomSheetVisibility(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,18 +21,26 @@ import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||
import com.cappielloantonio.tempo.service.BaseMediaService
|
||||
import com.cappielloantonio.tempo.service.MediaService
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
|
||||
class EqualizerFragment : Fragment() {
|
||||
|
||||
private lateinit var activity: MainActivity
|
||||
private var equalizerManager: EqualizerManager? = null
|
||||
private lateinit var eqBandsContainer: LinearLayout
|
||||
private lateinit var eqSwitch: Switch
|
||||
private lateinit var resetButton: Button
|
||||
private lateinit var safeSpace: Space
|
||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||
|
||||
private var receiverRegistered = false
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
activity = requireActivity() as MainActivity
|
||||
}
|
||||
|
||||
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -73,6 +81,8 @@ class EqualizerFragment : Fragment() {
|
||||
)
|
||||
receiverRegistered = true
|
||||
}
|
||||
val showBottomBar = !Preferences.getHideBottomNavbarOnPortrait()
|
||||
activity.setBottomNavigationBarVisibility(showBottomBar)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class HomeFragment extends Fragment {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
activity.setBottomNavigationBarVisibility(true);
|
||||
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||
activity.setBottomSheetVisibility(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
@@ -16,8 +18,11 @@ import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding;
|
||||
@@ -43,6 +48,7 @@ import java.util.Objects;
|
||||
@UnstableApi
|
||||
public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
private static final String TAG = "LibraryFragment";
|
||||
private static final String TOAST_MSG = "Long press to refresh" ;
|
||||
|
||||
private FragmentLibraryBinding bind;
|
||||
private MainActivity activity;
|
||||
@@ -81,13 +87,14 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
initArtistView();
|
||||
initGenreView();
|
||||
initPlaylistView();
|
||||
initSwipeToRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
activity.setBottomNavigationBarVisibility(true);
|
||||
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,22 +119,41 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle);
|
||||
});
|
||||
|
||||
// Album
|
||||
bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||
libraryViewModel.refreshAlbumSample(getViewLifecycleOwner());
|
||||
return true;
|
||||
});
|
||||
bind.albumCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
|
||||
// Artist
|
||||
bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||
libraryViewModel.refreshArtistSample(getViewLifecycleOwner());
|
||||
return true;
|
||||
});
|
||||
bind.artistCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
|
||||
// Genre
|
||||
bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||
libraryViewModel.refreshGenreSample(getViewLifecycleOwner());
|
||||
return true;
|
||||
});
|
||||
bind.genreCatalogueSampleTextViewRefreshable.setOnClickListener(v ->
|
||||
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
|
||||
// Playlist
|
||||
bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||
libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner());
|
||||
return true;
|
||||
});
|
||||
bind.playlistCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
}
|
||||
|
||||
private void initAppBar() {
|
||||
@@ -304,4 +330,20 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
private void initializeMediaBrowser() {
|
||||
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||
}
|
||||
|
||||
public void initSwipeToRefresh() {
|
||||
bind.swipeLibraryToRefresh.setOnRefreshListener(() -> {
|
||||
pullToRefresh();
|
||||
bind.swipeLibraryToRefresh.setRefreshing(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void pullToRefresh() {
|
||||
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
|
||||
libraryViewModel.refreshAlbumSample(lifecycleOwner);
|
||||
libraryViewModel.refreshGenreSample(lifecycleOwner);
|
||||
libraryViewModel.refreshArtistSample(lifecycleOwner);
|
||||
libraryViewModel.refreshPlaylistSample(lifecycleOwner);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onServerClick(Bundle bundle) {
|
||||
Server server = bundle.getParcelable("server_object");
|
||||
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity());
|
||||
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity(), server.getClientCert());
|
||||
|
||||
SystemRepository systemRepository = new SystemRepository();
|
||||
systemRepository.checkUserCredential(new SystemCallback() {
|
||||
@@ -142,13 +142,14 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
||||
dialog.show(activity.getSupportFragmentManager(), null);
|
||||
}
|
||||
|
||||
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity) {
|
||||
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity, String clientCert) {
|
||||
Preferences.setServerId(serverId);
|
||||
Preferences.setServer(server);
|
||||
Preferences.setLocalAddress(localAddress);
|
||||
Preferences.setUser(user);
|
||||
Preferences.setPassword(password);
|
||||
Preferences.setLowSecurity(isLowSecurity);
|
||||
Preferences.setClientCert(clientCert);
|
||||
|
||||
App.getSubsonicClientInstance(true);
|
||||
}
|
||||
@@ -161,6 +162,7 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
||||
Preferences.setToken(null);
|
||||
Preferences.setSalt(null);
|
||||
Preferences.setLowSecurity(false);
|
||||
Preferences.setClientCert(null);
|
||||
|
||||
App.getSubsonicClientInstance(true);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -173,25 +174,54 @@ public class PlayerBottomSheetFragment extends Fragment {
|
||||
playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId"));
|
||||
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
String type = mediaMetadata.extras.getString("type");
|
||||
|
||||
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||
// For radio: keep header consistent with full player
|
||||
String stationName = mediaMetadata.extras.getString(
|
||||
"stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : ""
|
||||
);
|
||||
|
||||
String artist = mediaMetadata.extras.getString("radioArtist", "");
|
||||
String title = mediaMetadata.extras.getString("radioTitle", "");
|
||||
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mainTitle);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(stationName);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
// Default (music, podcast, etc.)
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: ""
|
||||
);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||
mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), "")
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
}
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||
(mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), ""))
|
||||
|| (Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null)
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@ import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
@@ -32,6 +35,10 @@ import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavOptions;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.transition.ChangeBounds;
|
||||
import androidx.transition.Slide;
|
||||
import androidx.transition.TransitionManager;
|
||||
import androidx.transition.TransitionSet;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
@@ -55,7 +62,6 @@ import com.google.android.material.elevation.SurfaceColors;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -214,12 +220,53 @@ public class PlayerControllerFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void setMetadata(MediaMetadata mediaMetadata) {
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
|
||||
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||
// MediaMetadata.title/artist are formatted for notification
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||
|
||||
String artist = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
// Format: "Artist - Song" or fallback to title or station name
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
playerMediaTitleLabel.setText(mainTitle);
|
||||
playerArtistNameLabel.setText(stationName);
|
||||
|
||||
playerMediaTitleLabel.setSelected(true);
|
||||
playerArtistNameLabel.setSelected(true);
|
||||
|
||||
playerMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||
playerArtistNameLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||
|
||||
updateAssetLinkChips(mediaMetadata);
|
||||
return;
|
||||
}
|
||||
|
||||
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
|
||||
playerArtistNameLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? String.valueOf(mediaMetadata.artist)
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
playerMediaTitleLabel.setSelected(true);
|
||||
@@ -236,41 +283,80 @@ public class PlayerControllerFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
||||
boolean isLocal = false;
|
||||
|
||||
if (mediaBrowserListenableFuture != null && mediaBrowserListenableFuture.isDone()) {
|
||||
try {
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
if (browser != null && browser.getCurrentMediaItem() != null) {
|
||||
android.net.Uri currentUri = browser.getCurrentMediaItem().requestMetadata.mediaUri;
|
||||
if (currentUri != null) {
|
||||
String scheme = currentUri.getScheme();
|
||||
isLocal = "content".equals(scheme) || "file".equals(scheme);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("DEBUG_PLAYER", "Error getting browser for UI update", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaMetadata.extras != null) {
|
||||
String extension = mediaMetadata.extras.getString("suffix", getString(R.string.player_unknown_format));
|
||||
String bitrate = mediaMetadata.extras.getInt("bitrate", 0) != 0 ? mediaMetadata.extras.getInt("bitrate", 0) + "kbps" : "Original";
|
||||
String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? new DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : "";
|
||||
int rawBitrate = mediaMetadata.extras.getInt("bitrate", 0);
|
||||
String bitrate = rawBitrate != 0 ? rawBitrate + "kbps" : "Original";
|
||||
String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ?
|
||||
new java.text.DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : "";
|
||||
String bitDepth = mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + "b" : "";
|
||||
|
||||
playerMediaExtension.setText(extension);
|
||||
|
||||
if (bitrate.equals("Original")) {
|
||||
if (bitrate.equals("Original") && !isLocal) {
|
||||
playerMediaBitrate.setVisibility(View.GONE);
|
||||
} else {
|
||||
List<String> mediaQualityItems = new ArrayList<>();
|
||||
|
||||
if (!bitrate.trim().isEmpty()) mediaQualityItems.add(bitrate);
|
||||
if (!bitDepth.trim().isEmpty()) mediaQualityItems.add(bitDepth);
|
||||
if (!samplingRate.trim().isEmpty()) mediaQualityItems.add(samplingRate);
|
||||
|
||||
String mediaQuality = TextUtils.join(" • ", mediaQualityItems);
|
||||
playerMediaBitrate.setVisibility(View.VISIBLE);
|
||||
playerMediaBitrate.setText(mediaQuality);
|
||||
List<String> items = new ArrayList<>();
|
||||
if (!bitrate.trim().isEmpty()) items.add(bitrate);
|
||||
if (!bitDepth.trim().isEmpty()) items.add(bitDepth);
|
||||
if (!samplingRate.trim().isEmpty()) items.add(samplingRate);
|
||||
String mediaQuality = TextUtils.join(" • ", items);
|
||||
|
||||
playerMediaBitrate.setVisibility(Preferences.getBitrateVisible() ? View.VISIBLE : View.GONE);
|
||||
playerMediaBitrate.setText(isLocal ? mediaQuality : mediaQuality);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw");
|
||||
boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0");
|
||||
|
||||
if (!isLocal) {
|
||||
boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw");
|
||||
boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0");
|
||||
if (isTranscodingExtension || isTranscodingBitrate) {
|
||||
playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")");
|
||||
playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ?
|
||||
MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested));
|
||||
}
|
||||
|
||||
if (isTranscodingExtension || isTranscodingBitrate) {
|
||||
playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")");
|
||||
playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ? MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested));
|
||||
}
|
||||
|
||||
playerTrackInfo.setOnClickListener(view -> {
|
||||
TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata);
|
||||
dialog.show(activity.getSupportFragmentManager(), null);
|
||||
});
|
||||
});
|
||||
|
||||
playerMediaExtension.setOnClickListener( v -> toggleBitrateVisibility() );
|
||||
playerMediaBitrate.setOnClickListener(v -> toggleBitrateVisibility() );
|
||||
}
|
||||
|
||||
private void toggleBitrateVisibility() {
|
||||
ViewGroup parent = (ViewGroup) playerMediaBitrate.getParent();
|
||||
|
||||
TransitionSet transition = new TransitionSet()
|
||||
.addTransition(new Slide(Gravity.START))
|
||||
.addTransition(new ChangeBounds())
|
||||
.setDuration(500)
|
||||
.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
TransitionManager.beginDelayedTransition(parent, transition);
|
||||
|
||||
playerMediaBitrate.setVisibility(Preferences.getBitrateVisible() ? View.GONE : View.VISIBLE);
|
||||
Preferences.setBitrateVisible(!Preferences.getBitrateVisible());
|
||||
}
|
||||
|
||||
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
||||
|
||||
@@ -216,8 +216,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
});
|
||||
|
||||
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
||||
Collections.shuffle(songs);
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> shuffledSongs = new java.util.ArrayList<>(songs);
|
||||
java.util.Collections.shuffle(shuffledSongs);
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, shuffledSongs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
});
|
||||
}
|
||||
@@ -227,32 +228,33 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
private void initBackCover() {
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
||||
if (bind != null && songs != null && !songs.isEmpty()) {
|
||||
Collections.shuffle(songs);
|
||||
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> randomSongs = new java.util.ArrayList<>(songs);
|
||||
java.util.Collections.shuffle(randomSongs);
|
||||
|
||||
// Pic top-left
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), !songs.isEmpty() ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.from(requireContext(), !randomSongs.isEmpty() ? randomSongs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
||||
.into(bind.playlistCoverImageViewTopLeft);
|
||||
|
||||
// Pic top-right
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), songs.size() > 1 ? songs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.from(requireContext(), randomSongs.size() > 1 ? randomSongs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
||||
.into(bind.playlistCoverImageViewTopRight);
|
||||
|
||||
// Pic bottom-left
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), songs.size() > 2 ? songs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.from(requireContext(), randomSongs.size() > 2 ? randomSongs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
||||
.into(bind.playlistCoverImageViewBottomLeft);
|
||||
|
||||
// Pic bottom-right
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), songs.size() > 3 ? songs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.from(requireContext(), randomSongs.size() > 3 ? randomSongs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
||||
.into(bind.playlistCoverImageViewBottomRight);
|
||||
@@ -271,6 +273,11 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
if (songs != null) {
|
||||
bind.playlistSongCountLabel.setText(getString(R.string.playlist_song_count, songs.size()));
|
||||
long totalDuration = songs.stream().mapToLong(s -> s.getDuration() != null ? s.getDuration() : 0).sum();
|
||||
bind.playlistDurationLabel.setText(getString(R.string.playlist_duration, MusicUtil.getReadableDurationString(totalDuration, false)));
|
||||
}
|
||||
reapplyPlayback();
|
||||
});
|
||||
}
|
||||
@@ -291,6 +298,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
@Override
|
||||
public void onMediaLongClick(Bundle bundle) {
|
||||
bundle.putString(Constants.PLAYLIST_ID, playlistPageViewModel.getPlaylist().getId());
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
super.onStart();
|
||||
activity.setBottomNavigationBarVisibility(false);
|
||||
activity.setBottomSheetVisibility(false);
|
||||
activity.setNavigationDrawerLock(true);
|
||||
activity.setSystemBarsVisibility(!activity.isLandscape);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,6 +169,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
activity.setBottomSheetVisibility(true);
|
||||
activity.toggleNavigationDrawerLockOnOrientationChange();
|
||||
activity.setSystemBarsVisibility(!activity.isLandscape);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -229,6 +229,34 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
});
|
||||
|
||||
updateDownloadButtons();
|
||||
|
||||
String playlistId = requireArguments().getString(Constants.PLAYLIST_ID);
|
||||
int itemPosition = requireArguments().getInt(Constants.ITEM_POSITION, -1);
|
||||
|
||||
TextView removeFromPlaylist = view.findViewById(R.id.remove_from_playlist_text_view);
|
||||
if (playlistId != null && itemPosition != -1) {
|
||||
removeFromPlaylist.setVisibility(View.VISIBLE);
|
||||
removeFromPlaylist.setOnClickListener(v -> {
|
||||
songBottomSheetViewModel.removeFromPlaylist(playlistId, itemPosition, new com.cappielloantonio.tempo.repository.PlaylistRepository.AddToPlaylistCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_success, Toast.LENGTH_SHORT).show();
|
||||
dismissBottomSheet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_failure, Toast.LENGTH_SHORT).show();
|
||||
dismissBottomSheet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllSkipped() {
|
||||
dismissBottomSheet();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||
addToPlaylist.setOnClickListener(v -> {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.cappielloantonio.tempo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.security.KeyChain
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import okhttp3.internal.platform.Platform
|
||||
import java.net.Socket
|
||||
import java.security.KeyManagementException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509KeyManager
|
||||
|
||||
object ClientCertManager {
|
||||
|
||||
private const val TAG = "ClientCertManager"
|
||||
|
||||
val trustManager = Platform.get().platformTrustManager()
|
||||
var sslSocketFactory: SSLSocketFactory? = null
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
fun setupSslSocketFactory(context: Context) {
|
||||
sslSocketFactory = createSslSocketFactory(context)
|
||||
sslSocketFactory?.let {
|
||||
// HttpsURLConnection is used both by:
|
||||
// - Glide: in IPv6StringLoader
|
||||
// - ExoPlayer: in DefaultHttpDataSource
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSslSocketFactory(context: Context): SSLSocketFactory? {
|
||||
return try {
|
||||
val clientKeyManager = object : X509KeyManager {
|
||||
override fun getClientAliases(keyType: String?, issuers: Array<Principal>?) = null
|
||||
|
||||
override fun chooseClientAlias(
|
||||
keyType: Array<String>?,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
): String? {
|
||||
val clientCert = Preferences.getClientCert() ?: return null
|
||||
val server = Preferences.getServer() ?: return null
|
||||
return if (server.toUri().host == socket?.inetAddress?.hostName) {
|
||||
clientCert
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun getServerAliases(keyType: String?, issuers: Array<Principal>?) = null
|
||||
|
||||
override fun chooseServerAlias(
|
||||
keyType: String?,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
) = null
|
||||
|
||||
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
|
||||
val clientCert = Preferences.getClientCert()
|
||||
return if (alias == clientCert && clientCert != null) {
|
||||
KeyChain.getCertificateChain(
|
||||
context,
|
||||
clientCert
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun getPrivateKey(alias: String?): PrivateKey? {
|
||||
val clientCert = Preferences.getClientCert()
|
||||
return if (alias == clientCert && clientCert != null) {
|
||||
KeyChain.getPrivateKey(
|
||||
context,
|
||||
clientCert
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
|
||||
sslContext.socketFactory
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Log.e(TAG, "Failed setting mTLS", e)
|
||||
null
|
||||
} catch (e: KeyManagementException) {
|
||||
Log.e(TAG, "Failed setting mTLS", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ object Constants {
|
||||
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
||||
const val GENRE_OBJECT = "GENRE_OBJECT"
|
||||
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
||||
const val PLAYLIST_ID = "PLAYLIST_ID"
|
||||
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
||||
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
||||
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
||||
|
||||
@@ -29,6 +29,8 @@ import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@UnstableApi
|
||||
@@ -78,12 +80,33 @@ public final class DownloadUtil {
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getHttpDataSourceFactoryForRadio() {
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
|
||||
// Create a factory with ICY metadata support for radio streams
|
||||
Map<String, String> defaultRequestProperties = new HashMap<>();
|
||||
defaultRequestProperties.put("Icy-MetaData", "1");
|
||||
defaultRequestProperties.put("User-Agent", "Tempus/1.0");
|
||||
|
||||
return new DefaultHttpDataSource
|
||||
.Factory()
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
.setDefaultRequestProperties(defaultRequestProperties);
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactoryForRadio(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactoryForRadio());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||
.setCache(getStreamingCache(context))
|
||||
|
||||
@@ -20,10 +20,15 @@ class DynamicMediaSourceFactory(
|
||||
) : MediaSource.Factory {
|
||||
|
||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||
val mediaType: String? = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||
// Detect radio streams in a backwards-compatible way.
|
||||
// Older Tempus versions tagged radio items via MediaMetadata extras
|
||||
// (`type == MEDIA_TYPE_RADIO`), while newer upstream changes use an
|
||||
// "ir-" mediaId prefix. Support BOTH so radio works after rebases.
|
||||
val mediaType = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||
val isRadio = mediaType == Constants.MEDIA_TYPE_RADIO || mediaItem.mediaId.startsWith("ir-")
|
||||
|
||||
val streamingCacheSize = Preferences.getStreamingCacheSize()
|
||||
val bypassCache = mediaType == Constants.MEDIA_TYPE_RADIO
|
||||
val bypassCache = isRadio
|
||||
|
||||
val useUpstream = when {
|
||||
streamingCacheSize.toInt() == 0 -> true
|
||||
@@ -32,7 +37,10 @@ class DynamicMediaSourceFactory(
|
||||
else -> true
|
||||
}
|
||||
|
||||
val dataSourceFactory: DataSource.Factory = if (useUpstream) {
|
||||
val dataSourceFactory: DataSource.Factory = if (bypassCache) {
|
||||
// For radio streams, use a DataSourceFactory with ICY metadata support
|
||||
DownloadUtil.getUpstreamDataSourceFactoryForRadio(context)
|
||||
} else if (useUpstream) {
|
||||
DownloadUtil.getUpstreamDataSourceFactory(context)
|
||||
} else {
|
||||
DownloadUtil.getCacheDataSourceFactory(context)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -25,6 +26,7 @@ import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class MappingUtil {
|
||||
@@ -207,18 +209,34 @@ public class MappingUtil {
|
||||
|
||||
public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) {
|
||||
Uri uri = Uri.parse(internetRadioStation.getStreamUrl());
|
||||
Uri artworkUri = null;
|
||||
String homePageUrl = internetRadioStation.getHomePageUrl();
|
||||
String coverArtId = null;
|
||||
|
||||
if (homePageUrl != null && !homePageUrl.isEmpty() && MusicUtil.isImageUrl(homePageUrl)) {
|
||||
String encodedUrl = Base64.encodeToString(homePageUrl.getBytes(StandardCharsets.UTF_8),
|
||||
Base64.URL_SAFE | Base64.NO_WRAP);
|
||||
coverArtId = "ir_" + encodedUrl;
|
||||
artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", internetRadioStation.getId());
|
||||
bundle.putString("title", internetRadioStation.getName());
|
||||
bundle.putString("stationName", internetRadioStation.getName());
|
||||
bundle.putString("uri", uri.toString());
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
||||
bundle.putString("coverArtId", coverArtId);
|
||||
if (homePageUrl != null) {
|
||||
bundle.putString("homepageUrl", homePageUrl);
|
||||
}
|
||||
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(internetRadioStation.getId())
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(internetRadioStation.getName())
|
||||
.setArtworkUri(artworkUri)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
@@ -288,13 +306,24 @@ public class MappingUtil {
|
||||
}
|
||||
|
||||
private static Uri getUri(Child media) {
|
||||
// Check if it's in our local SQL Database
|
||||
DownloadRepository repo = new DownloadRepository();
|
||||
Download localDownload = repo.getDownload(media.getId());
|
||||
|
||||
if (localDownload != null && localDownload.getDownloadUri() != null && !localDownload.getDownloadUri().isEmpty()) {
|
||||
Log.d(TAG, "Playing local file for: " + media.getTitle());
|
||||
return Uri.parse(localDownload.getDownloadUri());
|
||||
}
|
||||
|
||||
// Legacy check for external directory, i think this was broken/buggy
|
||||
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||
Uri local = ExternalAudioReader.getUri(media);
|
||||
return local != null ? local : MusicUtil.getStreamUri(media.getId());
|
||||
if (local != null) return local;
|
||||
}
|
||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
||||
? getDownloadUri(media.getId())
|
||||
: MusicUtil.getStreamUri(media.getId());
|
||||
|
||||
// Fallback to streaming
|
||||
Log.d(TAG, "No local file found. Streaming: " + media.getTitle());
|
||||
return MusicUtil.getStreamUri(media.getId());
|
||||
}
|
||||
|
||||
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
||||
@@ -318,4 +347,4 @@ public class MappingUtil {
|
||||
}
|
||||
ExternalAudioReader.getRefreshEvents().observe(owner, event -> onRefresh.run());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public class MusicUtil {
|
||||
if (params.containsKey("c") && params.get("c") != null)
|
||||
uri.append("&c=").append(params.get("c"));
|
||||
|
||||
String selectedBitrate = getBitratePreference();
|
||||
String selectedFormat = getTranscodingFormatPreference();
|
||||
Log.i(TAG, "DEBUG: Requesting Format: " + selectedFormat + " at Bitrate: " + selectedBitrate);
|
||||
|
||||
if (!Preferences.isServerPrioritized())
|
||||
uri.append("&maxBitRate=").append(getBitratePreference());
|
||||
if (!Preferences.isServerPrioritized())
|
||||
@@ -73,7 +77,17 @@ public class MusicUtil {
|
||||
}
|
||||
|
||||
public static Uri updateStreamUri(Uri uri) {
|
||||
if (uri == null) return null;
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
// If it is local (content:// or file://), return it IMMEDIATELY.
|
||||
// This prevents the code below from appending &maxBitRate to a local path.
|
||||
if (scheme != null && (scheme.equals("content") || scheme.equals("file"))) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
String s = uri.toString();
|
||||
|
||||
Matcher m1 = BITRATE_PATTERN.matcher(s);
|
||||
s = m1.replaceAll("");
|
||||
Matcher m2 = FORMAT_PATTERN.matcher(s);
|
||||
@@ -157,7 +171,6 @@ public class MusicUtil {
|
||||
return Uri.parse(uri.toString());
|
||||
}
|
||||
|
||||
|
||||
public static String getReadableDurationString(Long duration, boolean millis) {
|
||||
long lenght = duration != null ? duration : 0;
|
||||
|
||||
@@ -303,13 +316,17 @@ public class MusicUtil {
|
||||
|
||||
if (network == null || networkCapabilities == null) return "raw";
|
||||
|
||||
String format;
|
||||
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
return Preferences.getAudioTranscodeFormatWifi();
|
||||
format = Preferences.getAudioTranscodeFormatWifi();
|
||||
Log.d(TAG, "DEBUG: Using WIFI Format: " + format);
|
||||
} else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
return Preferences.getAudioTranscodeFormatMobile();
|
||||
format = Preferences.getAudioTranscodeFormatMobile();
|
||||
Log.d(TAG, "DEBUG: Using MOBILE Format: " + format);
|
||||
} else {
|
||||
return Preferences.getAudioTranscodeFormatWifi();
|
||||
format = Preferences.getAudioTranscodeFormatWifi();
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
public static String getBitratePreferenceForDownload() {
|
||||
@@ -360,4 +377,15 @@ public class MusicUtil {
|
||||
|
||||
toFilter.addAll(filtered);
|
||||
}
|
||||
|
||||
public static boolean isImageUrl(String url) {
|
||||
if (url == null || url.isEmpty())
|
||||
return false;
|
||||
String path = url.toLowerCase().trim().split("\\?")[0];
|
||||
|
||||
return path.endsWith(".jpg") || path.endsWith(".jpeg") ||
|
||||
path.endsWith(".png") || path.endsWith(".webp") ||
|
||||
path.endsWith(".gif") || path.endsWith(".bmp") ||
|
||||
path.endsWith(".svg");
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ object Preferences {
|
||||
private const val TOKEN = "token"
|
||||
private const val SALT = "salt"
|
||||
private const val LOW_SECURITY = "low_security"
|
||||
private const val CLIENT_CERT = "client_cert"
|
||||
private const val BATTERY_OPTIMIZATION = "battery_optimization"
|
||||
private const val SERVER_ID = "server_id"
|
||||
private const val OPEN_SUBSONIC = "open_subsonic"
|
||||
@@ -24,12 +25,15 @@ object Preferences {
|
||||
private const val IN_USE_SERVER_ADDRESS = "in_use_server_address"
|
||||
private const val NEXT_SERVER_SWITCH = "next_server_switch"
|
||||
private const val PLAYBACK_SPEED = "playback_speed"
|
||||
private const val BITRATE_VISIBLE = "bitrate_visible"
|
||||
private const val SKIP_SILENCE = "skip_silence"
|
||||
private const val SHUFFLE_MODE = "shuffle_mode"
|
||||
private const val REPEAT_MODE = "repeat_mode"
|
||||
private const val IMAGE_CACHE_SIZE = "image_cache_size"
|
||||
private const val STREAMING_CACHE_SIZE = "streaming_cache_size"
|
||||
private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row"
|
||||
private const val ENABLE_DRAWER_ON_PORTRAIT = "enable_drawer_on_portrait"
|
||||
private const val HIDE_BOTTOM_NAVBAR_ON_PORTRAIT = "hide_bottom_navbar_on_portrait"
|
||||
private const val IMAGE_SIZE = "image_size"
|
||||
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
|
||||
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
|
||||
@@ -88,8 +92,17 @@ object Preferences {
|
||||
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
||||
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
|
||||
|
||||
|
||||
@JvmStatic
|
||||
private const val AA_ALBUM_VIEW = "androidauto_album_view"
|
||||
private const val AA_HOME_VIEW = "androidauto_home_view"
|
||||
private const val AA_PLAYLIST_VIEW = "androidauto_playlist_view"
|
||||
private const val AA_PODCAST_VIEW = "androidauto_podcast_view"
|
||||
private const val AA_RADIO_VIEW = "androidauto_radio_view"
|
||||
private const val AA_FIRST_TAB = "androidauto_first_tab"
|
||||
private const val AA_SECOND_TAB = "androidauto_second_tab"
|
||||
private const val AA_THIRD_TAB = "androidauto_third_tab"
|
||||
private const val AA_FOURTH_TAB = "androidauto_fourth_tab"
|
||||
|
||||
@JvmStatic
|
||||
fun getServer(): String? {
|
||||
return App.getInstance().preferences.getString(SERVER, null)
|
||||
}
|
||||
@@ -162,6 +175,16 @@ object Preferences {
|
||||
App.getInstance().preferences.edit().putBoolean(LOW_SECURITY, isLowSecurity).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getClientCert(): String? {
|
||||
return App.getInstance().preferences.getString(CLIENT_CERT, null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setClientCert(clientCert: String?) {
|
||||
App.getInstance().preferences.edit().putString(CLIENT_CERT, clientCert).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getServerId(): String? {
|
||||
return App.getInstance().preferences.getString(SERVER_ID, null)
|
||||
@@ -270,6 +293,16 @@ object Preferences {
|
||||
App.getInstance().preferences.edit().putFloat(PLAYBACK_SPEED, playbackSpeed).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getBitrateVisible(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(BITRATE_VISIBLE, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setBitrateVisible(bitrateVisible: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(BITRATE_VISIBLE, bitrateVisible).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isSkipSilenceMode(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(SKIP_SILENCE, false)
|
||||
@@ -310,6 +343,16 @@ object Preferences {
|
||||
return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEnableDrawerOnPortrait(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(ENABLE_DRAWER_ON_PORTRAIT, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getHideBottomNavbarOnPortrait(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(HIDE_BOTTOM_NAVBAR_ON_PORTRAIT, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getImageSize(): Int {
|
||||
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()
|
||||
@@ -724,4 +767,50 @@ object Preferences {
|
||||
fun setArtistDisplayBiography(displayBiographyEnabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(ARTIST_DISPLAY_BIOGRAPHY, displayBiographyEnabled).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAndroidAutoAlbumViewEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(AA_ALBUM_VIEW, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAndroidAutoHomeViewEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(AA_HOME_VIEW, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAndroidAutoPlaylistViewEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(AA_PLAYLIST_VIEW, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAndroidAutoPodcastViewEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(AA_PODCAST_VIEW, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAndroidAutoRadioViewEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(AA_RADIO_VIEW, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAndroidAutoFirstTab(): Int {
|
||||
return App.getInstance().preferences.getString(AA_FIRST_TAB, "0")!!.toInt()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAndroidAutoSecondTab(): Int {
|
||||
return App.getInstance().preferences.getString(AA_SECOND_TAB, "1")!!.toInt()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAndroidAutoThirdTab(): Int {
|
||||
return App.getInstance().preferences.getString(AA_THIRD_TAB, "2")!!.toInt()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAndroidAutoFourthTab(): Int {
|
||||
return App.getInstance().preferences.getString(AA_FOURTH_TAB, "3")!!.toInt()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,12 +33,18 @@ class TranscodingMediaSource(
|
||||
|
||||
init {
|
||||
val extras = mediaItem.mediaMetadata.extras
|
||||
if (extras != null && extras.containsKey("duration")) {
|
||||
val uri = mediaItem.localConfiguration?.uri
|
||||
val isLocal = uri?.scheme == "content" || uri?.scheme == "file"
|
||||
|
||||
// Only apply the override if it's NOT a local file
|
||||
if (!isLocal && extras != null && extras.containsKey("duration")) {
|
||||
val seconds = extras.getInt("duration")
|
||||
if (seconds > 0) {
|
||||
durationUs = Util.msToUs(seconds * 1000L)
|
||||
}
|
||||
}
|
||||
|
||||
currentSource = progressiveMediaSourceFactory.createMediaSource(mediaItem)
|
||||
}
|
||||
|
||||
override fun getMediaItem() = mediaItem
|
||||
|
||||
@@ -20,14 +20,36 @@ public class PlaylistPageViewModel extends AndroidViewModel {
|
||||
private Playlist playlist;
|
||||
private boolean isOffline;
|
||||
|
||||
private final MutableLiveData<List<Child>> songLiveList = new MutableLiveData<>();
|
||||
|
||||
public PlaylistPageViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
||||
playlistRepository = new PlaylistRepository();
|
||||
playlistRepository.getPlaylistUpdateTrigger().observeForever(needsRefresh -> {
|
||||
if (needsRefresh != null && needsRefresh && playlist != null) {
|
||||
refreshSongs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getPlaylistSongLiveList() {
|
||||
return playlistRepository.getPlaylistSongs(playlist.getId());
|
||||
if (songLiveList.getValue() == null && playlist != null) {
|
||||
refreshSongs();
|
||||
}
|
||||
return songLiveList;
|
||||
}
|
||||
|
||||
private void refreshSongs() {
|
||||
if (playlist == null) return;
|
||||
LiveData<List<Child>> remoteData = playlistRepository.getPlaylistSongs(playlist.getId());
|
||||
remoteData.observeForever(new androidx.lifecycle.Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
songLiveList.postValue(songs);
|
||||
remoteData.removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Playlist getPlaylist() {
|
||||
@@ -35,7 +57,10 @@ public class PlaylistPageViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public void setPlaylist(Playlist playlist) {
|
||||
this.playlist = playlist;
|
||||
if (this.playlist == null || !this.playlist.getId().equals(playlist.getId())) {
|
||||
this.playlist = playlist;
|
||||
this.songLiveList.setValue(null); // Clear old data immediately
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isPinned(LifecycleOwner owner) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||
import com.cappielloantonio.tempo.repository.SharingRepository;
|
||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
@@ -39,6 +40,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||
private final ArtistRepository artistRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final SharingRepository sharingRepository;
|
||||
private final PlaylistRepository playlistRepository;
|
||||
|
||||
private Child song;
|
||||
|
||||
@@ -52,6 +54,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||
artistRepository = new ArtistRepository();
|
||||
favoriteRepository = new FavoriteRepository();
|
||||
sharingRepository = new SharingRepository();
|
||||
playlistRepository = new PlaylistRepository();
|
||||
}
|
||||
|
||||
public Child getSong() {
|
||||
@@ -62,6 +65,10 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||
this.song = song;
|
||||
}
|
||||
|
||||
public void removeFromPlaylist(String playlistId, int index, PlaylistRepository.AddToPlaylistCallback callback) {
|
||||
playlistRepository.removeSongFromPlaylist(playlistId, index, callback);
|
||||
}
|
||||
|
||||
public void setFavorite(Context context) {
|
||||
if (song.getStarred() != null) {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
|
||||
5
app/src/main/res/drawable/ic_aa_added_album.xml
Normal file
5
app/src/main/res/drawable/ic_aa_added_album.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M720,800L720,680L600,680L600,600L720,600L720,480L800,480L800,600L920,600L920,680L800,680L800,800L720,800ZM120,840Q87,840 63.5,816.5Q40,793 40,760L40,200Q40,167 63.5,143.5Q87,120 120,120L680,120Q713,120 736.5,143.5Q760,167 760,200L760,400L680,400L680,320L120,320L120,760Q120,760 120,760Q120,760 120,760L640,760L640,840L120,840ZM120,240L680,240L680,200Q680,200 680,200Q680,200 680,200L120,200Q120,200 120,200Q120,200 120,200L120,240ZM120,240L120,200Q120,200 120,200Q120,200 120,200L120,200Q120,200 120,200Q120,200 120,200L120,240Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_added_title.xml
Normal file
5
app/src/main/res/drawable/ic_aa_added_title.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,468Q821,459 801,452.5Q781,446 760,443L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L442,760Q445,782 451.5,802Q458,822 467,840L200,840ZM200,720Q200,731 200,740.5Q200,750 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,443Q200,441 200,440.5Q200,440 200,440Q200,440 200,522Q200,604 200,720ZM280,680L443,680Q446,659 452.5,639Q459,619 467,600L280,600L280,680ZM280,520L524,520Q556,490 595.5,470Q635,450 680,443L680,440L280,440L280,520ZM280,360L680,360L680,280L280,280L280,360ZM720,920Q637,920 578.5,861.5Q520,803 520,720Q520,637 578.5,578.5Q637,520 720,520Q803,520 861.5,578.5Q920,637 920,720Q920,803 861.5,861.5Q803,920 720,920ZM700,840L740,840L740,740L840,740L840,700L740,700L740,600L700,600L700,700L600,700L600,740L700,740L700,840Z"/>
|
||||
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_aa_albums.xml
Normal file
11
app/src/main/res/drawable/ic_aa_albums.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960"
|
||||
android:width="24dp">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,660Q555,660 607.5,607.5Q660,555 660,480Q660,405 607.5,352.5Q555,300 480,300Q405,300 352.5,352.5Q300,405 300,480Q300,555 352.5,607.5Q405,660 480,660ZM451.5,508.5Q440,497 440,480Q440,463 451.5,451.5Q463,440 480,440Q497,440 508.5,451.5Q520,463 520,480Q520,497 508.5,508.5Q497,520 480,520Q463,520 451.5,508.5ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_aa_artists.xml
Normal file
11
app/src/main/res/drawable/ic_aa_artists.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960"
|
||||
android:width="24dp">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M740,400L880,400L880,480L800,480L800,700Q800,742 771,771Q742,800 700,800Q658,800 629,771Q600,742 600,700Q600,658 629,629Q658,600 700,600Q708,600 718,601.5Q728,603 740,608L740,400ZM120,800L120,688Q120,653 137.5,625Q155,597 184,582Q246,551 310,535.5Q374,520 440,520Q482,520 523.5,526.5Q565,533 607,546Q587,558 571,575Q555,592 543,612Q517,606 491.5,603Q466,600 440,600Q383,600 328,614Q273,628 220,654Q211,659 205.5,668Q200,677 200,688L200,720L521,720Q523,740 530.5,760Q538,780 551,800L120,800ZM327,433Q280,386 280,320Q280,254 327,207Q374,160 440,160Q506,160 553,207Q600,254 600,320Q600,386 553,433Q506,480 440,480Q374,480 327,433ZM496.5,376.5Q520,353 520,320Q520,287 496.5,263.5Q473,240 440,240Q407,240 383.5,263.5Q360,287 360,320Q360,353 383.5,376.5Q407,400 440,400Q473,400 496.5,376.5ZM440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320ZM440,720L440,720L440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_folders.xml
Normal file
5
app/src/main/res/drawable/ic_aa_folders.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M120,840Q87,840 63.5,816.5Q40,793 40,760L40,240L120,240L120,760Q120,760 120,760Q120,760 120,760L800,760L800,840L120,840ZM280,680Q247,680 223.5,656.5Q200,633 200,600L200,160Q200,127 223.5,103.5Q247,80 280,80L480,80L560,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,600Q920,633 896.5,656.5Q873,680 840,680L280,680ZM280,600L840,600Q840,600 840,600Q840,600 840,600L840,240Q840,240 840,240Q840,240 840,240L527,240L447,160L280,160Q280,160 280,160Q280,160 280,160L280,600Q280,600 280,600Q280,600 280,600ZM280,600Q280,600 280,600Q280,600 280,600L280,160Q280,160 280,160Q280,160 280,160L280,160L280,240L280,240Q280,240 280,240Q280,240 280,240L280,600Q280,600 280,600Q280,600 280,600Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_for_you.xml
Normal file
5
app/src/main/res/drawable/ic_aa_for_you.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M649,463.5Q737,447 800,420L800,820Q740,847 654,863.5Q568,880 480,880Q392,880 306,863.5Q220,847 160,820L160,420Q223,447 311,463.5Q399,480 480,480Q561,480 649,463.5ZM720,760L720,530Q670,544 604.5,552Q539,560 480,560Q421,560 355.5,552Q290,544 240,530L240,760Q290,778 355,789Q420,800 480,800Q540,800 605,789Q670,778 720,760ZM593,127Q640,174 640,240Q640,306 593,353Q546,400 480,400Q414,400 367,353Q320,306 320,240Q320,174 367,127Q414,80 480,80Q546,80 593,127ZM536.5,296.5Q560,273 560,240Q560,207 536.5,183.5Q513,160 480,160Q447,160 423.5,183.5Q400,207 400,240Q400,273 423.5,296.5Q447,320 480,320Q513,320 536.5,296.5ZM480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240ZM480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665L480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_home.xml
Normal file
5
app/src/main/res/drawable/ic_aa_home.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_mostplayed.xml
Normal file
5
app/src/main/res/drawable/ic_aa_mostplayed.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M400,640L640,480L400,320L400,640ZM324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,437 89,395.5Q98,354 115,315L177,377Q169,403 164.5,428.5Q160,454 160,480Q160,614 253,707Q346,800 480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q453,160 427.5,164.5Q402,169 377,177L316,116Q356,98 396,89Q436,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Q397,880 324,848.5ZM177.5,262.5Q160,245 160,220Q160,195 177.5,177.5Q195,160 220,160Q245,160 262.5,177.5Q280,195 280,220Q280,245 262.5,262.5Q245,280 220,280Q195,280 177.5,262.5ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_other.xml
Normal file
5
app/src/main/res/drawable/ic_aa_other.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M120,880L120,800L840,800L840,880L120,880ZM120,640L120,560L840,560L840,640L120,640ZM120,400L120,320L840,320L840,400L120,400ZM120,160L120,80L840,80L840,160L120,160Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_playlist.xml
Normal file
5
app/src/main/res/drawable/ic_aa_playlist.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_podcasts.xml
Normal file
5
app/src/main/res/drawable/ic_aa_podcasts.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440,880L440,549Q422,538 411,520.5Q400,503 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,503 549,521Q538,539 520,549L520,880L440,880ZM204,770Q147,715 113.5,640.5Q80,566 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,566 846.5,641Q813,716 756,770L700,714Q746,670 773,609.5Q800,549 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,549 187,609Q214,669 261,713L204,770ZM317,657Q282,624 261,578.5Q240,533 240,480Q240,380 310,310Q380,240 480,240Q580,240 650,310Q720,380 720,480Q720,533 699,579Q678,625 643,657L586,600Q611,577 625.5,546Q640,515 640,480Q640,414 593,367Q546,320 480,320Q414,320 367,367Q320,414 320,480Q320,516 334.5,546.5Q349,577 374,600L317,657Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_radio.xml
Normal file
5
app/src/main/res/drawable/ic_aa_radio.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,320Q80,295 93.5,275Q107,255 130,246L636,40L662,106L332,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,800Q880,833 856.5,856.5Q833,880 800,880L160,880ZM160,800L800,800Q800,800 800,800Q800,800 800,800L800,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM391,731Q420,702 420,660Q420,618 391,589Q362,560 320,560Q278,560 249,589Q220,618 220,660Q220,702 249,731Q278,760 320,760Q362,760 391,731ZM160,440L640,440L640,360L720,360L720,440L800,440L800,320Q800,320 800,320Q800,320 800,320L160,320Q160,320 160,320Q160,320 160,320L160,440ZM160,800Q160,800 160,800Q160,800 160,800L160,520L160,520L160,800Q160,800 160,800Q160,800 160,800Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_random.xml
Normal file
5
app/src/main/res/drawable/ic_aa_random.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M280,800L80,600L280,400L336,457L233,560L520,560L520,640L233,640L336,743L280,800ZM680,560L624,503L727,400L440,400L440,320L727,320L624,217L680,160L880,360L680,560Z"/>
|
||||
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_aa_recent.xml
Normal file
12
app/src/main/res/drawable/ic_aa_recent.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="
|
||||
M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z
|
||||
|
||||
"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_recent_title.xml
Normal file
5
app/src/main/res/drawable/ic_aa_recent_title.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M380,660L380,300L660,480L380,660ZM480,920Q372,920 277.5,870.5Q183,821 120,732L120,840L40,840L40,600L280,600L280,680L182,680Q233,755 311.5,797.5Q390,840 480,840Q595,840 688.5,774Q782,708 820,599L898,617Q853,753 738,836.5Q623,920 480,920ZM42,440Q49,373 74,311.5Q99,250 143,198L200,255Q168,296 148,342.5Q128,389 123,440L42,440ZM256,199L199,142Q252,98 313,72.5Q374,47 440,42L440,122Q389,127 343,147Q297,167 256,199ZM705,199Q664,167 617.5,147Q571,127 520,122L520,42Q587,48 648.5,73Q710,98 762,142L705,199ZM838,440Q833,389 813,342.5Q793,296 761,255L818,198Q862,250 887,311.5Q912,373 918,440L838,440Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_star_album.xml
Normal file
5
app/src/main/res/drawable/ic_aa_star_album.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M320,720L480,598L640,720L580,522L740,408L544,408L480,200L416,408L220,408L380,522L320,720ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_aa_star_title.xml
Normal file
5
app/src/main/res/drawable/ic_aa_star_title.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M489,500L580,445L671,500L647,396L727,327L622,318L580,220L538,318L433,327L513,396L489,500ZM508,760L732,760Q725,786 708,802Q691,818 664,822L228,875Q195,880 168.5,859.5Q142,839 138,806L85,369Q81,336 101,310Q121,284 154,280L200,274L200,354L164,359Q164,359 164,359Q164,359 164,359L218,796Q218,796 218,796Q218,796 218,796L508,760ZM360,680Q327,680 303.5,656.5Q280,633 280,600L280,160Q280,127 303.5,103.5Q327,80 360,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,600Q880,633 856.5,656.5Q833,680 800,680L360,680ZM360,600L800,600Q800,600 800,600Q800,600 800,600L800,160Q800,160 800,160Q800,160 800,160L360,160Q360,160 360,160Q360,160 360,160L360,600Q360,600 360,600Q360,600 360,600ZM580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380ZM218,796L218,796L218,796L218,796L218,796Q218,796 218,796Q218,796 218,796Z"/>
|
||||
|
||||
</vector>
|
||||
23
app/src/main/res/drawable/ic_albums.xml
Normal file
23
app/src/main/res/drawable/ic_albums.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2,17.4V2.6C2,2.269 2.269,2 2.6,2H17.4C17.731,2 18,2.269 18,2.6V17.4C18,17.731 17.731,18 17.4,18H2.6C2.269,18 2,17.731 2,17.4Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M8,22H21.4C21.731,22 22,21.731 22,21.4V8"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M11,12.5C11,13.328 10.328,14 9.5,14C8.672,14 8,13.328 8,12.5C8,11.672 8.672,11 9.5,11C10.328,11 11,11.672 11,12.5ZM11,12.5V6.6C11,6.269 11.269,6 11.6,6H13"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
32
app/src/main/res/drawable/ic_artists.xml
Normal file
32
app/src/main/res/drawable/ic_artists.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M1,20V19C1,15.134 4.134,12 8,12V12C11.866,12 15,15.134 15,19V20"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M13,14V14C13,11.239 15.239,9 18,9V9C20.761,9 23,11.239 23,14V14.5"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,12C10.209,12 12,10.209 12,8C12,5.791 10.209,4 8,4C5.791,4 4,5.791 4,8C4,10.209 5.791,12 8,12Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M18,9C19.657,9 21,7.657 21,6C21,4.343 19.657,3 18,3C16.343,3 15,4.343 15,6C15,7.657 16.343,9 18,9Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_genres.xml
Normal file
11
app/src/main/res/drawable/ic_genres.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="49dp"
|
||||
android:height="49dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3,17.4V6.6C3,6.269 3.269,6 3.6,6H16.679C16.879,6 17.067,6.1 17.178,6.267L20.778,11.667C20.913,11.869 20.913,12.131 20.778,12.333L17.178,17.733C17.067,17.9 16.879,18 16.679,18H3.6C3.269,18 3,17.731 3,17.4Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
||||
33
app/src/main/res/drawable/ic_playlist.xml
Normal file
33
app/src/main/res/drawable/ic_playlist.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="49dp"
|
||||
android:height="49dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2,11L16,11"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M2,17L13,17"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M2,5L20,5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M20,18.5C20,19.328 19.328,20 18.5,20C17.672,20 17,19.328 17,18.5C17,17.672 17.672,17 18.5,17C19.328,17 20,17.672 20,18.5ZM20,18.5V10.6C20,10.269 20.269,10 20.6,10H22"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@@ -1,16 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -18,20 +17,17 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="75dp"
|
||||
android:id="@+id/bottom_navigation_frame"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="75dp"
|
||||
android:rotation="90"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:visibility="gone"
|
||||
android:rotation="90"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
@@ -42,7 +38,6 @@
|
||||
android:layout_weight="1"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
@@ -52,9 +47,16 @@
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
|
||||
app:layout_behavior="@string/bottom_sheet_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:menu="@menu/nav_drawer"
|
||||
app:headerLayout="@layout/nav_drawer_header" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/offline_mode_text_view"
|
||||
style="@style/NoConnectionTextView"
|
||||
@@ -64,5 +66,4 @@
|
||||
android:text="@string/activity_info_offline_mode"
|
||||
android:textSize="6sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/now_playing_media_controller_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -23,30 +24,42 @@
|
||||
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/player_media_extension"
|
||||
style="@style/Widget.Material3.Chip.Suggestion"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_media_quality_sector_center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:clickable="false"
|
||||
android:text="Unknown"
|
||||
app:chipStrokeWidth="0dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/player_media_bitrate"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"/>
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/player_media_bitrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_extension"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
|
||||
app:layout_constraintStart_toEndOf="@id/player_media_extension"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
<TextView
|
||||
android:id="@+id/player_media_bitrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_extension"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
|
||||
app:layout_constraintStart_toEndOf="@id/player_media_extension"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/player_media_extension"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:checked="true"
|
||||
android:focusable="true"
|
||||
android:text="Unknown"
|
||||
app:chipStrokeWidth="0dp"
|
||||
app:chipBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/player_media_bitrate"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/player_info_track"
|
||||
@@ -57,11 +70,25 @@
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_extension"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_quality_sector_center"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_quality_sector_center"
|
||||
app:srcCompat="@drawable/ic_info_stream"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/player_playback_speed_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
app:cornerRadius="30dp"
|
||||
app:tint="?attr/colorOnPrimaryContainer"
|
||||
tools:layout_editor_absoluteX="36dp"
|
||||
tools:layout_editor_absoluteY="2dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
@@ -244,23 +271,6 @@
|
||||
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
|
||||
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/player_playback_speed_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
app:cornerRadius="30dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
|
||||
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
|
||||
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
|
||||
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exo_shuffle"
|
||||
android:layout_width="32dp"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
@@ -35,11 +35,31 @@
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:visibility="gone"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<!--
|
||||
This FrameLayout id is always called,
|
||||
if removed the app crashes
|
||||
-->
|
||||
<FrameLayout
|
||||
android:id="@+id/bottom_navigation_frame"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:menu="@menu/nav_drawer"
|
||||
app:headerLayout="@layout/nav_drawer_header" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/offline_mode_text_view"
|
||||
style="@style/NoConnectionTextView"
|
||||
@@ -47,8 +67,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/activity_info_offline_mode"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="6sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
@@ -164,6 +164,20 @@
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/song_bottom_sheet_remove" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/remove_from_playlist_text_view"
|
||||
style="@style/LabelMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/song_bottom_sheet_remove_from_playlist"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/add_to_playlist_text_view"
|
||||
style="@style/LabelMedium"
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/playlist_dialog_chooser_visibility_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:checked="false"
|
||||
android:showText="false"
|
||||
android:text="@string/playlist_chooser_dialog_visibility_switch_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_dialog_chooser_visibility_summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/playlist_chooser_dialog_visibility_summary"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/no_playlists_created_text_view"
|
||||
style="@style/TitleMedium"
|
||||
@@ -23,4 +43,35 @@
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_bar"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="bottom|center_horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:weightSum="2"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/playlist_chooser_dialog_create_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="start"
|
||||
android:text="@string/playlist_chooser_dialog_create_button" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/playlist_chooser_dialog_cancel_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/playlist_chooser_dialog_cancel_button"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -129,6 +129,25 @@
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/server_signup_dialog_action_low_security" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:id="@+id/client_cert_text_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:textColorHint="?android:textColorHint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/client_cert_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusableInTouchMode="false"
|
||||
android:hint="@string/server_signup_dialog_hint_client_certificate"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:textCursorDrawable="@null" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -131,6 +131,33 @@
|
||||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/station_info_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/station_key_sector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="4"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/track_info_station" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/station_value_sector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="7"
|
||||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_library_to_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/fragment_library_nested_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -77,21 +83,41 @@
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
<!-- Refreshable area -->
|
||||
<LinearLayout
|
||||
android:id="@+id/album_catalogue_sample_text_view_refreshable"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_album" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:alpha="0.4"
|
||||
android:src="@drawable/ic_refresh"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_album" />
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_catalogue_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_album_see_all_button" />
|
||||
@@ -130,22 +156,41 @@
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
<!-- Refreshable area -->
|
||||
<LinearLayout
|
||||
android:id="@+id/artist_catalogue_sample_text_view_refreshable"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_artist" />
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_artist" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:alpha="0.4"
|
||||
android:src="@drawable/ic_refresh"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artist_catalogue_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_artist_see_all_button" />
|
||||
@@ -184,25 +229,45 @@
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
<!-- Refreshable area -->
|
||||
<LinearLayout
|
||||
android:id="@+id/genre_catalogue_sample_text_view_refreshable"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_genre" />
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_genre" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:alpha="0.4"
|
||||
android:src="@drawable/ic_refresh"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/genre_catalogue_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_genre_see_all_button" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
@@ -236,21 +301,41 @@
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
<!-- Refreshable area -->
|
||||
<LinearLayout
|
||||
android:id="@+id/playlist_catalogue_sample_text_view_refreshable"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_playlist" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:alpha="0.4"
|
||||
android:src="@drawable/ic_refresh"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_playlist" />
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_catalogue_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_playlist_see_all_button" />
|
||||
@@ -270,4 +355,5 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -33,30 +33,42 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/player_media_extension"
|
||||
style="@style/Widget.Material3.Chip.Suggestion"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_media_quality_sector_center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:clickable="false"
|
||||
android:text="Unknown"
|
||||
app:chipStrokeWidth="0dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/player_media_bitrate"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"/>
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/player_media_bitrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_extension"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
|
||||
app:layout_constraintStart_toEndOf="@id/player_media_extension"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
<TextView
|
||||
android:id="@+id/player_media_bitrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_extension"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
|
||||
app:layout_constraintStart_toEndOf="@id/player_media_extension"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/player_media_extension"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:checked="true"
|
||||
android:focusable="true"
|
||||
android:text="Unknown"
|
||||
app:chipStrokeWidth="0dp"
|
||||
app:chipBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/player_media_bitrate"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/player_info_track"
|
||||
@@ -67,8 +79,8 @@
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_extension"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
|
||||
app:layout_constraintTop_toTopOf="@id/player_media_quality_sector_center"
|
||||
app:layout_constraintBottom_toBottomOf="@id/player_media_quality_sector_center"
|
||||
app:srcCompat="@drawable/ic_info_stream"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
|
||||
31
app/src/main/res/layout/nav_drawer_header.xml
Normal file
31
app/src/main/res/layout/nav_drawer_header.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingTop="30dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/ic_toolbar_tempo" />
|
||||
|
||||
<TextView
|
||||
style="@style/HeadlineMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/app_name" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,15 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/homeFragment"
|
||||
android:icon="@drawable/ic_home_land"
|
||||
android:title="@string/menu_home_label" />
|
||||
android:icon="@drawable/ic_home_land" />
|
||||
<item
|
||||
android:id="@+id/libraryFragment"
|
||||
android:icon="@drawable/ic_graphic_eq_land"
|
||||
android:title="@string/menu_library_label" />
|
||||
android:icon="@drawable/ic_graphic_eq_land" />
|
||||
<item
|
||||
android:id="@+id/downloadFragment"
|
||||
android:icon="@drawable/ic_play_for_work_land"
|
||||
android:title="@string/menu_download_label" />
|
||||
android:icon="@drawable/ic_play_for_work_land" />
|
||||
</menu>
|
||||
@@ -4,6 +4,7 @@
|
||||
android:id="@+id/homeFragment"
|
||||
android:icon="@drawable/ic_home"
|
||||
android:title="@string/menu_home_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/libraryFragment"
|
||||
android:icon="@drawable/ic_graphic_eq"
|
||||
|
||||
60
app/src/main/res/menu/nav_drawer.xml
Normal file
60
app/src/main/res/menu/nav_drawer.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/searchFragment"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:title="@string/menu_search_button" />
|
||||
|
||||
<item android:title="Index" >
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/homeFragment"
|
||||
android:icon="@drawable/ic_home"
|
||||
android:title="@string/menu_home_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/libraryFragment"
|
||||
android:icon="@drawable/ic_graphic_eq"
|
||||
android:title="@string/menu_library_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/downloadFragment"
|
||||
android:icon="@drawable/ic_play_for_work"
|
||||
android:title="@string/menu_download_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/settingsFragment"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/menu_settings_button" />
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
<item android:title="All" >
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/albumCatalogueFragment"
|
||||
android:icon="@drawable/ic_albums"
|
||||
android:title="Albums" />
|
||||
|
||||
<item
|
||||
android:id="@+id/artistCatalogueFragment"
|
||||
android:icon="@drawable/ic_artists"
|
||||
android:title="Artists" />
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/genreCatalogueFragment"
|
||||
android:icon="@drawable/ic_genres"
|
||||
android:title="Genres" />
|
||||
|
||||
<item
|
||||
android:id="@+id/playlistCatalogueFragment"
|
||||
android:icon="@drawable/ic_playlist"
|
||||
android:title="Playlists"
|
||||
android:defaultValue="ALL"/>
|
||||
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
@@ -220,6 +220,10 @@
|
||||
<action
|
||||
android:id="@+id/action_playlistCatalogueFragment_to_playlistPageFragment"
|
||||
app:destination="@id/playlistPageFragment" />
|
||||
<argument
|
||||
android:name="playlist_all"
|
||||
app:argType="string"
|
||||
android:defaultValue="ALL" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
||||
@@ -224,8 +224,8 @@
|
||||
<string name="playlist_catalogue_title">Catàleg de llistes de reproducció</string>
|
||||
<string name="playlist_catalogue_title_expanded">Exploració de llistes de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_empty">No s\'ha creat cap llista de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancel·la</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancel·la</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_title">Addició a una llista de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">S\'han afegit les cançons a la llista de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">No s\'han pogut afegir les cançons a la llista de reproducció</string>
|
||||
|
||||
@@ -188,8 +188,8 @@
|
||||
<string name="playlist_catalogue_title">Playlisten</string>
|
||||
<string name="playlist_catalogue_title_expanded">Playlisten durchsuchen</string>
|
||||
<string name="playlist_chooser_dialog_empty">Keine Playlisten erstellt</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Abbrechen</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Erstellen</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Abbrechen</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Erstellen</string>
|
||||
<string name="playlist_chooser_dialog_title">Zu einer Playliste hinzufügen</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Lied zu Playlist hinzugefügt</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Titel kann nicht zur Playlist hinzugefügt werden</string>
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
<string name="menu_unpin_button">Eliminar de la pantalla de inicio</string>
|
||||
<string name="menu_sort_year">Año</string>
|
||||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="playback_speed_dialog_negative_button">Cancelar</string>
|
||||
<string name="player_queue_clean_all_button">Limpiar la cola de reproducción</string>
|
||||
<string name="player_queue_save_queue_success">Cola de reproducción guardada</string>
|
||||
<string name="player_lyrics_download_failure">La letra no se puede descargar</string>
|
||||
@@ -222,8 +223,8 @@
|
||||
<string name="playlist_catalogue_title">Catálogo de listas de reproducción</string>
|
||||
<string name="playlist_catalogue_title_expanded">Explorar listas de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_empty">No hay listas de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Crear</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Crear</string>
|
||||
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
|
||||
@@ -327,6 +328,8 @@
|
||||
<string name="settings_delete_download_storage_summary">Al continuar se eliminarán de forma irreversible todos los elementos guardados.</string>
|
||||
<string name="settings_delete_download_storage_title">Eliminar elementos guardados</string>
|
||||
<string name="settings_download_storage_title">Almacenamiento de descargas</string>
|
||||
<string name="settings_ping_timeout_summary">Establece el tiempo de espera de la URL local. Por defecto son 2 segundos (el servidor remoto usará este valor x3 hasta un máximo de 10 segundos).</string>
|
||||
<string name="settings_ping_timeout_dialog">Establece el tiempo de espera base en segundos</string>
|
||||
<string name="settings_max_bitrate_download">Tasa de bits para las descargas</string>
|
||||
<string name="settings_max_bitrate_mobile">Tasa de bits en datos móviles</string>
|
||||
<string name="settings_max_bitrate_wifi">Tasa de bits en Wi-Fi</string>
|
||||
@@ -406,6 +409,7 @@
|
||||
<string name="settings_title_transcoding">Transcodificación</string>
|
||||
<string name="settings_title_transcoding_download">Transcodificación en descargas</string>
|
||||
<string name="settings_title_ui">Interfaz de usuario</string>
|
||||
<string name="settings_title_ui_landscape_items_per_row_dialog">Número de elementos por fila</string>
|
||||
<string name="settings_transcoded_download">Descargas transcodificadas</string>
|
||||
<string name="settings_version_title">Versión</string>
|
||||
<string name="settings_wifi_only_title">Aviso de streaming solo por Wi-Fi</string>
|
||||
@@ -497,6 +501,7 @@
|
||||
<string name="settings_show_mini_shuffle_button">Mostrar el botón «Aleatorio»</string>
|
||||
<string name="settings_auto_download_lyrics">Descargar automáticamente las letras</string>
|
||||
<string name="starred_artist_sync_dialog_summary">Descargar los artistas destacados podría consumir una gran cantidad de datos.</string>
|
||||
<string name="settings_summary_landscape_items_per_row">Aplica a todos los listados de álbumes y artistas. Por defecto es 4</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_summary">Si se habilita, los artistas destacados se descargarán para uso sin conexión.</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="exo_controls_heart_off_description">Eliminar de favoritos</string>
|
||||
@@ -528,4 +533,7 @@
|
||||
<string name="folder_play_no_songs">No se encontraron pistas en la carpeta</string>
|
||||
<string name="search_sort_title">Ordenar las búsquedas recientes cronológicamente</string>
|
||||
<string name="search_sort_summary">Si se habilita, se ordenan las búsquedas en orden cronológico. En caso contrario, se ordenan por nombre.</string>
|
||||
<string name="settings_ping_timeout_title">Tiempo de espera de ping al servidor</string>
|
||||
<string name="playback_speed_dialog_title">Velocidad de reproducción</string>
|
||||
<string name="settings_title_ui_landscape_items_per_row">Elementos por fila en modo horizontal</string>
|
||||
</resources>
|
||||
@@ -254,4 +254,46 @@
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Add by MFO -->
|
||||
<string-array name="aa_tab_titles">
|
||||
<item>Ne pas afficher</item>
|
||||
<item>Accueil</item>
|
||||
<item>Récent</item>
|
||||
<item>Albums</item>
|
||||
<item>Artists</item>
|
||||
<item>Playlists</item>
|
||||
<item>Podcast</item>
|
||||
<item>Radio</item>
|
||||
<item>Dossiers</item>
|
||||
<item>Albums plus joués</item>
|
||||
<!-- <item>Titres joués</item> -->
|
||||
<item>Albums ajouté</item>
|
||||
<!-- <item>Pour vous</item> -->
|
||||
<item>Titres favoris</item>
|
||||
<item>Albums favoris</item>
|
||||
<item>Artistes favoris</item>
|
||||
<item>Aléatoire</item>
|
||||
</string-array>
|
||||
<string-array name="aa_tab_values">
|
||||
<item>-1</item>
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
<item>10</item>
|
||||
<item>11</item>
|
||||
<item>12</item>
|
||||
<item>13</item>
|
||||
<item>14</item>
|
||||
<item>15</item>
|
||||
</string-array>
|
||||
<!-- end Add by MFO -->
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
<resources>
|
||||
<string name="aa_albums">Albums</string>
|
||||
<string name="aa_album_most_played">Albums plus joués</string>
|
||||
<string name="aa_album_recently_added">Albums ajoutés</string>
|
||||
<string name="aa_artists">Artistes</string>
|
||||
<string name="aa_home">Accueil</string>
|
||||
<string name="aa_made_for_you">Pour vous</string>
|
||||
<string name="aa_more">Plus</string>
|
||||
<string name="aa_music_folder">Dossiers</string>
|
||||
<string name="aa_playlists">Playlists</string>
|
||||
<string name="aa_podcast">Podcast</string>
|
||||
<string name="aa_radio">Radio</string>
|
||||
<string name="aa_random">Aléatoire</string>
|
||||
<string name="aa_recent_albums">Récents</string>
|
||||
<string name="aa_song_recently_played">Titres joués</string>
|
||||
<string name="aa_starred_albums">★ Albums</string>
|
||||
<string name="aa_starred_artists">★ Artistes</string>
|
||||
<string name="aa_starred_tracks">★ Titres</string>
|
||||
|
||||
<string name="activity_battery_optimizations_conclusion">Si vous rencontrez un problème, visitez https://dontkillmyapp.com. Des instructions pour désactiver les fonctions de sauvegarde d\'énergie qui pourrait affecter les performance de l\'app y sont disponibles.</string>
|
||||
<string name="activity_battery_optimizations_summary">Veuillez désactiver les optimisations de la batterie pour permettre la lecture des médias lorsque l\'écran est éteint.</string>
|
||||
<string name="activity_battery_optimizations_title">Optimisations de la batterie</string>
|
||||
@@ -230,8 +248,8 @@
|
||||
<string name="playlist_catalogue_title">Catalogue des Playlists</string>
|
||||
<string name="playlist_catalogue_title_expanded">Parcourir les playlists</string>
|
||||
<string name="playlist_chooser_dialog_empty">Pas de playlist</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Annuler</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Créer</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Annuler</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Créer</string>
|
||||
<string name="playlist_chooser_dialog_title">Ajouter à une playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Titre ajouté à la playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Échec d\'ajout du titre à la playlist</string>
|
||||
@@ -310,6 +328,16 @@
|
||||
<string name="settings_always_on_display">Toujours visible</string>
|
||||
<string name="settings_allow_playlist_duplicates">Autoriser l\'ajout de doublons à une playlist</string>
|
||||
<string name="settings_allow_playlist_duplicates_summary">Si activé, les doublons ne seront pas détectés à l\'ajout d\'un titre à une playlist.</string>
|
||||
<string name="settings_androidauto">Android Auto</string>
|
||||
<string name="settings_androidauto_album_view">Vue en grille des albums</string>
|
||||
<string name="settings_androidauto_home_view">Vue en grille du menu accueil</string>
|
||||
<string name="settings_androidauto_playlist_view">Vue en grille des playlists</string>
|
||||
<string name="settings_androidauto_podcast_view">Vue en grille des podcasts</string>
|
||||
<string name="settings_androidauto_radio_view">Vue en grille des radios</string>
|
||||
<string name="settings_androidauto_first_tab">Affichage du premier onglet</string>
|
||||
<string name="settings_androidauto_second_tab">Affichage du deuxième onglet</string>
|
||||
<string name="settings_androidauto_third_tab">Affichage du troisième onglet</string>
|
||||
<string name="settings_androidauto_fourth_tab">Affichage du quatrième onglet</string>
|
||||
<string name="settings_audio_transcode_download_format">Format de transcodage</string>
|
||||
<string name="settings_audio_transcode_download_priority_summary">Si activé, Tempus ne forcera pas le téléchargement de la piste avec les paramètres de transcodage ci-dessous.</string>
|
||||
<string name="settings_audio_transcode_download_priority_title">Prioriser les paramètres du serveurs, utilisés pour le streaming, dans les téléchargements</string>
|
||||
@@ -404,7 +432,7 @@
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si activé, les pistes favorites seront téléchargées pour l\'écoute hors-ligne.</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_title">Synchronisation des pistes favorites pour écoute hors-ligne</string>
|
||||
<string name="settings_theme">Thème</string>
|
||||
<string name="settings_title_data">Données</string>
|
||||
<string name="settings_title_data">Données</string>
|
||||
<string name="settings_title_general">Géneral</string>
|
||||
<string name="settings_title_playlist">Playlist</string>
|
||||
<string name="settings_title_rating">Note</string>
|
||||
@@ -463,7 +491,7 @@
|
||||
<string name="song_list_page_downloaded">Téléchargé</string>
|
||||
<string name="song_list_page_most_played">Titres les plus joués</string>
|
||||
<string name="song_list_page_recently_added">Titres ajoutés récemment</string>
|
||||
<string name="song_list_page_recently_played">Titrés joués récemment</string>
|
||||
<string name="song_list_page_recently_played">Titres joués récemment</string>
|
||||
<string name="song_list_page_starred">Titres favoris</string>
|
||||
<string name="song_list_page_top">Les meilleurs titres de %1$s</string>
|
||||
<string name="song_list_page_year">Année %1$d</string>
|
||||
|
||||
@@ -223,11 +223,13 @@
|
||||
<string name="playlist_catalogue_title">Catalogo playlist</string>
|
||||
<string name="playlist_catalogue_title_expanded">Sfoglia le playlist</string>
|
||||
<string name="playlist_chooser_dialog_empty">Nessuna playlist creata</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Annulla</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Annulla</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_title">Aggiungi a una playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_remove_success">Canzone rimossa dalla playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_remove_failure">Impossibile rimuovere la canzone dalla playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
|
||||
<string name="playlist_chooser_dialog_visibility_public">Pubblico</string>
|
||||
<string name="playlist_chooser_dialog_visibility_private">Privato</string>
|
||||
@@ -448,7 +450,8 @@
|
||||
<string name="song_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="song_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="song_bottom_sheet_rate">Valuta</string>
|
||||
<string name="song_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="song_bottom_sheet_remove">Rimuovi dal dispositivo</string>
|
||||
<string name="song_bottom_sheet_remove_from_playlist">Rimuovi dalla playlist</string>
|
||||
<string name="song_bottom_sheet_share">Condividi</string>
|
||||
<string name="song_list_page_downloaded">Scaricato</string>
|
||||
<string name="song_list_page_most_played">Tracce più riprodotte</string>
|
||||
|
||||
@@ -172,8 +172,8 @@
|
||||
<string name="playlist_catalogue_title">플레이리스트 카탈로그</string>
|
||||
<string name="playlist_catalogue_title_expanded">플레이리스트 찾아보기</string>
|
||||
<string name="playlist_chooser_dialog_empty">플레이리스트가 없습니다.</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">취소</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">생성</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">취소</string>
|
||||
<string name="playlist_chooser_dialog_create_button">생성</string>
|
||||
<string name="playlist_chooser_dialog_title">플레이리스트 추가</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">재생 목록에 음악 추가</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">재생 목록에 음악을 추가하지 못했습니다.</string>
|
||||
|
||||
@@ -263,4 +263,18 @@
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</string-array>
|
||||
<string-array name="landscape_items_per_row">
|
||||
<item>3"</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
</string-array>
|
||||
<string-array name="landscape_items_per_row_values">
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -207,6 +207,8 @@
|
||||
<string name="menu_unpin_button">Usuń z ekranu głównego</string>
|
||||
<string name="menu_sort_year">Rok</string>
|
||||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="playback_speed_dialog_title">Prędkość odtwarzania</string>
|
||||
<string name="playback_speed_dialog_negative_button">Anuluj</string>
|
||||
<string name="player_queue_clean_all_button">Wyczyść kolejkę odtwarzania</string>
|
||||
<string name="player_queue_save_queue_success">Zapisano kolejkę odtwarzania</string>
|
||||
<string name="player_queue_save_to_playlist">Zapisz kolejkę do playlisty</string>
|
||||
@@ -222,11 +224,13 @@
|
||||
<string name="playlist_catalogue_title">Katalog Playlist</string>
|
||||
<string name="playlist_catalogue_title_expanded">Przeglądaj Playlisty</string>
|
||||
<string name="playlist_chooser_dialog_empty">Nie utworzono playlist</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Anuluj</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Utwórz</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Anuluj</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Utwórz</string>
|
||||
<string name="playlist_chooser_dialog_title">Dodaj do playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Dodano piosenki do playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenek do playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_remove_success">Usunięto piosenkę z playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_remove_failure">Nie udało się usunąć piosenki z playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">Pominięto wszystkie piosenki jako duplikaty</string>
|
||||
<string name="playlist_chooser_dialog_visibility_public">Publiczna</string>
|
||||
<string name="playlist_chooser_dialog_visibility_private">Prywatna</string>
|
||||
@@ -288,6 +292,7 @@
|
||||
<string name="server_signup_dialog_hint_password">Hasło</string>
|
||||
<string name="server_signup_dialog_hint_url">URL Serwera</string>
|
||||
<string name="server_signup_dialog_hint_username">Nazwa użytkownika</string>
|
||||
<string name="server_signup_dialog_hint_client_certificate">Certyfikat klienta (opcjonalny)</string>
|
||||
<string name="server_signup_dialog_negative_button">Anuluj</string>
|
||||
<string name="server_signup_dialog_neutral_button">Usuń</string>
|
||||
<string name="server_signup_dialog_positive_button">Zapisz</string>
|
||||
@@ -370,6 +375,10 @@
|
||||
<string name="settings_show_mini_shuffle_button_summary">Jeżeli włączone, pokazuje przycisk losowego odtwarzania, i usuwa przycisk serca w mini odtwarzaczu</string>
|
||||
<string name="settings_radio">Pokaż radio</string>
|
||||
<string name="settings_radio_summary">Jeżeli włączone, widoczna będzie sekcja radia. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
|
||||
<string name="settings_enable_drawer_on_landscape">Włącz szufladę w trybie pionowym [Eksperymentalne]</string>
|
||||
<string name="settings_enable_drawer_on_landscape_summary">Odblokowywuje boczną szufladę trybu poziomego w trybie pionowym. Zmiany przyniosą efekt po restarcie.</string>
|
||||
<string name="settings_hide_bottom_navbar_on_portrait">Ukryj dolny pasek nawigacji w trybie pionowym [Eksperymentalne]</string>
|
||||
<string name="settings_hide_bottom_navbar_on_portrait_summary">Eksperymentalne. Zwiększa ilość miejsca w pionie przez usunięcie dolnego paska nawigacji. Zmiany przyniosą efekt po restarcie.</string>
|
||||
<string name="settings_auto_download_lyrics">Automatyczne pobieranie tesktów</string>
|
||||
<string name="settings_auto_download_lyrics_summary">Automatycznie zapisuj teksty jeżeli, są dostępne aby, mogły być wyświetlane offline.</string>
|
||||
<string name="settings_replay_gain">Tryb wzmocnienia głośności przy ponownym odtwarzaniu</string>
|
||||
@@ -393,6 +402,7 @@
|
||||
<string name="settings_summary_transcoding">Priorytet dawany trybowi transkodowania. Jeżeli ustawiony na \"Odtwarzanie bezpośrednie\" bitrate pliku nie zostanie zmieniony.</string>
|
||||
<string name="settings_summary_transcoding_download">Pobieraj transkdowane media. Jeżeli włączone, endpoint pobierania nie będzie używnany, poza następującymi ustawieniami. \n\n Jeżeli \"Format transkodowania dla pobierania\" jest ustawiony na \"Pobieranie bezpośrednie\" bitrate pliku nie zostanie zmieniony.</string>
|
||||
<string name="settings_summary_transcoding_estimate_content_length">Kiedy plik jest transkodowany w locie, klient nie pokazuje zwykle długości utworu.Jest możliwe odpytanie serwera który wspiera tą funkcjonalność aby oszacował długość odtwarzanego utworu, ale czasy odpowiedzi mogą być dłuższe.</string>
|
||||
<string name="settings_summary_landscape_items_per_row">Ma zastosowanie do wszystkich list albumów i wykonawców. Domyślnie to 4</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_summary">Jeżeli włączone, utwory wykonawców oznaczonych gwiazdką będą pobierane do użycia offline.</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_title">Synchronizuj wykonawców oznacznych gwiazdką do użycia offline</string>
|
||||
<string name="settings_sync_starred_albums_for_offline_use_summary">Jeżeli włączone, albumy oznaczone gwiazdką będą pobieranew do użycia offline.</string>
|
||||
@@ -413,6 +423,8 @@
|
||||
<string name="settings_title_transcoding">Transkodowanie</string>
|
||||
<string name="settings_title_transcoding_download">Transkodowanie Pobrań</string>
|
||||
<string name="settings_title_ui">Interfejs</string>
|
||||
<string name="settings_title_ui_landscape_items_per_row">Elementów na wiersz w poziomie</string>
|
||||
<string name="settings_title_ui_landscape_items_per_row_dialog">Liczba elementów na wiersz</string>
|
||||
<string name="settings_transcoded_download">Transkodowane pobieranie</string>
|
||||
<string name="settings_version_summary" translatable="false">3.1.0</string>
|
||||
<string name="settings_version_title">Wersja</string>
|
||||
@@ -455,7 +467,8 @@
|
||||
<string name="song_bottom_sheet_instant_mix">Natychmiastowy miks</string>
|
||||
<string name="song_bottom_sheet_play_next">Odtwarzaj jako następne</string>
|
||||
<string name="song_bottom_sheet_rate">Oceń</string>
|
||||
<string name="song_bottom_sheet_remove">Usuń</string>
|
||||
<string name="song_bottom_sheet_remove">Usuń z urządzenia</string>
|
||||
<string name="song_bottom_sheet_remove_from_playlist">Usuń z playlisty</string>
|
||||
<string name="song_bottom_sheet_share">Udostępnij</string>
|
||||
<string name="song_list_page_downloaded">Pobrane</string>
|
||||
<string name="song_list_page_most_played">Najczęściej odtwarzane utwory</string>
|
||||
|
||||
@@ -159,8 +159,8 @@
|
||||
<string name="playlist_catalogue_title">Catálogo de Playlists</string>
|
||||
<string name="playlist_catalogue_title_expanded">Navegar pelas Playlists</string>
|
||||
<string name="playlist_chooser_dialog_empty">Nenhuma playlist criada</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Criar</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Criar</string>
|
||||
<string name="playlist_chooser_dialog_title">Adicionar a uma playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Adicionada playlist de reprodução</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
|
||||
|
||||
@@ -233,8 +233,8 @@
|
||||
<string name="playlist_catalogue_title">Catalogul Playlisturi</string>
|
||||
<string name="playlist_catalogue_title_expanded">Răsfoiți Playlisturi</string>
|
||||
<string name="playlist_chooser_dialog_empty">Niciun playlist creat</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Anulati</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Creaţi</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Anulati</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Creaţi</string>
|
||||
<string name="playlist_chooser_dialog_title">Adăugați la un playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Piesa(e) adăugată(e) la playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Eșec la adăugarea piese(lor) la playlist</string>
|
||||
|
||||
@@ -200,8 +200,8 @@
|
||||
<string name="playlist_catalogue_title">Каталог плейлистов</string>
|
||||
<string name="playlist_catalogue_title_expanded">Просмотр плейлистов</string>
|
||||
<string name="playlist_chooser_dialog_empty">Плейлисты не созданы</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Отмена</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Создать</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Отмена</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Создать</string>
|
||||
<string name="playlist_chooser_dialog_title">Добавить в плейлист</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Добавьте песню в плейлист</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Не удалось добавить песню в список воспроизведения</string>
|
||||
|
||||
@@ -203,8 +203,8 @@
|
||||
<string name="playlist_catalogue_title">Çalma Listesi Kataloğu</string>
|
||||
<string name="playlist_catalogue_title_expanded">Çalma listelerine göz at</string>
|
||||
<string name="playlist_chooser_dialog_empty">Henüz çalma listesi oluşturulmadı</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">İptal</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Oluştur</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">İptal</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Oluştur</string>
|
||||
<string name="playlist_chooser_dialog_title">Çalma listesine ekle</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Şarkı çalma listesine eklendi</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Şarkı çalma listesine eklenemedi</string>
|
||||
|
||||
@@ -260,8 +260,8 @@
|
||||
<string name="playlist_catalogue_title">播放列表目录</string>
|
||||
<string name="playlist_catalogue_title_expanded">浏览播放列表</string>
|
||||
<string name="playlist_chooser_dialog_empty">尚未创建播放列表</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">取消</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">新建</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">取消</string>
|
||||
<string name="playlist_chooser_dialog_create_button">新建</string>
|
||||
<string name="playlist_chooser_dialog_title">添加到播放列表</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">未能将歌曲添加到播放列表</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">将歌曲添加到播放列表</string>
|
||||
|
||||
@@ -278,4 +278,46 @@
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Add by MFO -->
|
||||
<string-array name="aa_tab_titles">
|
||||
<item>Do not display</item>
|
||||
<item>Home</item>
|
||||
<item>Recent</item>
|
||||
<item>Albums</item>
|
||||
<item>Artists</item>
|
||||
<item>Playlists</item>
|
||||
<item>Podcast</item>
|
||||
<item>Radio</item>
|
||||
<item>Folder</item>
|
||||
<item>Albums most played</item>
|
||||
<!-- <item>Tracks played</item> -->
|
||||
<item>Albums added</item>
|
||||
<!-- <item>For you</item> -->
|
||||
<item>Star tracks</item>
|
||||
<item>Star albums</item>
|
||||
<item>Star artistes</item>
|
||||
<item>Random</item>
|
||||
</string-array>
|
||||
<string-array name="aa_tab_values">
|
||||
<item>-1</item>
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
<item>10</item>
|
||||
<item>11</item>
|
||||
<item>12</item>
|
||||
<item>13</item>
|
||||
<item>14</item>
|
||||
<item>15</item>
|
||||
</string-array>
|
||||
<!-- end Add by MFO -->
|
||||
|
||||
</resources>
|
||||
@@ -1,4 +1,22 @@
|
||||
<resources>
|
||||
<string name="aa_albums">Albums</string>
|
||||
<string name="aa_album_most_played">Albums most played</string>
|
||||
<string name="aa_album_recently_added">Albums added</string>
|
||||
<string name="aa_artists">Artists</string>
|
||||
<string name="aa_home">Home</string>
|
||||
<string name="aa_made_for_you">For you</string>
|
||||
<string name="aa_more">More</string>
|
||||
<string name="aa_music_folder">Folder</string>
|
||||
<string name="aa_playlists">Playlists</string>
|
||||
<string name="aa_podcast">Podcast</string>
|
||||
<string name="aa_radio">Radio</string>
|
||||
<string name="aa_random">Random</string>
|
||||
<string name="aa_recent_albums">Recent</string>
|
||||
<string name="aa_song_recently_played">Song played</string>
|
||||
<string name="aa_starred_albums">★ Albums</string>
|
||||
<string name="aa_starred_artists">★ Artists</string>
|
||||
<string name="aa_starred_tracks">★ Tracks</string>
|
||||
|
||||
<string name="activity_battery_optimizations_conclusion">If in trouble visit https://dontkillmyapp.com. It provides detailed instructions on how to disable any power-saving features that may affect app\'s performance.</string>
|
||||
<string name="activity_battery_optimizations_summary">Please disable battery optimizations for media playback while the screen is off.</string>
|
||||
<string name="activity_battery_optimizations_title">Battery Optimizations</string>
|
||||
@@ -234,14 +252,18 @@
|
||||
<string name="playlist_catalogue_title">Playlist Catalogue</string>
|
||||
<string name="playlist_catalogue_title_expanded">Browse Playlists</string>
|
||||
<string name="playlist_chooser_dialog_empty">No playlists created</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancel</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Create</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancel</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Create</string>
|
||||
<string name="playlist_chooser_dialog_title">Add to a playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_remove_success">Removed song from playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_remove_failure">Failed to remove song from playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
||||
<string name="playlist_chooser_dialog_visibility_public">Public</string>
|
||||
<string name="playlist_chooser_dialog_visibility_private">Private</string>
|
||||
<string name="playlist_chooser_dialog_visibility_switch_label">Mark the playlist as public</string>
|
||||
<string name="playlist_chooser_dialog_visibility_summary">The server updates the visibility on each request. By default it is set to private.</string>
|
||||
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
||||
<string name="playlist_duration">Duration • %1$s</string>
|
||||
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
||||
@@ -300,6 +322,7 @@
|
||||
<string name="server_signup_dialog_hint_password">Password</string>
|
||||
<string name="server_signup_dialog_hint_url">Server URL</string>
|
||||
<string name="server_signup_dialog_hint_username">Username</string>
|
||||
<string name="server_signup_dialog_hint_client_certificate">Client certificate (optional)</string>
|
||||
<string name="server_signup_dialog_negative_button">Cancel</string>
|
||||
<string name="server_signup_dialog_neutral_button">Delete</string>
|
||||
<string name="server_signup_dialog_positive_button">Save</string>
|
||||
@@ -369,6 +392,16 @@
|
||||
<string name="settings_podcast">Show podcast</string>
|
||||
<string name="settings_podcast_summary">If enabled, show the podcast section. Restart the app for it to take full effect.</string>
|
||||
<string name="settings_playlist_sort">Playlist sorting</string>
|
||||
<string name="settings_androidauto">Android Auto</string>
|
||||
<string name="settings_androidauto_album_view">Grid view for albums</string>
|
||||
<string name="settings_androidauto_home_view">Grid view for home</string>
|
||||
<string name="settings_androidauto_playlist_view">Grid view for playlists</string>
|
||||
<string name="settings_androidauto_podcast_view">Grid view for podcast</string>
|
||||
<string name="settings_androidauto_radio_view">Grid view for radio</string>
|
||||
<string name="settings_androidauto_first_tab">First tab display</string>
|
||||
<string name="settings_androidauto_second_tab">Second tab display</string>
|
||||
<string name="settings_androidauto_third_tab">Third tab display</string>
|
||||
<string name="settings_androidauto_fourth_tab">Fourth tab display</string>
|
||||
<string name="settings_audio_quality">Show audio quality</string>
|
||||
<string name="settings_audio_quality_summary">The bitrate and audio format will be shown for each audio track.</string>
|
||||
<string name="settings_song_rating">Show song star rating</string>
|
||||
@@ -382,6 +415,10 @@
|
||||
<string name="settings_show_mini_shuffle_button_summary">If enabled, show the shuffle button, remove the heart in the mini player</string>
|
||||
<string name="settings_radio">Show radio</string>
|
||||
<string name="settings_radio_summary">If enabled, show the radio section. Restart the app for it to take full effect.</string>
|
||||
<string name="settings_enable_drawer_on_landscape">Enable drawer on portrait [Experimental]</string>
|
||||
<string name="settings_enable_drawer_on_landscape_summary">Unlocks the lateral landscape menu drawer on portrait. The changes will take effect on restart.</string>
|
||||
<string name="settings_hide_bottom_navbar_on_portrait">Hide bottom navbar on portrait [Experimental]</string>
|
||||
<string name="settings_hide_bottom_navbar_on_portrait_summary">Experimental.Increases vertical space by removing the bottom navbar. The changes will take effect on restart.</string>
|
||||
<string name="settings_auto_download_lyrics">Auto download lyrics</string>
|
||||
<string name="settings_auto_download_lyrics_summary">Automatically save lyrics when they are available so they can be shown while offline.</string>
|
||||
<string name="settings_replay_gain">Set replay gain mode</string>
|
||||
@@ -470,7 +507,8 @@
|
||||
<string name="song_bottom_sheet_instant_mix">Instant mix</string>
|
||||
<string name="song_bottom_sheet_play_next">Play next</string>
|
||||
<string name="song_bottom_sheet_rate">Rate</string>
|
||||
<string name="song_bottom_sheet_remove">Remove</string>
|
||||
<string name="song_bottom_sheet_remove">Remove from device</string>
|
||||
<string name="song_bottom_sheet_remove_from_playlist">Remove from playlist</string>
|
||||
<string name="song_bottom_sheet_share">Share</string>
|
||||
<string name="song_list_page_downloaded">Downloaded</string>
|
||||
<string name="song_list_page_most_played">Most played tracks</string>
|
||||
@@ -517,6 +555,7 @@
|
||||
<string name="track_info_summary_transcoding_codec">The application will request the server to transcode the file. The requested codec by the user is %1$s, while the bitrate will be the same as the source file. The potential transcoding of the file into the chosen format is dependent on the server, as it may or may not support the operation.</string>
|
||||
<string name="track_info_title">Title</string>
|
||||
<string name="track_info_track_number">Track number</string>
|
||||
<string name="track_info_station">Station</string>
|
||||
<string name="track_info_transcoded_content_type">Transcoded content type</string>
|
||||
<string name="track_info_transcoded_suffix">Transcoded suffix</string>
|
||||
<string name="track_info_year">Year</string>
|
||||
|
||||
@@ -54,6 +54,16 @@
|
||||
android:defaultValue="false"
|
||||
android:key="always_on_display" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_enable_drawer_on_landscape"
|
||||
android:key="enable_drawer_on_portrait"
|
||||
android:summary="@string/settings_enable_drawer_on_landscape_summary"/>
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_hide_bottom_navbar_on_portrait"
|
||||
android:key="hide_bottom_navbar_on_portrait"
|
||||
android:summary="@string/settings_hide_bottom_navbar_on_portrait_summary"/>
|
||||
|
||||
<SwitchPreference
|
||||
android:layout_height="match_parent"
|
||||
android:defaultValue="true"
|
||||
@@ -136,7 +146,7 @@
|
||||
android:defaultValue="false"
|
||||
android:summary="@string/search_sort_summary"
|
||||
android:key="sort_search_chronologically" />
|
||||
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="4"
|
||||
app:dialogTitle="@string/settings_title_ui_landscape_items_per_row_dialog"
|
||||
@@ -145,7 +155,7 @@
|
||||
app:key="landscape_items_per_row"
|
||||
android:summary="@string/settings_summary_landscape_items_per_row"
|
||||
app:title="@string/settings_title_ui_landscape_items_per_row" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_title_playlist">
|
||||
@@ -444,6 +454,76 @@
|
||||
android:key="github_update_check" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<!-- Android Auto configuration -->
|
||||
<PreferenceCategory app:title="@string/settings_androidauto">
|
||||
<Preference
|
||||
app:selectable="false"
|
||||
app:summary="@string/home_rearrangement_dialog_subtitle" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_androidauto_home_view"
|
||||
android:defaultValue="false"
|
||||
android:key="androidauto_home_view" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_androidauto_album_view"
|
||||
android:defaultValue="true"
|
||||
android:key="androidauto_album_view" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_androidauto_playlist_view"
|
||||
android:defaultValue="false"
|
||||
android:key="androidauto_playlist_view" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_androidauto_radio_view"
|
||||
android:defaultValue="false"
|
||||
android:key="androidauto_radio_view" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_androidauto_podcast_view"
|
||||
android:defaultValue="false"
|
||||
android:key="androidauto_podcast_view" />
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="0"
|
||||
app:dialogTitle="@string/settings_androidauto_first_tab"
|
||||
app:entries="@array/aa_tab_titles"
|
||||
app:entryValues="@array/aa_tab_values"
|
||||
app:key="androidauto_first_tab"
|
||||
app:title="@string/settings_androidauto_first_tab"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="1"
|
||||
app:dialogTitle="@string/settings_androidauto_second_tab"
|
||||
app:entries="@array/aa_tab_titles"
|
||||
app:entryValues="@array/aa_tab_values"
|
||||
app:key="androidauto_second_tab"
|
||||
app:title="@string/settings_androidauto_second_tab"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="2"
|
||||
app:dialogTitle="@string/settings_androidauto_third_tab"
|
||||
app:entries="@array/aa_tab_titles"
|
||||
app:entryValues="@array/aa_tab_values"
|
||||
app:key="androidauto_third_tab"
|
||||
app:title="@string/settings_androidauto_third_tab"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="3"
|
||||
app:dialogTitle="@string/settings_androidauto_fourth_tab"
|
||||
app:entries="@array/aa_tab_titles"
|
||||
app:entryValues="@array/aa_tab_values"
|
||||
app:key="androidauto_fourth_tab"
|
||||
app:title="@string/settings_androidauto_fourth_tab"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceCategory>
|
||||
<!-- end Add by MFO -->
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_about_title">
|
||||
<Preference
|
||||
app:selectable="false"
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import android.os.Bundle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaConstants
|
||||
import com.cappielloantonio.tempo.BuildConfig
|
||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||
import com.cappielloantonio.tempo.util.Preferences.getServerId
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
|
||||
object MediaBrowserTree {
|
||||
|
||||
private lateinit var appContext: Context
|
||||
private lateinit var automotiveRepository: AutomotiveRepository
|
||||
|
||||
private var treeNodes: MutableMap<String, MediaItemNode> = mutableMapOf()
|
||||
|
||||
private var isInitialized = false
|
||||
|
||||
/* data class FunctionItem(
|
||||
val id: String,
|
||||
var isDisplayed: Boolean
|
||||
)
|
||||
*/
|
||||
// Root
|
||||
private const val ROOT_ID = "[rootID]"
|
||||
|
||||
// First level
|
||||
// Available functions
|
||||
private const val HOME_ID = "[homeID]"
|
||||
private const val LIBRARY_ID = "[libraryID]"
|
||||
private const val OTHER_ID = "[otherID]"
|
||||
|
||||
// Second level HOME_ID
|
||||
private const val MOST_PLAYED_ID = "[mostPlayedID]"
|
||||
private const val LAST_PLAYED_ID = "[lastPlayedID]"
|
||||
private const val ALBUMS_ID = "[albumsID]"
|
||||
private const val ARTISTS_ID = "[artistsID]"
|
||||
private const val MOST_PLAYED_ID = "[mostPlayedID]"
|
||||
private const val PLAYLIST_ID = "[playlistID]"
|
||||
private const val PODCAST_ID = "[podcastID]"
|
||||
private const val RADIO_ID = "[radioID]"
|
||||
private const val RECENTLY_ADDED_ID = "[recentlyAddedID]"
|
||||
private const val RECENT_SONGS_ID = "[recentSongsID]"
|
||||
private const val MADE_FOR_YOU_ID = "[madeForYouID]"
|
||||
@@ -39,20 +49,17 @@ object MediaBrowserTree {
|
||||
private const val STARRED_ALBUMS_ID = "[starredAlbumsID]"
|
||||
private const val STARRED_ARTISTS_ID = "[starredArtistsID]"
|
||||
private const val RANDOM_ID = "[randomID]"
|
||||
|
||||
// Second level LIBRARY_ID
|
||||
private const val FOLDER_ID = "[folderID]"
|
||||
|
||||
// System functions
|
||||
private const val INDEX_ID = "[indexID]"
|
||||
private const val DIRECTORY_ID = "[directoryID]"
|
||||
private const val PLAYLIST_ID = "[playlistID]"
|
||||
|
||||
// Second level OTHER_ID
|
||||
private const val PODCAST_ID = "[podcastID]"
|
||||
private const val RADIO_ID = "[radioID]"
|
||||
|
||||
private const val ALBUM_ID = "[albumID]"
|
||||
private const val ARTIST_ID = "[artistID]"
|
||||
|
||||
private fun iconUri(resId: Int): Uri =
|
||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/$resId")
|
||||
|
||||
private class MediaItemNode(val item: MediaItem) {
|
||||
private val children: MutableList<MediaItem> = ArrayList()
|
||||
|
||||
@@ -71,6 +78,7 @@ object MediaBrowserTree {
|
||||
}
|
||||
|
||||
private fun buildMediaItem(
|
||||
gridView: Boolean,
|
||||
title: String,
|
||||
mediaId: String,
|
||||
isPlayable: Boolean,
|
||||
@@ -83,18 +91,43 @@ object MediaBrowserTree {
|
||||
sourceUri: Uri? = null,
|
||||
imageUri: Uri? = null
|
||||
): MediaItem {
|
||||
val metadata =
|
||||
MediaMetadata.Builder()
|
||||
.setAlbumTitle(album)
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setGenre(genre)
|
||||
.setIsBrowsable(isBrowsable)
|
||||
.setIsPlayable(isPlayable)
|
||||
.setArtworkUri(imageUri)
|
||||
.setMediaType(mediaType)
|
||||
.build()
|
||||
var extras = Bundle()
|
||||
if( gridView ) {
|
||||
extras = Bundle().apply {
|
||||
putInt(
|
||||
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
||||
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
|
||||
)
|
||||
putInt(
|
||||
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
|
||||
)
|
||||
}
|
||||
}
|
||||
else{
|
||||
extras = Bundle().apply {
|
||||
putInt(
|
||||
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
||||
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
||||
)
|
||||
putInt(
|
||||
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val metadata = MediaMetadata.Builder()
|
||||
.setAlbumTitle(album)
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setGenre(genre)
|
||||
.setIsBrowsable(isBrowsable)
|
||||
.setIsPlayable(isPlayable)
|
||||
.setArtworkUri(imageUri)
|
||||
.setMediaType(mediaType)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setSubtitleConfigurations(subtitleConfigurations)
|
||||
@@ -102,19 +135,57 @@ object MediaBrowserTree {
|
||||
.setUri(sourceUri)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun initialize(automotiveRepository: AutomotiveRepository) {
|
||||
fun initialize(
|
||||
context: Context,
|
||||
automotiveRepository: AutomotiveRepository) {
|
||||
this.automotiveRepository = automotiveRepository
|
||||
|
||||
appContext = context.applicationContext
|
||||
if (isInitialized) return
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
fun buildTree() {
|
||||
val albumView: Boolean = Preferences.isAndroidAutoAlbumViewEnabled()
|
||||
val homeView: Boolean = Preferences.isAndroidAutoHomeViewEnabled()
|
||||
val playlistView: Boolean = Preferences.isAndroidAutoPlaylistViewEnabled()
|
||||
val podcastView: Boolean = Preferences.isAndroidAutoPodcastViewEnabled()
|
||||
val radioView: Boolean = Preferences.isAndroidAutoRadioViewEnabled()
|
||||
|
||||
val tabIndex = listOf(
|
||||
Preferences.getAndroidAutoFirstTab(),
|
||||
Preferences.getAndroidAutoSecondTab(),
|
||||
Preferences.getAndroidAutoThirdTab(),
|
||||
Preferences.getAndroidAutoFourthTab()
|
||||
)
|
||||
// clear before rebuild
|
||||
treeNodes.clear()
|
||||
|
||||
// This list must be exactly the same as the one in aa_tab_titles
|
||||
val allFunctions = listOf(
|
||||
HOME_ID,
|
||||
LAST_PLAYED_ID,
|
||||
ALBUMS_ID,
|
||||
ARTISTS_ID,
|
||||
PLAYLIST_ID,
|
||||
PODCAST_ID,
|
||||
RADIO_ID,
|
||||
FOLDER_ID,
|
||||
MOST_PLAYED_ID,
|
||||
// RECENT_SONGS_ID, // => doesn't work !
|
||||
RECENTLY_ADDED_ID,
|
||||
// MADE_FOR_YOU_ID, // => doesn't work !
|
||||
STARRED_TRACKS_ID,
|
||||
STARRED_ALBUMS_ID,
|
||||
STARRED_ARTISTS_ID,
|
||||
RANDOM_ID
|
||||
)
|
||||
|
||||
// Root level
|
||||
|
||||
treeNodes[ROOT_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = "Root Folder",
|
||||
mediaId = ROOT_ID,
|
||||
isPlayable = false,
|
||||
@@ -123,192 +194,98 @@ object MediaBrowserTree {
|
||||
)
|
||||
)
|
||||
|
||||
// First level
|
||||
|
||||
treeNodes[HOME_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Home",
|
||||
mediaId = HOME_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[LIBRARY_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Library",
|
||||
mediaId = LIBRARY_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[OTHER_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Other",
|
||||
mediaId = OTHER_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[ROOT_ID]!!.addChild(HOME_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(LIBRARY_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(OTHER_ID)
|
||||
|
||||
// Second level HOME_ID
|
||||
|
||||
treeNodes[MOST_PLAYED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Most played",
|
||||
mediaId = MOST_PLAYED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
// All available functions
|
||||
// if HOME is in first place or no item is selected
|
||||
if (tabIndex.firstOrNull() == 0 || tabIndex.all { it == -1 }){
|
||||
treeNodes[HOME_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = homeView,
|
||||
title = appContext.getString(R.string.aa_home),
|
||||
mediaId = HOME_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_home),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
}
|
||||
else { // More instead of Home
|
||||
treeNodes[HOME_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = homeView,
|
||||
title = appContext.getString(R.string.aa_more),
|
||||
mediaId = HOME_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_other),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
treeNodes[LAST_PLAYED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Last played",
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_recent_albums),
|
||||
mediaId = LAST_PLAYED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_recent),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RECENTLY_ADDED_ID] =
|
||||
|
||||
treeNodes[ALBUMS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Recently added",
|
||||
mediaId = RECENTLY_ADDED_ID,
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_albums),
|
||||
mediaId = ALBUMS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_albums),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RECENT_SONGS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Recent songs",
|
||||
mediaId = RECENT_SONGS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[MADE_FOR_YOU_ID] =
|
||||
|
||||
treeNodes[ARTISTS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Made for you",
|
||||
mediaId = MADE_FOR_YOU_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_TRACKS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Starred tracks",
|
||||
mediaId = STARRED_TRACKS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_ALBUMS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Starred albums",
|
||||
mediaId = STARRED_ALBUMS_ID,
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_artists),
|
||||
mediaId = ARTISTS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_artists),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_ARTISTS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Starred artists",
|
||||
mediaId = STARRED_ARTISTS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RANDOM_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Random",
|
||||
mediaId = RANDOM_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[HOME_ID]!!.addChild(MOST_PLAYED_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(LAST_PLAYED_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(RECENTLY_ADDED_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(RECENT_SONGS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(MADE_FOR_YOU_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(STARRED_TRACKS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(STARRED_ALBUMS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(STARRED_ARTISTS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(RANDOM_ID)
|
||||
|
||||
// Second level LIBRARY_ID
|
||||
|
||||
treeNodes[FOLDER_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Folders",
|
||||
mediaId = FOLDER_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
treeNodes[PLAYLIST_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Playlists",
|
||||
gridView = playlistView,
|
||||
title = appContext.getString(R.string.aa_playlists),
|
||||
mediaId = PLAYLIST_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_playlist),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[LIBRARY_ID]!!.addChild(FOLDER_ID)
|
||||
treeNodes[LIBRARY_ID]!!.addChild(PLAYLIST_ID)
|
||||
|
||||
// Second level OTHER_ID
|
||||
|
||||
treeNodes[PODCAST_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Podcasts",
|
||||
gridView = podcastView,
|
||||
title = appContext.getString(R.string.aa_podcast),
|
||||
mediaId = PODCAST_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_podcasts),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS
|
||||
)
|
||||
)
|
||||
@@ -316,18 +293,161 @@ object MediaBrowserTree {
|
||||
treeNodes[RADIO_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Radio stations",
|
||||
gridView = radioView,
|
||||
title = appContext.getString(R.string.aa_radio),
|
||||
mediaId = RADIO_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_radio),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_RADIO_STATIONS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[OTHER_ID]!!.addChild(PODCAST_ID)
|
||||
treeNodes[OTHER_ID]!!.addChild(RADIO_ID)
|
||||
}
|
||||
treeNodes[MOST_PLAYED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_album_most_played),
|
||||
mediaId = MOST_PLAYED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_mostplayed),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
treeNodes[RECENTLY_ADDED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_album_recently_added),
|
||||
mediaId = RECENTLY_ADDED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_added_album),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RECENT_SONGS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_song_recently_played),
|
||||
mediaId = RECENT_SONGS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_recent_title),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[MADE_FOR_YOU_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_made_for_you),
|
||||
mediaId = MADE_FOR_YOU_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_for_you),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_TRACKS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_starred_tracks),
|
||||
mediaId = STARRED_TRACKS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_star_title),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_ALBUMS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_starred_albums),
|
||||
mediaId = STARRED_ALBUMS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_star_album),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_ARTISTS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_starred_artists),
|
||||
mediaId = STARRED_ARTISTS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_artists),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[FOLDER_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = false,
|
||||
title = appContext.getString(R.string.aa_music_folder),
|
||||
mediaId = FOLDER_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_folders),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RANDOM_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
gridView = albumView,
|
||||
title = appContext.getString(R.string.aa_random),
|
||||
mediaId = RANDOM_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
imageUri = iconUri(R.drawable.ic_aa_random),
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
val root = treeNodes[ROOT_ID]!!
|
||||
val selectedIds = mutableSetOf<String>()
|
||||
|
||||
// First level
|
||||
// add functions selected by user for the 4 tabs
|
||||
tabIndex
|
||||
.filter { it != -1 }
|
||||
.forEach { index ->
|
||||
allFunctions.getOrNull(index)?.let { function ->
|
||||
if (selectedIds.add(function)) {
|
||||
root.addChild(function)
|
||||
}
|
||||
}
|
||||
}
|
||||
// if no function is selected, add at least HOME_ID
|
||||
if (selectedIds.isEmpty()) {
|
||||
root.addChild(HOME_ID)
|
||||
selectedIds.add(HOME_ID)
|
||||
}
|
||||
|
||||
// Second level for HOME_ID even there is no HOME_ID displayed
|
||||
// add all functions not previously added
|
||||
allFunctions
|
||||
.filter { it !in selectedIds }
|
||||
.forEach { function ->
|
||||
treeNodes[HOME_ID]?.addChild(function)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRootItem(): MediaItem {
|
||||
return treeNodes[ROOT_ID]!!.item
|
||||
}
|
||||
@@ -337,125 +457,83 @@ object MediaBrowserTree {
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
return when (id) {
|
||||
ROOT_ID -> treeNodes[ROOT_ID]?.getChildren()!!
|
||||
HOME_ID -> treeNodes[HOME_ID]?.getChildren()!!
|
||||
LIBRARY_ID -> treeNodes[LIBRARY_ID]?.getChildren()!!
|
||||
OTHER_ID -> treeNodes[OTHER_ID]?.getChildren()!!
|
||||
|
||||
MOST_PLAYED_ID -> automotiveRepository.getAlbums(id, "frequent", 100)
|
||||
LAST_PLAYED_ID -> automotiveRepository.getAlbums(id, "recent", 100)
|
||||
RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100)
|
||||
RECENT_SONGS_ID -> automotiveRepository.getRecentlyPlayedSongs(getServerId(),100)
|
||||
HOME_ID -> treeNodes[HOME_ID]?.getChildren()!!
|
||||
LAST_PLAYED_ID -> automotiveRepository.getAlbums(id, "recent", 15)
|
||||
ALBUMS_ID -> automotiveRepository.getAlbums(id, "alphabeticalByName", 500)
|
||||
ARTISTS_ID -> automotiveRepository.getAlbums(id, "alphabeticalByArtist", 500)
|
||||
PLAYLIST_ID -> automotiveRepository.getPlaylists(id)
|
||||
PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(100)
|
||||
RADIO_ID -> automotiveRepository.internetRadioStations
|
||||
FOLDER_ID -> automotiveRepository.getMusicFolders(id)
|
||||
MOST_PLAYED_ID -> automotiveRepository.getAlbums(id, "frequent", 15)
|
||||
RECENT_SONGS_ID -> automotiveRepository.getRecentlyPlayedSongs(getServerId(),30)
|
||||
RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 15)
|
||||
MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id)
|
||||
STARRED_TRACKS_ID -> automotiveRepository.starredSongs
|
||||
STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id)
|
||||
STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id)
|
||||
RANDOM_ID -> automotiveRepository.getRandomSongs(100)
|
||||
FOLDER_ID -> automotiveRepository.getMusicFolders(id)
|
||||
PLAYLIST_ID -> automotiveRepository.getPlaylists(id)
|
||||
PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(100)
|
||||
RADIO_ID -> automotiveRepository.internetRadioStations
|
||||
|
||||
else -> {
|
||||
if (id.startsWith(MOST_PLAYED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
MOST_PLAYED_ID
|
||||
)
|
||||
)
|
||||
if (id.startsWith(LAST_PLAYED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(LAST_PLAYED_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(LAST_PLAYED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
LAST_PLAYED_ID
|
||||
)
|
||||
)
|
||||
if (id.startsWith(ALBUMS_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(ALBUMS_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(ARTISTS_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(ARTISTS_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(HOME_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(HOME_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(MOST_PLAYED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(MOST_PLAYED_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(RECENTLY_ADDED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
RECENTLY_ADDED_ID
|
||||
)
|
||||
)
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(RECENTLY_ADDED_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(MADE_FOR_YOU_ID)) {
|
||||
return automotiveRepository.getMadeForYou(
|
||||
id.removePrefix(
|
||||
MADE_FOR_YOU_ID
|
||||
),
|
||||
20
|
||||
)
|
||||
return automotiveRepository.getMadeForYou(id.removePrefix(MADE_FOR_YOU_ID),20)
|
||||
}
|
||||
|
||||
if (id.startsWith(STARRED_ALBUMS_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
STARRED_ALBUMS_ID
|
||||
)
|
||||
)
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(STARRED_ALBUMS_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(STARRED_ARTISTS_ID)) {
|
||||
return automotiveRepository.getArtistAlbum(
|
||||
STARRED_ALBUMS_ID,
|
||||
id.removePrefix(
|
||||
STARRED_ARTISTS_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(FOLDER_ID)) {
|
||||
return automotiveRepository.getIndexes(
|
||||
INDEX_ID,
|
||||
id.removePrefix(
|
||||
FOLDER_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(INDEX_ID)) {
|
||||
return automotiveRepository.getDirectories(
|
||||
DIRECTORY_ID,
|
||||
id.removePrefix(
|
||||
INDEX_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(DIRECTORY_ID)) {
|
||||
return automotiveRepository.getDirectories(
|
||||
DIRECTORY_ID,
|
||||
id.removePrefix(
|
||||
DIRECTORY_ID
|
||||
)
|
||||
)
|
||||
return automotiveRepository.getArtistAlbum(STARRED_ALBUMS_ID,id.removePrefix(STARRED_ARTISTS_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(PLAYLIST_ID)) {
|
||||
return automotiveRepository.getPlaylistSongs(
|
||||
id.removePrefix(
|
||||
PLAYLIST_ID
|
||||
)
|
||||
)
|
||||
return automotiveRepository.getPlaylistSongs(id.removePrefix(PLAYLIST_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(ALBUM_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
ALBUM_ID
|
||||
)
|
||||
)
|
||||
return automotiveRepository.getAlbumTracks(id.removePrefix(ALBUM_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(ARTIST_ID)) {
|
||||
return automotiveRepository.getArtistAlbum(
|
||||
ALBUM_ID,
|
||||
id.removePrefix(
|
||||
ARTIST_ID
|
||||
)
|
||||
)
|
||||
return automotiveRepository.getArtistAlbum(ALBUM_ID,id.removePrefix(ARTIST_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(FOLDER_ID)) {
|
||||
return automotiveRepository.getIndexes(INDEX_ID,id.removePrefix(FOLDER_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(INDEX_ID)) {
|
||||
return automotiveRepository.getDirectories(DIRECTORY_ID,id.removePrefix(INDEX_ID))
|
||||
}
|
||||
|
||||
if (id.startsWith(DIRECTORY_ID)) {
|
||||
return automotiveRepository.getDirectories(DIRECTORY_ID,id.removePrefix(DIRECTORY_ID))
|
||||
}
|
||||
|
||||
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
|
||||
@@ -490,6 +568,7 @@ object MediaBrowserTree {
|
||||
fun search(query: String): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
return automotiveRepository.search(
|
||||
query,
|
||||
// ALBUM_ID,
|
||||
ALBUM_ID,
|
||||
ARTIST_ID
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MO
|
||||
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF
|
||||
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.cappielloantonio.tempo.util.Constants
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
@@ -40,13 +41,14 @@ import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
open class MediaLibrarySessionCallback(
|
||||
context: Context,
|
||||
automotiveRepository: AutomotiveRepository
|
||||
private val context: Context,
|
||||
private val automotiveRepository: AutomotiveRepository
|
||||
) :
|
||||
MediaLibraryService.MediaLibrarySession.Callback {
|
||||
|
||||
init {
|
||||
MediaBrowserTree.initialize(automotiveRepository)
|
||||
// modified by MFO
|
||||
MediaBrowserTree.initialize(context, automotiveRepository)
|
||||
}
|
||||
|
||||
private val customCommandToggleShuffleModeOn = CommandButton.Builder()
|
||||
@@ -347,6 +349,8 @@ open class MediaLibrarySessionCallback(
|
||||
browser: MediaSession.ControllerInfo,
|
||||
params: MediaLibraryService.LibraryParams?
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
// added by MFO
|
||||
MediaBrowserTree.buildTree()
|
||||
return Futures.immediateFuture(LibraryResult.ofItem(MediaBrowserTree.getRootItem(), params))
|
||||
}
|
||||
|
||||
@@ -366,11 +370,31 @@ open class MediaLibrarySessionCallback(
|
||||
controller: MediaSession.ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
return super.onAddMediaItems(
|
||||
mediaSession,
|
||||
controller,
|
||||
MediaBrowserTree.getItems(mediaItems)
|
||||
)
|
||||
val firstItem = mediaItems.firstOrNull()
|
||||
val isRadio = firstItem?.mediaId?.startsWith("ir-") == true
|
||||
|
||||
if (isRadio) {
|
||||
return Futures.transformAsync(
|
||||
automotiveRepository.internetRadioStations,
|
||||
{ result ->
|
||||
val stations = result?.value
|
||||
val selected = stations?.find { item -> item.mediaId == firstItem?.mediaId }
|
||||
if (selected != null) {
|
||||
val updatedSelected = selected.buildUpon()
|
||||
.setMimeType(selected.localConfiguration?.mimeType)
|
||||
.build()
|
||||
|
||||
Futures.immediateFuture(listOf(updatedSelected))
|
||||
} else {
|
||||
Futures.immediateFuture(emptyList())
|
||||
}
|
||||
},
|
||||
androidx.core.content.ContextCompat.getMainExecutor(context)
|
||||
)
|
||||
}
|
||||
|
||||
val resolvedItems = MediaBrowserTree.getItems(mediaItems)
|
||||
return super.onAddMediaItems(mediaSession, controller, resolvedItems)
|
||||
}
|
||||
|
||||
override fun onSearch(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user