5 Commits

Author SHA1 Message Date
eddyizm
21ed78d959 chore: bumping version, fastlane and changelog 2026-02-08 16:14:22 -08:00
Tom
5ad99b9f27 feat: increase items per row on landscape view (#411)
* feat: increase items per row on landscape view

This covers the catalogues: artist, album and genre; also the list of albums on artist view.
This was implemented by adierebel/tempo fork, I only cherry-picked some commits.

Co-authored-by: adierebel <adie.rebel@gmail.com>

* feat: add landscape layout to song listing views

This includes the playlist page and the album page.

* fix: bad scaling on small screens

This rollbacks to the original code by adierebel/tempo fork

* fix: remove hardcoded height blocking scroll

This was addressed in 989ca35, forgot to fix it here as well

* fix: wrap content height rather than inheriting it from parent

* feat: add ui of choice selector in setting for items per row

* feat: link getter to landscapes items per row setting an implement it

* fix: wrong default value

Co-authored-by: eddyizm <wtfisup@hotmail.com>

* feat: add default value on setting string

To introduce the new feature of landscape layouts.

Co-authored-by: eddyizm <wtfisup@hotmail.com>

---------

Co-authored-by: adierebel <adie.rebel@gmail.com>
Co-authored-by: eddyizm <wtfisup@hotmail.com>
Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-02-08 15:20:53 -08:00
T R
3de5390140 fix: album art now displays on android auto (#414)
Co-authored-by: Thomas R <tdr@thomasr.co>
Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-02-08 10:34:44 -08:00
eddyizm
d215581e19 fix: keep observer until data is received on continuous play (#421) 2026-02-08 10:18:36 -08:00
tiltshiftfocus
54612c6b74 patch: Addressing some UI/UX quirks (#413)
* beautify lyrics display

* use dialog to select playback speed

to prevent accidental clicks
2026-02-08 10:18:01 -08:00
31 changed files with 1038 additions and 65 deletions

View File

@@ -2,6 +2,18 @@
## Pending release ## Pending release
## What's Changed
* 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
* feat: improve landscape view and increase items per row on landscape view by @tvillega in https://github.com/eddyizm/tempus/pull/411
## New Contributors
* @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
## What's Changed ## What's Changed
## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02) ## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02)
* fix: missing Replay Gain metadata from .m4a files by @pgrit in https://github.com/eddyizm/tempus/pull/396 * fix: missing Replay Gain metadata from .m4a files by @pgrit in https://github.com/eddyizm/tempus/pull/396

View File

@@ -10,8 +10,8 @@ android {
minSdkVersion 24 minSdkVersion 24
targetSdk 35 targetSdk 35
versionCode 17 versionCode 18
versionName '4.9.8' versionName '4.10.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions { javaCompileOptions {

View File

@@ -96,7 +96,12 @@
android:resource="@xml/widget_info"/> android:resource="@xml/widget_info"/>
</receiver> </receiver>
<provider
android:name=".provider.AlbumArtContentProvider"
android:authorities="com.cappielloantonio.tempo.provider"
android:enabled="true"
android:exported="true"
/>
</application> </application>
</manifest> </manifest>

View File

@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.model package com.cappielloantonio.tempo.model
import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.annotation.Keep import androidx.annotation.Keep
@@ -13,6 +14,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.glide.CustomGlideRequest import com.cappielloantonio.tempo.glide.CustomGlideRequest
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider
import com.cappielloantonio.tempo.subsonic.models.Child import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
@@ -197,7 +199,7 @@ class SessionMediaItem() {
fun getMediaItem(): MediaItem { fun getMediaItem(): MediaItem {
val uri: Uri = getStreamUri() val uri: Uri = getStreamUri()
val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize())) val artworkUri = AlbumArtContentProvider.contentUri(coverArtId)
val bundle = Bundle() val bundle = Bundle()
bundle.putString("id", id) bundle.putString("id", id)

View File

@@ -0,0 +1,149 @@
package com.cappielloantonio.tempo.provider;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.util.Preferences;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
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 ALBUM_ART = "albumArt";
private ExecutorService executor;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, "albumArt/*", 1);
}
public static Uri contentUri(String artworkId) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(ALBUM_ART)
.appendPath(artworkId)
.build();
}
@Nullable
@Override
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()));
try {
// use pipe to communicate between background thread and caller of openFile()
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
// perform loading in background thread to avoid blocking UI
executor.execute(() -> {
try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
// request artwork from API using Glide
File file = Glide.with(context)
.asFile()
.load(artworkUri)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.submit()
.get();
// copy artwork down pipe returned by ContentProvider
try (InputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
writeSide.closeWithError("Failed to load image: " + e.getMessage());
}
} catch (Exception e) {
try {
writeSide.closeWithError("Failed to load image: " + e.getMessage());
} catch (IOException ignored) {}
}
});
return readSide;
} catch (IOException e) {
throw new FileNotFoundException("Could not create pipe: " + e.getMessage());
}
}
@Override
public boolean onCreate() {
executor = Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors() / 2)
);
return true;
}
@Override
public void shutdown() {
if (executor != null) {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return "";
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}

View File

@@ -1,6 +1,7 @@
package com.cappielloantonio.tempo.repository; package com.cappielloantonio.tempo.repository;
import android.content.ContentResolver;
import android.net.Uri; import android.net.Uri;
import android.view.View; import android.view.View;
@@ -22,6 +23,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.SessionMediaItem; import com.cappielloantonio.tempo.model.SessionMediaItem;
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
import com.cappielloantonio.tempo.service.DownloaderManager; import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
@@ -70,7 +72,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) { for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName()) .setTitle(album.getName())
@@ -217,7 +219,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) { for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName()) .setTitle(album.getName())
@@ -272,7 +274,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (ArtistID3 artist : artists) { for (ArtistID3 artist : artists) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName()) .setTitle(artist.getName())
@@ -397,7 +399,7 @@ public class AutomotiveRepository {
List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren(); List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren();
for (Child song : children) { for (Child song : children) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(song.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(song.getTitle()) .setTitle(song.getTitle())
@@ -451,7 +453,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (Child child : directory.getChildren()) { for (Child child : directory.getChildren()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(child.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(child.getTitle()) .setTitle(child.getTitle())
@@ -550,7 +552,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (PodcastEpisode episode : episodes) { for (PodcastEpisode episode : episodes) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(episode.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(episode.getTitle()) .setTitle(episode.getTitle())
@@ -687,7 +689,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) { for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName()) .setTitle(album.getName())
@@ -800,7 +802,7 @@ public class AutomotiveRepository {
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) { if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) { for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName()) .setTitle(artist.getName())
@@ -822,7 +824,7 @@ public class AutomotiveRepository {
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) { if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) { for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder() MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName()) .setTitle(album.getName())

View File

@@ -33,6 +33,8 @@ import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
private const val TAG = "BaseMediaService"
@UnstableApi @UnstableApi
open class BaseMediaService : MediaLibraryService() { open class BaseMediaService : MediaLibraryService() {
companion object { companion object {
@@ -82,7 +84,7 @@ open class BaseMediaService : MediaLibraryService() {
} }
fun updateMediaItems(player: Player) { fun updateMediaItems(player: Player) {
Log.d(javaClass.toString(), "update items") Log.d(TAG, "update items")
val n = player.mediaItemCount val n = player.mediaItemCount
val k = player.currentMediaItemIndex val k = player.currentMediaItemIndex
val current = player.currentPosition val current = player.currentPosition
@@ -121,7 +123,7 @@ open class BaseMediaService : MediaLibraryService() {
fun initializePlayerListener(player: Player) { fun initializePlayerListener(player: Player) {
player.addListener(object : Player.Listener { player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
Log.d(javaClass.toString(), "onMediaItemTransition" + player.currentMediaItemIndex) Log.d(TAG, "onMediaItemTransition" + player.currentMediaItemIndex)
if (mediaItem == null) return if (mediaItem == null) return
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
@@ -131,7 +133,7 @@ open class BaseMediaService : MediaLibraryService() {
} }
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
Log.d(javaClass.toString(), "onTracksChanged " + player.currentMediaItemIndex) Log.d(TAG, "onTracksChanged " + player.currentMediaItemIndex)
ReplayGainUtil.setReplayGain(player, tracks) ReplayGainUtil.setReplayGain(player, tracks)
val currentMediaItem = player.currentMediaItem val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null) { if (currentMediaItem != null) {
@@ -151,7 +153,7 @@ open class BaseMediaService : MediaLibraryService() {
if (player is ExoPlayer) { if (player is ExoPlayer) {
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs // https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
if (MediaManager.justStarted.get()) { if (MediaManager.justStarted.get()) {
Log.d(javaClass.toString(), "update shuffle order") Log.d(TAG, "update shuffle order")
MediaManager.justStarted.set(false) MediaManager.justStarted.set(false)
val shuffledList = IntArray(player.mediaItemCount) { i -> i } val shuffledList = IntArray(player.mediaItemCount) { i -> i }
shuffledList.shuffle() shuffledList.shuffle()
@@ -169,7 +171,7 @@ open class BaseMediaService : MediaLibraryService() {
} }
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.d(javaClass.toString(), "onIsPlayingChanged " + player.currentMediaItemIndex) Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
if (!isPlaying) { if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp( MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem, player.currentMediaItem,
@@ -187,7 +189,7 @@ open class BaseMediaService : MediaLibraryService() {
} }
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
Log.d(javaClass.toString(), "onPlaybackStateChanged") Log.d(TAG, "onPlaybackStateChanged")
super.onPlaybackStateChanged(playbackState) super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() && if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED && playbackState == Player.STATE_ENDED &&
@@ -204,7 +206,7 @@ open class BaseMediaService : MediaLibraryService() {
newPosition: Player.PositionInfo, newPosition: Player.PositionInfo,
reason: Int reason: Int
) { ) {
Log.d(javaClass.toString(), "onPositionDiscontinuity") Log.d(TAG, "onPositionDiscontinuity")
super.onPositionDiscontinuity(oldPosition, newPosition, reason) super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
@@ -228,7 +230,7 @@ open class BaseMediaService : MediaLibraryService() {
} }
override fun onAudioSessionIdChanged(audioSessionId: Int) { override fun onAudioSessionIdChanged(audioSessionId: Int) {
Log.d(javaClass.toString(), "onAudioSessionIdChanged") Log.d(TAG, "onAudioSessionIdChanged")
attachEqualizerIfPossible(audioSessionId) attachEqualizerIfPossible(audioSessionId)
} }
}) })
@@ -320,7 +322,7 @@ open class BaseMediaService : MediaLibraryService() {
} }
private fun initializeMediaLibrarySession(player: Player) { private fun initializeMediaLibrarySession(player: Player) {
Log.d(javaClass.toString(), "initializeMediaLibrarySession") Log.d(TAG, "initializeMediaLibrarySession")
val sessionActivityPendingIntent = val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run { TaskStackBuilder.create(this).run {
addNextIntent(Intent(baseContext, MainActivity::class.java)) addNextIntent(Intent(baseContext, MainActivity::class.java))
@@ -467,7 +469,7 @@ open class BaseMediaService : MediaLibraryService() {
customCommand: SessionCommand, customCommand: SessionCommand,
args: Bundle args: Bundle
): ListenableFuture<SessionResult> { ): ListenableFuture<SessionResult> {
Log.d(javaClass.toString(), "onCustomCommand") Log.d(TAG, "onCustomCommand")
when (customCommand.customAction) { when (customCommand.customAction) {
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
@@ -492,7 +494,7 @@ open class BaseMediaService : MediaLibraryService() {
controller: ControllerInfo, controller: ControllerInfo,
mediaItems: List<MediaItem> mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> { ): ListenableFuture<List<MediaItem>> {
Log.d(javaClass.toString(), "onAddMediaItems") Log.d(TAG, "onAddMediaItems")
val updatedMediaItems = mediaItems.map { mediaItem -> val updatedMediaItems = mediaItems.map { mediaItem ->
val mediaMetadata = mediaItem.mediaMetadata val mediaMetadata = mediaItem.mediaMetadata
val newMetadata = mediaMetadata.buildUpon() val newMetadata = mediaMetadata.buildUpon()

View File

@@ -444,24 +444,33 @@ public class MediaManager {
} }
@OptIn(markerClass = UnstableApi.class) @OptIn(markerClass = UnstableApi.class)
public static void continuousPlay(MediaItem mediaItem, ListenableFuture<MediaBrowser> existingBrowserFuture) { public static void continuousPlay(MediaItem mediaItem,
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { ListenableFuture<MediaBrowser> existingBrowserFuture) {
Preferences.setLastInstantMix(); if (mediaItem == null
|| !Preferences.isContinuousPlayEnabled()
LiveData<List<Child>> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId, 25); || !Preferences.isInstantMixUsable()) {
return;
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media != null && existingBrowserFuture != null) {
Log.d(TAG, "Continuous play: adding " + media.size() + " tracks");
enqueue(existingBrowserFuture, media, false);
}
instantMix.removeObserver(this);
}
});
} }
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix =
getSongRepository().getContinuousMix(mediaItem.mediaId, 25);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media == null || media.isEmpty()) {
return;
}
if (existingBrowserFuture != null) {
Log.d(TAG, "Continuous play: adding " + media.size() + " tracks");
enqueue(existingBrowserFuture, media, true);
}
instantMix.removeObserver(this);
}
});
} }
public static void saveChronology(MediaItem mediaItem) { public static void saveChronology(MediaItem mediaItem) {

View File

@@ -2,6 +2,8 @@ package com.cappielloantonio.tempo.ui.activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
@@ -11,6 +13,7 @@ import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen; import androidx.core.splashscreen.SplashScreen;
@@ -62,6 +65,7 @@ public class MainActivity extends BaseActivity {
private BottomNavigationView bottomNavigationView; private BottomNavigationView bottomNavigationView;
public NavController navController; public NavController navController;
private BottomSheetBehavior bottomSheetBehavior; private BottomSheetBehavior bottomSheetBehavior;
private boolean isLandscape = false;
private AssetLinkNavigator assetLinkNavigator; private AssetLinkNavigator assetLinkNavigator;
private AssetLinkUtil.AssetLink pendingAssetLink; private AssetLinkUtil.AssetLink pendingAssetLink;
@@ -85,6 +89,8 @@ public class MainActivity extends BaseActivity {
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this); connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
connectivityStatusReceiverManager(true); connectivityStatusReceiverManager(true);
isLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
init(); init();
checkConnectionType(); checkConnectionType();
getOpenSubsonicExtensions(); getOpenSubsonicExtensions();
@@ -141,6 +147,15 @@ public class MainActivity extends BaseActivity {
} else { } else {
goToLogin(); 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);
}
} }
// BOTTOM SHEET/NAVIGATION // BOTTOM SHEET/NAVIGATION
@@ -215,7 +230,9 @@ public class MainActivity extends BaseActivity {
@Override @Override
public void onSlide(@NonNull View view, float slideOffset) { public void onSlide(@NonNull View view, float slideOffset) {
animateBottomSheet(slideOffset); animateBottomSheet(slideOffset);
animateBottomNavigation(slideOffset, navigationHeight); if (!isLandscape) {
animateBottomNavigation(slideOffset, navigationHeight);
}
} }
}; };

View File

@@ -173,10 +173,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
) )
) )
) { ) {
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
if (songs.get(position).getDiscNumber() != null && !Objects.requireNonNull(songs.get(position).getDiscNumber()).toString().isBlank()) { if (songs.get(position).getDiscNumber() != null && !Objects.requireNonNull(songs.get(position).getDiscNumber()).toString().isBlank()) {
holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titleless, songs.get(position).getDiscNumber().toString())); holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titleless, songs.get(position).getDiscNumber().toString()));
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
} else {
holder.item.differentDiskDividerSector.setVisibility(View.GONE);
} }
if (album.getDiscTitles() != null) { if (album.getDiscTitles() != null) {

View File

@@ -0,0 +1,57 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class PlaybackSpeedDialog extends DialogFragment {
private static final String TAG = "PlaybackSpeedDialog";
public interface PlaybackSpeedListener {
void onSpeedSelected(float speed);
}
private PlaybackSpeedListener listener;
private static final float[] SPEED_VALUES = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
private static final String[] SPEED_LABELS = {"0.5x", "0.75x", "1.0x", "1.25x", "1.5x", "1.75x", "2.0x"};
public void setPlaybackSpeedListener(PlaybackSpeedListener listener) {
this.listener = listener;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
float currentSpeed = Preferences.getPlaybackSpeed();
int selectedIndex = getSelectedIndex(currentSpeed);
return new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.playback_speed_dialog_title)
.setSingleChoiceItems(SPEED_LABELS, selectedIndex, (dialog, which) -> {
float selectedSpeed = SPEED_VALUES[which];
Preferences.setPlaybackSpeed(selectedSpeed);
if (listener != null) {
listener.onSpeedSelected(selectedSpeed);
}
dialog.dismiss();
})
.setNegativeButton(R.string.playback_speed_dialog_negative_button, (dialog, id) -> dialog.cancel())
.create();
}
private int getSelectedIndex(float currentSpeed) {
for (int i = 0; i < SPEED_VALUES.length; i++) {
if (Math.abs(SPEED_VALUES[i] - currentSpeed) < 0.01f) {
return i;
}
}
return 2; // Default to 1.0x
}
}

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -49,6 +50,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
private AlbumCatalogueViewModel albumCatalogueViewModel; private AlbumCatalogueViewModel albumCatalogueViewModel;
private AlbumCatalogueAdapter albumAdapter; private AlbumCatalogueAdapter albumAdapter;
private int spanCount = 2;
private String currentSortOrder; private String currentSortOrder;
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums; private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
@@ -90,6 +92,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false); bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
initAppBar(); initAppBar();
initAlbumCatalogueView(); initAlbumCatalogueView();
initProgressLoader(); initProgressLoader();
@@ -133,8 +139,8 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private void initAlbumCatalogueView() { private void initAlbumCatalogueView() {
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.albumCatalogueRecyclerView.setHasFixedSize(true); bind.albumCatalogueRecyclerView.setHasFixedSize(true);
albumAdapter = new AlbumCatalogueAdapter(this, true); albumAdapter = new AlbumCatalogueAdapter(this, true);

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@@ -50,6 +51,7 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
private ArtistCatalogueViewModel artistCatalogueViewModel; private ArtistCatalogueViewModel artistCatalogueViewModel;
private ArtistCatalogueAdapter artistAdapter; private ArtistCatalogueAdapter artistAdapter;
private int spanCount = 2;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -66,6 +68,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false); bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
View view = bind.getRoot(); View view = bind.getRoot();
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
initAppBar(); initAppBar();
initArtistCatalogueView(); initArtistCatalogueView();
@@ -108,8 +114,8 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private void initArtistCatalogueView() { private void initArtistCatalogueView() {
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.artistCatalogueRecyclerView.setHasFixedSize(true); bind.artistCatalogueRecyclerView.setHasFixedSize(true);
artistAdapter = new ArtistCatalogueAdapter(this); artistAdapter = new ArtistCatalogueAdapter(this);

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@@ -63,6 +64,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private int spanCount = 2;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity(); activity = (MainActivity) getActivity();
@@ -72,6 +75,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class); artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
init(view); init(view);
initAppBar(); initAppBar();
initArtistInfo(); initArtistInfo();
@@ -277,8 +284,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
} }
private void initAlbumsView() { private void initAlbumsView() {
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.albumsRecyclerView.setHasFixedSize(true); bind.albumsRecyclerView.setHasFixedSize(true);
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false); albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
@@ -296,8 +303,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
} }
private void initSimilarArtistsView() { private void initSimilarArtistsView() {
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.similarArtistsRecyclerView.setHasFixedSize(true); bind.similarArtistsRecyclerView.setHasFixedSize(true);
artistCatalogueAdapter = new ArtistCatalogueAdapter(this); artistCatalogueAdapter = new ArtistCatalogueAdapter(this);

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@@ -32,6 +33,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel; import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
@OptIn(markerClass = UnstableApi.class) @OptIn(markerClass = UnstableApi.class)
@@ -41,6 +43,7 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
private GenreCatalogueViewModel genreCatalogueViewModel; private GenreCatalogueViewModel genreCatalogueViewModel;
private GenreCatalogueAdapter genreCatalogueAdapter; private GenreCatalogueAdapter genreCatalogueAdapter;
private int spanCount = 2;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -56,6 +59,10 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
View view = bind.getRoot(); View view = bind.getRoot();
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class); genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
init(); init();
initAppBar(); initAppBar();
initGenreCatalogueView(); initGenreCatalogueView();
@@ -97,8 +104,8 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private void initGenreCatalogueView() { private void initGenreCatalogueView() {
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 16, false)); bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 16, false));
bind.genreCatalogueRecyclerView.setHasFixedSize(true); bind.genreCatalogueRecyclerView.setHasFixedSize(true);
genreCatalogueAdapter = new GenreCatalogueAdapter(this); genreCatalogueAdapter = new GenreCatalogueAdapter(this);

View File

@@ -39,6 +39,7 @@ import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBindi
import com.cappielloantonio.tempo.service.EqualizerManager; import com.cappielloantonio.tempo.service.EqualizerManager;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.PlaybackSpeedDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog; import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog; import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager; import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
@@ -522,13 +523,12 @@ public class PlayerControllerFragment extends Fragment {
private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) { private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) {
playbackSpeedButton.setOnClickListener(view -> { playbackSpeedButton.setOnClickListener(view -> {
float currentSpeed = Preferences.getPlaybackSpeed(); PlaybackSpeedDialog dialog = new PlaybackSpeedDialog();
dialog.setPlaybackSpeedListener(speed -> {
currentSpeed += 0.25f; mediaBrowser.setPlaybackParameters(new PlaybackParameters(speed));
if (currentSpeed > 2.0f) currentSpeed = 0.5f; playbackSpeedButton.setText(getString(R.string.player_playback_speed, speed));
mediaBrowser.setPlaybackParameters(new PlaybackParameters(currentSpeed)); });
playbackSpeedButton.setText(getString(R.string.player_playback_speed, currentSpeed)); dialog.show(requireActivity().getSupportFragmentManager(), null);
Preferences.setPlaybackSpeed(currentSpeed);
}); });
skipSilenceToggleButton.setOnClickListener(view -> { skipSilenceToggleButton.setOnClickListener(view -> {

View File

@@ -253,7 +253,7 @@ public class PlayerLyricsFragment extends Fragment {
if (lines != null) { if (lines != null) {
for (Line line : lines) { for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n"); lyricsBuilder.append(line.getValue().trim()).append("\n\n");
} }
} }
@@ -316,7 +316,7 @@ public class PlayerLyricsFragment extends Fragment {
StringBuilder lyricsBuilder = new StringBuilder(); StringBuilder lyricsBuilder = new StringBuilder();
for (Line line : lines) { for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n"); lyricsBuilder.append(line.getValue().trim()).append("\n\n");
} }
String lyrics = lyricsBuilder.toString(); String lyrics = lyricsBuilder.toString();
Spannable spannableString = new SpannableString(lyrics); Spannable spannableString = new SpannableString(lyrics);
@@ -328,7 +328,7 @@ public class PlayerLyricsFragment extends Fragment {
boolean highlight = i == curIdx; boolean highlight = i == curIdx;
if (highlight) highlightStart = offset; if (highlight) highlightStart = offset;
int len = lines.get(i).getValue().length() + 1; int len = lines.get(i).getValue().length() + 2;
final int lineStart = lines.get(i).getStart(); final int lineStart = lines.get(i).getStart();
spannableString.setSpan(new ClickableSpan() { spannableString.setSpan(new ClickableSpan() {
@Override @Override

View File

@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.util; package com.cappielloantonio.tempo.util;
import android.content.ContentResolver;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@@ -15,6 +16,7 @@ import androidx.media3.common.HeartRating;
import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
import com.cappielloantonio.tempo.repository.DownloadRepository; import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
@@ -45,7 +47,7 @@ public class MappingUtil {
Uri artworkUri = null; Uri artworkUri = null;
if (coverArtId != null) { if (coverArtId != null) {
artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, Preferences.getImageSize())); artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
} }
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
@@ -235,7 +237,7 @@ public class MappingUtil {
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) { public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
Uri uri = getUri(podcastEpisode); Uri uri = getUri(podcastEpisode);
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize())); Uri artworkUri = AlbumArtContentProvider.contentUri(podcastEpisode.getCoverArtId());
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("id", podcastEpisode.getId()); bundle.putString("id", podcastEpisode.getId());

View File

@@ -29,6 +29,7 @@ object Preferences {
private const val REPEAT_MODE = "repeat_mode" private const val REPEAT_MODE = "repeat_mode"
private const val IMAGE_CACHE_SIZE = "image_cache_size" private const val IMAGE_CACHE_SIZE = "image_cache_size"
private const val STREAMING_CACHE_SIZE = "streaming_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 IMAGE_SIZE = "image_size" private const val IMAGE_SIZE = "image_size"
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi" private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile" private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
@@ -304,6 +305,11 @@ object Preferences {
return App.getInstance().preferences.getString(IMAGE_CACHE_SIZE, "500")!!.toInt() return App.getInstance().preferences.getString(IMAGE_CACHE_SIZE, "500")!!.toInt()
} }
@JvmStatic
fun getLandscapeItemsPerRow(): Int {
return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt()
}
@JvmStatic @JvmStatic
fun getImageSize(): Int { fun getImageSize(): Int {
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt() return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotY="12"
android:pivotX="12"
android:rotation="270">
<path
android:fillColor="#FF000000"
android:pathData="M8,18c0.55,0 1,-0.45 1,-1L9,7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v10c0,0.55 0.45,1 1,1zM12,22c0.55,0 1,-0.45 1,-1L13,3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v18c0,0.55 0.45,1 1,1zM4,14c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM16,18c0.55,0 1,-0.45 1,-1L17,7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v10c0,0.55 0.45,1 1,1zM19,11v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1z" />
</group>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotY="12"
android:pivotX="12"
android:rotation="270">
<path
android:fillColor="#FF000000"
android:pathData="M12,5.69l5,4.5V18h-2v-6H9v6H7v-7.81l5,-4.5M12,3L2,12h3v8h6v-6h2v6h6v-8h3L12,3z" />
</group>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotY="12"
android:pivotX="12"
android:rotation="270">
<path
android:fillColor="#FF000000"
android:pathData="M11,5v5.59L7.5,10.59l4.5,4.5 4.5,-4.5L13,10.59L13,5h-2zM6,14c0,3.31 2.69,6 6,6s6,-2.69 6,-6h-2c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4L6,14z"/>
</group>
</vector>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:orientation="vertical">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:layout_width="75dp"
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_gravity="center"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:visibility="gone"
app:menu="@menu/bottom_nav_menu" />
</FrameLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</LinearLayout>
<FrameLayout
android:id="@+id/player_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
app:layout_behavior="@string/bottom_sheet_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<TextView
android:id="@+id/offline_mode_text_view"
style="@style/NoConnectionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/activity_info_offline_mode"
android:textSize="6sp"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,305 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/album_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:paddingStart="20dp"
app:layout_scrollFlags="exitUntilCollapsed">
<ImageView
android:id="@+id/album_cover_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="64dp"
android:layout_marginTop="8dp"
android:layout_marginRight="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/album_name_label"
style="@style/LabelExtraLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingTop="8dp"
android:singleLine="false"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="@+id/album_cover_image_view"
app:layout_constraintStart_toStartOf="@+id/album_cover_image_view"
app:layout_constraintTop_toBottomOf="@+id/album_cover_image_view" />
<FrameLayout
android:id="@+id/album_other_info_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@+id/album_name_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_name_label"
app:layout_constraintTop_toTopOf="@+id/album_name_label">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/ic_arrow_down" />
</FrameLayout>
<TextView
android:id="@+id/album_artist_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_name_label" />
<TextView
android:id="@+id/album_release_year_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_artist_label" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/album_detail_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_release_year_label"
tools:visibility="visible">
<TextView
android:id="@+id/album_genres_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/album_song_count_duration_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:paddingVertical="2dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_genres_textview" />
<TextView
android:id="@+id/album_notes_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:justificationMode="inter_word"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_song_count_duration_textview" />
<TextView
android:id="@+id/album_release_years_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:paddingVertical="4dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_notes_textview" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_detail_view" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
<LinearLayout
android:id="@+id/album_page_button_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/album_page_play_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/album_page_play_button"
android:textAllCaps="false"
app:icon="@drawable/ic_play"
app:iconGravity="textStart"
app:iconPadding="18dp" />
<Button
android:id="@+id/album_page_shuffle_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/album_page_shuffle_button"
android:textAllCaps="false"
app:icon="@drawable/ic_shuffle"
app:iconGravity="textStart"
app:iconPadding="18dp" />
</LinearLayout>
<ToggleButton
android:id="@+id/button_favorite"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="0dp"
android:background="@drawable/button_favorite_selector"
android:checked="false"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text=""
android:textOff=""
android:textOn="" />
</LinearLayout>
<TextView
android:id="@+id/album_bio_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/album_page_button_layout"
tools:ignore="NotSibling" />
<View
android:id="@+id/bottom_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_bio_label" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:nestedScrollingEnabled="false"
android:paddingTop="0dp"
android:paddingBottom="75dp"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:paddingBottom="@dimen/global_padding_bottom"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/playlist_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/playlist_cover_image_view_top_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_top_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/playlist_cover_image_view_top_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="64dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_top_left"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/playlist_cover_image_view_bottom_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_bottom_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/playlist_cover_image_view_top_left" />
<ImageView
android:id="@+id/playlist_cover_image_view_bottom_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_bottom_left"
app:layout_constraintTop_toTopOf="@id/playlist_cover_image_view_bottom_left" />
<TextView
android:id="@+id/playlist_name_label"
style="@style/LabelExtraLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:ellipsize="end"
android:maxLines="2"
android:paddingTop="8dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_cover_image_view_bottom_left" />
<TextView
android:id="@+id/playlist_song_count_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_name_label" />
<TextView
android:id="@+id/playlist_duration_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_song_count_label" />
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_duration_label" />
<LinearLayout
android:id="@+id/playlist_page_button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
<Button
android:id="@+id/playlist_page_play_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/playlist_page_play_button"
android:textAllCaps="false"
app:icon="@drawable/ic_play"
app:iconGravity="textStart"
app:iconPadding="18dp" />
<Button
android:id="@+id/playlist_page_shuffle_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/playlist_page_shuffle_button"
android:textAllCaps="false"
app:icon="@drawable/ic_shuffle"
app:iconGravity="textStart"
app:iconPadding="18dp" />
</LinearLayout>
<TextView
android:id="@+id/album_bio_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
<View
android:id="@+id/bottom_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -46,6 +46,8 @@
style="@style/BodyLarge" style="@style/BodyLarge"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center_horizontal"
android:lineSpacingExtra="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/ic_home_land"
android:title="@string/menu_home_label" />
<item
android:id="@+id/libraryFragment"
android:icon="@drawable/ic_graphic_eq_land"
android:title="@string/menu_library_label" />
<item
android:id="@+id/downloadFragment"
android:icon="@drawable/ic_play_for_work_land"
android:title="@string/menu_download_label" />
</menu>

View File

@@ -264,4 +264,18 @@
<item>3</item> <item>3</item>
<item>4</item> <item>4</item>
</string-array> </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> </resources>

View File

@@ -217,6 +217,8 @@
<string name="menu_unpin_button">Remove from home screen</string> <string name="menu_unpin_button">Remove from home screen</string>
<string name="menu_sort_year">Year</string> <string name="menu_sort_year">Year</string>
<string name="player_playback_speed">%1$.2fx</string> <string name="player_playback_speed">%1$.2fx</string>
<string name="playback_speed_dialog_title">Playback Speed</string>
<string name="playback_speed_dialog_negative_button">Cancel</string>
<string name="player_queue_clean_all_button">Clean play queue</string> <string name="player_queue_clean_all_button">Clean play queue</string>
<string name="player_queue_save_queue_success">Saved play queue</string> <string name="player_queue_save_queue_success">Saved play queue</string>
<string name="player_queue_save_to_playlist">Save Queue to Playlist</string> <string name="player_queue_save_to_playlist">Save Queue to Playlist</string>
@@ -403,6 +405,7 @@
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string> <string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for downloads\" is set to \"Direct download\" the bitrate of the file will not be changed.</string> <string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for downloads\" is set to \"Direct download\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_estimate_content_length">When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer.</string> <string name="settings_summary_transcoding_estimate_content_length">When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer.</string>
<string name="settings_summary_landscape_items_per_row">Applies to all album and artist listings. Defaults to 4</string>
<string name="settings_sync_starred_artists_for_offline_use_summary">If enabled, starred artists will be downloaded for offline use.</string> <string name="settings_sync_starred_artists_for_offline_use_summary">If enabled, starred artists will be downloaded for offline use.</string>
<string name="settings_sync_starred_artists_for_offline_use_title">Sync starred artists for offline use</string> <string name="settings_sync_starred_artists_for_offline_use_title">Sync starred artists for offline use</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">If enabled, starred albums will be downloaded for offline use.</string> <string name="settings_sync_starred_albums_for_offline_use_summary">If enabled, starred albums will be downloaded for offline use.</string>
@@ -423,6 +426,8 @@
<string name="settings_title_transcoding">Transcoding</string> <string name="settings_title_transcoding">Transcoding</string>
<string name="settings_title_transcoding_download">Transcoding Download</string> <string name="settings_title_transcoding_download">Transcoding Download</string>
<string name="settings_title_ui">UI</string> <string name="settings_title_ui">UI</string>
<string name="settings_title_ui_landscape_items_per_row">Items per row on landscape</string>
<string name="settings_title_ui_landscape_items_per_row_dialog">Number of items per row</string>
<string name="settings_transcoded_download">Transcoded download</string> <string name="settings_transcoded_download">Transcoded download</string>
<string name="settings_version_summary" translatable="false">3.1.0</string> <string name="settings_version_summary" translatable="false">3.1.0</string>
<string name="settings_version_title">Version</string> <string name="settings_version_title">Version</string>

View File

@@ -137,6 +137,15 @@
android:summary="@string/search_sort_summary" android:summary="@string/search_sort_summary"
android:key="sort_search_chronologically" /> android:key="sort_search_chronologically" />
<ListPreference
app:defaultValue="4"
app:dialogTitle="@string/settings_title_ui_landscape_items_per_row_dialog"
app:entries="@array/landscape_items_per_row"
app:entryValues="@array/landscape_items_per_row_values"
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>
<PreferenceCategory app:title="@string/settings_title_playlist"> <PreferenceCategory app:title="@string/settings_title_playlist">

View File

@@ -0,0 +1,4 @@
fix: Addressing some UI/UX quirks
fix: keep observer until data is received on continuousPlay bug
fix: album art now displays on android auto
feat: improve landscape view and increase items per row on landscape view