Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ed78d959 | ||
|
|
5ad99b9f27 | ||
|
|
3de5390140 | ||
|
|
d215581e19 | ||
|
|
54612c6b74 | ||
|
|
eaac728a26 | ||
|
|
65d2f8e33f | ||
|
|
baf4e0f0fc | ||
|
|
26c7bee106 | ||
|
|
6e51611867 | ||
|
|
d67e432731 | ||
|
|
8b61396b0f | ||
|
|
0fb6e55b12 | ||
|
|
dd7aa2291b | ||
|
|
ec33c32c89 | ||
|
|
e0ad4e3701 | ||
|
|
253f8033c5 | ||
|
|
c1aed1a4c1 | ||
|
|
23f58439ba | ||
|
|
4c99ced597 | ||
|
|
8d215a7f1c | ||
|
|
38fc4a0936 | ||
|
|
d9949349da | ||
|
|
877d29d285 | ||
|
|
9a17aa8b98 | ||
|
|
fd41395ab8 | ||
|
|
269066e036 | ||
|
|
04e692e5e9 |
43
CHANGELOG.md
43
CHANGELOG.md
@@ -3,7 +3,46 @@
|
|||||||
## Pending release
|
## Pending release
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
## [4.9.0](https://github.com/eddyizm/tempo/releases/tag/v4.9.0) (2026-01-24)
|
* 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
|
||||||
|
## [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: Improve Synced Lyrics by @pgrit in https://github.com/eddyizm/tempus/pull/384
|
||||||
|
* fix: Add selector for playlist visibility by @tvillega in https://github.com/eddyizm/tempus/pull/394
|
||||||
|
* chore(i18n): set links as untranslatable by @tvillega in https://github.com/eddyizm/tempus/pull/400
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @tvillega made their first contribution in https://github.com/eddyizm/tempus/pull/394
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.5...v4.5.8
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.9.5](https://github.com/eddyizm/tempo/releases/tag/v4.9.5) (2026-01-26)
|
||||||
|
* fix: Avoid crash when server has no songs by @jaime-grj in https://github.com/eddyizm/tempus/pull/389
|
||||||
|
* fix: updated dialog import to address crashing on android 15 by @eddyizm in https://github.com/eddyizm/tempus/pull/392
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.3...v4.9.5
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.9.3](https://github.com/eddyizm/tempo/releases/tag/v4.9.3) (2026-01-25)
|
||||||
|
* fix: Proper raw stream detection by @jaime-grj in https://github.com/eddyizm/tempus/pull/382
|
||||||
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/381
|
||||||
|
* feat: add configurable timeout by @eddyizm in https://github.com/eddyizm/tempus/pull/386
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.1...v4.9.3
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.9.1](https://github.com/eddyizm/tempo/releases/tag/v4.9.1) (2026-01-24)
|
||||||
* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357
|
* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357
|
||||||
* French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356
|
* French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356
|
||||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364
|
||||||
@@ -20,7 +59,7 @@
|
|||||||
## New Contributors
|
## New Contributors
|
||||||
* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375
|
* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.0
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.1
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13)
|
## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 14
|
versionCode 18
|
||||||
versionName '4.9.0'
|
versionName '4.10.0'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
import static android.provider.Settings.System.getString;
|
|
||||||
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -107,13 +104,13 @@ public class PlaylistRepository {
|
|||||||
return playlistLiveData;
|
return playlistLiveData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||||
if (songsId.isEmpty()) {
|
if (songsId.isEmpty()) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||||
} else{
|
} else{
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
.updatePlaylist(playlistId, null, playlistVisibilityIsPublic, songsId, null)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
|||||||
@@ -283,7 +283,10 @@ public class SongRepository {
|
|||||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> songs = new ArrayList<>();
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
|
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||||
|
if (returned != null) {
|
||||||
|
songs.addAll(returned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
randomSongsSample.setValue(songs);
|
randomSongsSample.setValue(songs);
|
||||||
}
|
}
|
||||||
@@ -299,7 +302,10 @@ public class SongRepository {
|
|||||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> songs = new ArrayList<>();
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
|
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||||
|
if (returned != null) {
|
||||||
|
songs.addAll(returned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
randomSongsSample.setValue(songs);
|
randomSongsSample.setValue(songs);
|
||||||
}
|
}
|
||||||
@@ -342,7 +348,10 @@ public class SongRepository {
|
|||||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
List<Child> songs = new ArrayList<>();
|
List<Child> songs = new ArrayList<>();
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||||
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs()));
|
List<Child> returned = response.body().getSubsonicResponse().getSongsByGenre().getSongs();
|
||||||
|
if (returned != null) {
|
||||||
|
songs.addAll(returned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
songsByGenre.setValue(songs);
|
songsByGenre.setValue(songs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ public class SystemClient {
|
|||||||
|
|
||||||
public Call<ApiResponse> ping() {
|
public Call<ApiResponse> ping() {
|
||||||
Log.d(TAG, "ping()");
|
Log.d(TAG, "ping()");
|
||||||
|
int timeoutSeconds = Preferences.getNetworkPingTimeout();
|
||||||
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
||||||
if (Preferences.isInUseServerAddressLocal()) {
|
if (Preferences.isInUseServerAddressLocal()) {
|
||||||
pingCall.timeout()
|
pingCall.timeout()
|
||||||
.timeout(1, TimeUnit.SECONDS);
|
.timeout(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
} else {
|
} else {
|
||||||
|
int finalTimeout = Math.min(timeoutSeconds * 2, 10);
|
||||||
pingCall.timeout()
|
pingCall.timeout()
|
||||||
.timeout(3, TimeUnit.SECONDS);
|
.timeout(finalTimeout, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
return pingCall;
|
return pingCall;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.view.View;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
||||||
private DialogPlaylistChooserBinding bind;
|
private DialogPlaylistChooserBinding bind;
|
||||||
@@ -35,9 +37,21 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
|||||||
|
|
||||||
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(getActivity())
|
return new MaterialAlertDialogBuilder(getActivity())
|
||||||
.setView(bind.getRoot())
|
.setView(bind.getRoot())
|
||||||
.setTitle(R.string.playlist_chooser_dialog_title)
|
.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) -> { })
|
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
||||||
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
||||||
.create();
|
.create();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.cappielloantonio.tempo.ui.dialog;
|
package com.cappielloantonio.tempo.ui.dialog;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import android.os.Handler;
|
|||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextPaint;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -51,6 +53,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
private Runnable syncLyricsRunnable;
|
private Runnable syncLyricsRunnable;
|
||||||
private String currentLyrics;
|
private String currentLyrics;
|
||||||
private LyricsList currentLyricsList;
|
private LyricsList currentLyricsList;
|
||||||
|
private Integer lastLineIdx;
|
||||||
private String currentDescription;
|
private String currentDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -109,6 +112,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
currentLyrics = null;
|
currentLyrics = null;
|
||||||
currentLyricsList = null;
|
currentLyricsList = null;
|
||||||
currentDescription = null;
|
currentDescription = null;
|
||||||
|
lastLineIdx = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initOverlay() {
|
private void initOverlay() {
|
||||||
@@ -162,6 +166,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
|
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
currentLyricsList = lyricsList;
|
currentLyricsList = lyricsList;
|
||||||
|
lastLineIdx = null;
|
||||||
updatePanelContent();
|
updatePanelContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,7 +199,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||||
|
|
||||||
if (hasStructuredLyrics(currentLyricsList)) {
|
if (hasStructuredLyrics(currentLyricsList)) {
|
||||||
setSyncLirics(currentLyricsList);
|
setSyncLyrics(currentLyricsList);
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
@@ -241,14 +246,14 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
private void setSyncLirics(LyricsList lyricsList) {
|
private void setSyncLyrics(LyricsList lyricsList) {
|
||||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,67 +293,75 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||||
|
|
||||||
if (hasStructuredLyrics(lyricsList)) {
|
if (hasStructuredLyrics(lyricsList)) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
if (lines == null || lines.isEmpty()) {
|
||||||
if (lines == null || lines.isEmpty()) return;
|
return;
|
||||||
|
|
||||||
for (Line line : lines) {
|
|
||||||
lyricsBuilder.append(line.getValue().trim()).append("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null);
|
// Find the index of the currently playing line
|
||||||
|
int curIdx = 0;
|
||||||
if (toHighlight != null) {
|
for (; curIdx < lines.size(); ++curIdx) {
|
||||||
String lyrics = lyricsBuilder.toString();
|
Integer start = lines.get(curIdx).getStart();
|
||||||
Spannable spannableString = new SpannableString(lyrics);
|
if (start != null && start > timestamp) {
|
||||||
|
curIdx--; // Found the first line that starts after the current timestamp
|
||||||
int startingPosition = getStartPosition(lines, toHighlight);
|
break;
|
||||||
int endingPosition = startingPosition + toHighlight.getValue().length();
|
|
||||||
|
|
||||||
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
|
|
||||||
bind.nowPlayingSongLyricsTextView.setText(spannableString);
|
|
||||||
|
|
||||||
if (playerBottomSheetViewModel.getSyncLyricsState()) {
|
|
||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getStartPosition(List<Line> lines, Line toHighlight) {
|
// Only update if the highlighted line has changed
|
||||||
int start = 0;
|
if (lastLineIdx != null && curIdx == lastLineIdx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastLineIdx = curIdx;
|
||||||
|
|
||||||
for (Line line : lines) {
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
if (line != toHighlight) {
|
for (Line line : lines) {
|
||||||
start = start + line.getValue().length() + 1;
|
lyricsBuilder.append(line.getValue().trim()).append("\n\n");
|
||||||
} else {
|
}
|
||||||
break;
|
String lyrics = lyricsBuilder.toString();
|
||||||
|
Spannable spannableString = new SpannableString(lyrics);
|
||||||
|
|
||||||
|
// Make each line clickable for navigation and highlight the current one
|
||||||
|
int offset = 0;
|
||||||
|
int highlightStart = -1;
|
||||||
|
for (int i = 0; i < lines.size(); ++i) {
|
||||||
|
boolean highlight = i == curIdx;
|
||||||
|
if (highlight) highlightStart = offset;
|
||||||
|
|
||||||
|
int len = lines.get(i).getValue().length() + 2;
|
||||||
|
final int lineStart = lines.get(i).getStart();
|
||||||
|
spannableString.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View view) {
|
||||||
|
// Seeking to 1ms after the actual start prevents scrolling / highlighting artifacts
|
||||||
|
mediaBrowser.seekTo(lineStart + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
|
super.updateDrawState(ds);
|
||||||
|
ds.setUnderlineText(false);
|
||||||
|
if (highlight) {
|
||||||
|
ds.setColor(requireContext().getResources().getColor(R.color.lyricsTextColor, null));
|
||||||
|
} else {
|
||||||
|
ds.setColor(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, offset, offset + len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
offset += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.nowPlayingSongLyricsTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
bind.nowPlayingSongLyricsTextView.setText(spannableString);
|
||||||
|
|
||||||
|
// Scroll to the highlighted line, but only if there is one
|
||||||
|
if (highlightStart >= 0 && playerBottomSheetViewModel.getSyncLyricsState()) {
|
||||||
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(highlightStart));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getLineCount(List<Line> lines, Line toHighlight) {
|
private int getScroll(int startIndex) {
|
||||||
int start = 0;
|
|
||||||
|
|
||||||
for (Line line : lines) {
|
|
||||||
if (line != toHighlight) {
|
|
||||||
bind.tempLyricsLineTextView.setText(line.getValue());
|
|
||||||
start = start + bind.tempLyricsLineTextView.getLineCount();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getScroll(List<Line> lines, Line toHighlight) {
|
|
||||||
int startIndex = getStartPosition(lines, toHighlight);
|
|
||||||
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
||||||
if (layout == null) return 0;
|
if (layout == null) return 0;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.media.audiofx.AudioEffect;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.text.InputFilter;
|
||||||
|
import android.text.InputType;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -27,6 +29,7 @@ import androidx.media3.common.util.UnstableApi;
|
|||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavOptions;
|
import androidx.navigation.NavOptions;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.preference.EditTextPreference;
|
||||||
import androidx.preference.ListPreference;
|
import androidx.preference.ListPreference;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceCategory;
|
import androidx.preference.PreferenceCategory;
|
||||||
@@ -141,6 +144,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
setStreamingCacheSize();
|
setStreamingCacheSize();
|
||||||
setAppLanguage();
|
setAppLanguage();
|
||||||
setVersion();
|
setVersion();
|
||||||
|
setNetorkPingTimeoutBase();
|
||||||
|
|
||||||
actionLogout();
|
actionLogout();
|
||||||
actionScan();
|
actionScan();
|
||||||
@@ -261,6 +265,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setNetorkPingTimeoutBase() {
|
||||||
|
EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base");
|
||||||
|
|
||||||
|
if (networkPingTimeoutBase != null) {
|
||||||
|
networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance());
|
||||||
|
networkPingTimeoutBase.setOnBindEditTextListener(editText -> {
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> {
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (!Character.isDigit(source.charAt(i))) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
|
networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
String input = (String) newValue;
|
||||||
|
return input != null && !input.isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setStreamingCacheSize() {
|
private void setStreamingCacheSize() {
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ class DynamicMediaSourceFactory(
|
|||||||
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
|
|
||||||
val uri = mediaItem.localConfiguration?.uri
|
val uri = mediaItem.localConfiguration?.uri
|
||||||
val isTranscoding = uri?.getQueryParameter("maxBitRate") != null ||
|
val isTranscoding = uri?.getQueryParameter("format") != null && uri.getQueryParameter("format") != "raw"
|
||||||
(uri?.getQueryParameter("format") != null && uri?.getQueryParameter("format") != "raw")
|
|
||||||
|
|
||||||
if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) {
|
if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) {
|
||||||
TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory)
|
TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory)
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -85,6 +86,8 @@ object Preferences {
|
|||||||
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
|
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
|
||||||
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
|
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
|
||||||
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
||||||
|
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServer(): String? {
|
fun getServer(): String? {
|
||||||
@@ -96,6 +99,19 @@ object Preferences {
|
|||||||
App.getInstance().preferences.edit().putString(SERVER, server).apply()
|
App.getInstance().preferences.edit().putString(SERVER, server).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getNetworkPingTimeout(): Int {
|
||||||
|
val timeoutString = App.getInstance().preferences.getString(NETWORK_PING_TIMEOUT, "2") ?: "2"
|
||||||
|
return (timeoutString.toIntOrNull() ?: 2).coerceAtLeast(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setNetworkPingTimeout(pingTimeout: String?) {
|
||||||
|
App.getInstance().preferences.edit().putString(NETWORK_PING_TIMEOUT, pingTimeout).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getUser(): String? {
|
fun getUser(): String? {
|
||||||
return App.getInstance().preferences.getString(USER, null)
|
return App.getInstance().preferences.getString(USER, null)
|
||||||
@@ -289,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()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.media3.common.Metadata;
|
|||||||
import androidx.media3.common.Tracks;
|
import androidx.media3.common.Tracks;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.extractor.metadata.id3.InternalFrame;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.model.ReplayGain;
|
import com.cappielloantonio.tempo.model.ReplayGain;
|
||||||
|
|
||||||
@@ -82,26 +83,32 @@ public class ReplayGainUtil {
|
|||||||
private static ReplayGain setReplayGains(Metadata.Entry entry) {
|
private static ReplayGain setReplayGains(Metadata.Entry entry) {
|
||||||
ReplayGain replayGain = new ReplayGain();
|
ReplayGain replayGain = new ReplayGain();
|
||||||
|
|
||||||
if (entry.toString().contains(tags[0])) {
|
// The logic below assumes .toString() contains the dB value. That's not the case for InternalFrame
|
||||||
replayGain.setTrackGain(parseReplayGainTag(entry));
|
String str = entry.toString();
|
||||||
|
if (entry instanceof InternalFrame) {
|
||||||
|
str = ((InternalFrame) entry).description + ((InternalFrame) entry).text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.toString().contains(tags[1])) {
|
if (str.contains(tags[0])) {
|
||||||
replayGain.setAlbumGain(parseReplayGainTag(entry));
|
replayGain.setTrackGain(parseReplayGainTag(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.toString().contains(tags[2])) {
|
if (str.contains(tags[1])) {
|
||||||
replayGain.setTrackGain(parseReplayGainTag(entry) / 256f);
|
replayGain.setAlbumGain(parseReplayGainTag(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.toString().contains(tags[3])) {
|
if (str.contains(tags[2])) {
|
||||||
replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f);
|
replayGain.setTrackGain(parseReplayGainTag(str) / 256f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.contains(tags[3])) {
|
||||||
|
replayGain.setAlbumGain(parseReplayGainTag(str) / 256f);
|
||||||
}
|
}
|
||||||
|
|
||||||
return replayGain;
|
return replayGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Float parseReplayGainTag(Metadata.Entry entry) {
|
private static Float parseReplayGainTag(String entry) {
|
||||||
try {
|
try {
|
||||||
return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", ""));
|
return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", ""));
|
||||||
} catch (NumberFormatException exception) {
|
} catch (NumberFormatException exception) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.viewmodel;
|
|||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
@@ -21,8 +20,17 @@ import java.util.List;
|
|||||||
|
|
||||||
public class PlaylistChooserViewModel extends AndroidViewModel {
|
public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
private final PlaylistRepository playlistRepository;
|
private final PlaylistRepository playlistRepository;
|
||||||
|
|
||||||
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> playlistIsPublic = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
public Boolean getIsPlaylistPublic() {
|
||||||
|
return playlistIsPublic.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsPlaylistPublic(boolean isPublic) {
|
||||||
|
playlistIsPublic.setValue(isPublic);
|
||||||
|
}
|
||||||
|
|
||||||
private ArrayList<Child> toAdd = new ArrayList<>();
|
private ArrayList<Child> toAdd = new ArrayList<>();
|
||||||
|
|
||||||
public PlaylistChooserViewModel(@NonNull Application application) {
|
public PlaylistChooserViewModel(@NonNull Application application) {
|
||||||
@@ -39,7 +47,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
|||||||
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
||||||
List<String> songIds = Lists.transform(toAdd, Child::getId);
|
List<String> songIds = Lists.transform(toAdd, Child::getId);
|
||||||
if (Preferences.allowPlaylistDuplicates()) {
|
if (Preferences.allowPlaylistDuplicates()) {
|
||||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
} else {
|
} else {
|
||||||
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
|
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
|
||||||
@@ -47,7 +55,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
|||||||
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
||||||
songIds.removeAll(playlistSongIds);
|
songIds.removeAll(playlistSongIds);
|
||||||
}
|
}
|
||||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
14
app/src/main/res/drawable/ic_graphic_eq_land.xml
Normal file
14
app/src/main/res/drawable/ic_graphic_eq_land.xml
Normal 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>
|
||||||
14
app/src/main/res/drawable/ic_home_land.xml
Normal file
14
app/src/main/res/drawable/ic_home_land.xml
Normal 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>
|
||||||
14
app/src/main/res/drawable/ic_play_for_work_land.xml
Normal file
14
app/src/main/res/drawable/ic_play_for_work_land.xml
Normal 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>
|
||||||
68
app/src/main/res/layout-land/activity_main.xml
Normal file
68
app/src/main/res/layout-land/activity_main.xml
Normal 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>
|
||||||
305
app/src/main/res/layout-land/fragment_album_page.xml
Normal file
305
app/src/main/res/layout-land/fragment_album_page.xml
Normal 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>
|
||||||
218
app/src/main/res/layout-land/fragment_playlist_page.xml
Normal file
218
app/src/main/res/layout-land/fragment_playlist_page.xml
Normal 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>
|
||||||
@@ -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" />
|
||||||
|
|||||||
15
app/src/main/res/menu-land/bottom_nav_menu.xml
Normal file
15
app/src/main/res/menu-land/bottom_nav_menu.xml
Normal 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>
|
||||||
@@ -240,6 +240,15 @@
|
|||||||
<item>8</item>
|
<item>8</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="playlist_sort_option_titles">
|
||||||
|
<item>Por nombre</item>
|
||||||
|
<item>Aleatoriamente</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="playlist_sort_option_values">
|
||||||
|
<item>ORDER_BY_NAME</item>
|
||||||
|
<item>ORDER_BY_RANDOM</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="skip_min_star_rating_titles">
|
<string-array name="skip_min_star_rating_titles">
|
||||||
<item>0 estrellas como mínimo</item>
|
<item>0 estrellas como mínimo</item>
|
||||||
<item>1 estrella como mínimo</item>
|
<item>1 estrella como mínimo</item>
|
||||||
|
|||||||
@@ -227,6 +227,8 @@
|
|||||||
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</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_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>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Público</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privado</string>
|
||||||
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
|
||||||
<string name="playlist_duration">Duración • %1$s</string>
|
<string name="playlist_duration">Duración • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
|
||||||
@@ -333,6 +335,7 @@
|
|||||||
<string name="settings_music_directory_summary">Si se habilita, se mostrará la sección de carpetas de música. Tenga en cuenta que para que la navegación funcione correctamente, el servidor debe soportar esta característica.</string>
|
<string name="settings_music_directory_summary">Si se habilita, se mostrará la sección de carpetas de música. Tenga en cuenta que para que la navegación funcione correctamente, el servidor debe soportar esta característica.</string>
|
||||||
<string name="settings_podcast">Mostrar pódcasts</string>
|
<string name="settings_podcast">Mostrar pódcasts</string>
|
||||||
<string name="settings_podcast_summary">Si se habilita, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
|
<string name="settings_podcast_summary">Si se habilita, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
|
||||||
|
<string name="settings_playlist_sort">Ordenar listas de reproducción</string>
|
||||||
<string name="settings_audio_quality">Mostrar calidad de audio</string>
|
<string name="settings_audio_quality">Mostrar calidad de audio</string>
|
||||||
<string name="settings_audio_quality_summary">La tasa de bits y el formato de audio se mostrarán para cada pista de audio.</string>
|
<string name="settings_audio_quality_summary">La tasa de bits y el formato de audio se mostrarán para cada pista de audio.</string>
|
||||||
<string name="settings_song_rating_summary">Si se habilita, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
|
<string name="settings_song_rating_summary">Si se habilita, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
|
||||||
|
|||||||
@@ -236,6 +236,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Titre ajouté à la 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>
|
<string name="playlist_chooser_dialog_toast_add_failure">Échec d\'ajout du titre à la playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tous les titres ont été traités comme des doublons et ignorés</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Tous les titres ont été traités comme des doublons et ignorés</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Publique</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privé</string>
|
||||||
<string name="playlist_counted_tracks">%1$d titres • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d titres • %2$s</string>
|
||||||
<string name="playlist_duration">Durée • %1$s</string>
|
<string name="playlist_duration">Durée • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Appui long pour supprimer</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Appui long pour supprimer</string>
|
||||||
|
|||||||
@@ -229,6 +229,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla 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_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</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>
|
||||||
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
|
||||||
<string name="playlist_duration">Durata • %1$s</string>
|
<string name="playlist_duration">Durata • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
||||||
|
|||||||
@@ -228,6 +228,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Dodano piosenki 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_add_failure">Nie udało się dodać piosenek do playlisty</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Pominięto wszystkie piosenki jako duplikaty</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>
|
||||||
<string name="playlist_counted_tracks">%1$d utworów • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d utworów • %2$s</string>
|
||||||
<string name="playlist_duration">Długość • %1$s</string>
|
<string name="playlist_duration">Długość • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
|
||||||
@@ -343,6 +345,9 @@
|
|||||||
<string name="settings_image_size">Rozdzielczość obrazów</string>
|
<string name="settings_image_size">Rozdzielczość obrazów</string>
|
||||||
<string name="settings_language">Język</string>
|
<string name="settings_language">Język</string>
|
||||||
<string name="settings_logout_title">Wyloguj</string>
|
<string name="settings_logout_title">Wyloguj</string>
|
||||||
|
<string name="settings_ping_timeout_title">Timeout pingów serwera</string>
|
||||||
|
<string name="settings_ping_timeout_summary">Timeout lokalnego adresu URL. Domyślnie to 2 sekundy. (Serwer zdalny będzie używał trzykrotności tej wartości maksymalnie do 10 sekund.)</string>
|
||||||
|
<string name="settings_ping_timeout_dialog">Bazowy timeout w sekundach.</string>
|
||||||
<string name="settings_max_bitrate_download">Bitrate dla pobierania</string>
|
<string name="settings_max_bitrate_download">Bitrate dla pobierania</string>
|
||||||
<string name="settings_max_bitrate_mobile">Bitrate dla danych komórkowych</string>
|
<string name="settings_max_bitrate_mobile">Bitrate dla danych komórkowych</string>
|
||||||
<string name="settings_max_bitrate_wifi">Bitrate dla Wi-Fi</string>
|
<string name="settings_max_bitrate_wifi">Bitrate dla Wi-Fi</string>
|
||||||
|
|||||||
@@ -164,6 +164,8 @@
|
|||||||
<string name="playlist_chooser_dialog_title">Adicionar a uma playlist</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_success">Adicionada playlist de reprodução</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_public">Pública</string>
|
||||||
|
<string name="playlist_chooser_dialog_visibility_private">Privada</string>
|
||||||
<string name="playlist_counted_tracks">%1$d faixas • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d faixas • %2$s</string>
|
||||||
<string name="playlist_duration">Duração • %1$s</string>
|
<string name="playlist_duration">Duração • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_hint_name">Nome da playlist</string>
|
<string name="playlist_editor_dialog_hint_name">Nome da playlist</string>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -238,6 +240,8 @@
|
|||||||
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to 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_add_failure">Failed to add song(s) to playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</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_counted_tracks">%1$d tracks • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
||||||
<string name="playlist_duration">Duration • %1$s</string>
|
<string name="playlist_duration">Duration • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
||||||
@@ -343,7 +347,7 @@
|
|||||||
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
||||||
<string name="settings_github_summary">Follow the development</string>
|
<string name="settings_github_summary">Follow the development</string>
|
||||||
<string name="settings_github_title">Github</string>
|
<string name="settings_github_title">Github</string>
|
||||||
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</string>
|
<string name="settings_support_discussion_link" translatable="false">https://github.com/eddyizm/tempus/discussions</string>
|
||||||
<string name="settings_github_update">Updates</string>
|
<string name="settings_github_update">Updates</string>
|
||||||
<string name="settings_github_update_title">Check github for release updates</string>
|
<string name="settings_github_update_title">Check github for release updates</string>
|
||||||
<string name="settings_github_update_summary">If using the github version, by default app will check for new apk release. Toggle to disable automatic github checks</string>
|
<string name="settings_github_update_summary">If using the github version, by default app will check for new apk release. Toggle to disable automatic github checks</string>
|
||||||
@@ -353,6 +357,9 @@
|
|||||||
<string name="settings_image_size">Set image resolution</string>
|
<string name="settings_image_size">Set image resolution</string>
|
||||||
<string name="settings_language">Language</string>
|
<string name="settings_language">Language</string>
|
||||||
<string name="settings_logout_title">Log out</string>
|
<string name="settings_logout_title">Log out</string>
|
||||||
|
<string name="settings_ping_timeout_title">Server Ping Timeout</string>
|
||||||
|
<string name="settings_ping_timeout_summary">Set Local URL timeout. Default 2 seconds. (Remote server will use this value x3 up to 10 seconds max.)</string>
|
||||||
|
<string name="settings_ping_timeout_dialog">Set base timeout in seconds.</string>
|
||||||
<string name="settings_max_bitrate_download">Bitrate for downloads</string>
|
<string name="settings_max_bitrate_download">Bitrate for downloads</string>
|
||||||
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
|
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
|
||||||
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
|
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
|
||||||
@@ -398,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>
|
||||||
@@ -418,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>
|
||||||
@@ -484,7 +494,7 @@
|
|||||||
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
|
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
|
||||||
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
|
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
|
||||||
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
|
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
|
||||||
<string name="support_url">https://ko-fi.com/eddyizm</string>
|
<string name="support_url" translatable="false">https://ko-fi.com/eddyizm</string>
|
||||||
<string name="track_info_album">Album</string>
|
<string name="track_info_album">Album</string>
|
||||||
<string name="track_info_artist">Artist</string>
|
<string name="track_info_artist">Artist</string>
|
||||||
<string name="track_info_bit_depth">Bit depth</string>
|
<string name="track_info_bit_depth">Bit depth</string>
|
||||||
@@ -512,7 +522,7 @@
|
|||||||
<string name="track_info_year">Year</string>
|
<string name="track_info_year">Year</string>
|
||||||
<string name="undraw_page">unDraw</string>
|
<string name="undraw_page">unDraw</string>
|
||||||
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
||||||
<string name="undraw_url">https://undraw.co/</string>
|
<string name="undraw_url" translatable="false">https://undraw.co/</string>
|
||||||
<string name="widget_label">Tempus Widget</string>
|
<string name="widget_label">Tempus Widget</string>
|
||||||
<string name="widget_not_playing">Not playing</string>
|
<string name="widget_not_playing">Not playing</string>
|
||||||
<string name="widget_placeholder_subtitle">Open Tempus</string>
|
<string name="widget_placeholder_subtitle">Open Tempus</string>
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
android:key="scan_library"
|
android:key="scan_library"
|
||||||
android:title="@string/settings_scan_title" />
|
android:title="@string/settings_scan_title" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="network_ping_timeout_base"
|
||||||
|
android:title="@string/settings_ping_timeout_title"
|
||||||
|
app:summary="@string/settings_ping_timeout_summary"
|
||||||
|
android:dialogTitle="@string/settings_ping_timeout_dialog"
|
||||||
|
android:inputType="number"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:defaultValue="2" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:key="logout"
|
android:key="logout"
|
||||||
android:title="@string/settings_logout_title"/>
|
android:title="@string/settings_logout_title"/>
|
||||||
@@ -128,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">
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
chore: i18n: Add Romanian translation (including locale_config this time!)
|
||||||
|
chore: French localization update
|
||||||
|
chore(i18n): Update Spanish translation
|
||||||
|
docs: updated readme and added known issues for airsonic work around
|
||||||
|
fix: toast for made for you click indication
|
||||||
|
fix: sort playlist view
|
||||||
|
feat: sort preference for playlists
|
||||||
|
fix: use existing future when adding tracks, dialed random album tracks off in instant mix
|
||||||
|
chore(i18n): Update Polish translation
|
||||||
|
fix: Check for OpenSubsonic extensions also with password authentication, addressing lyric sync
|
||||||
|
feat: Implement duration and seeking for transcodes
|
||||||
|
feat: Playback speed controls for music
|
||||||
|
|||||||
3
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fix: Proper raw stream detection
|
||||||
|
chore(i18n): Update Spanish translation
|
||||||
|
feat: add configurable timeout
|
||||||
2
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fix: Avoid crash when server has no songs
|
||||||
|
fix: updated dialog import to address crashing on android 15
|
||||||
3
fastlane/metadata/android/en-US/changelogs/17.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/17.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fix: missing Replay Gain metadata from .m4a files
|
||||||
|
fix: Improve Synced Lyrics
|
||||||
|
fix: Add selector for playlist visibility
|
||||||
4
fastlane/metadata/android/en-US/changelogs/18.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/18.txt
Normal 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
|
||||||
Reference in New Issue
Block a user