Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37842fd897 | ||
|
|
a1397a224b | ||
|
|
804d6af6c3 | ||
|
|
e315169005 | ||
|
|
ea76afee09 | ||
|
|
45dda3af9b | ||
|
|
3d70b51244 | ||
|
|
22f196c8c0 | ||
|
|
540aa9ba73 | ||
|
|
1ff0b83a19 | ||
|
|
27f5a47cc9 | ||
|
|
732b6ad09d | ||
|
|
0df7346a14 | ||
|
|
786697109d | ||
|
|
1bfadb0669 | ||
|
|
79dc1cc93b | ||
|
|
38fb2c69f1 | ||
|
|
b34f827bc0 | ||
|
|
97d1b408e1 | ||
|
|
a5065578ca | ||
|
|
aac5c6067d | ||
|
|
cfd7cf314b | ||
|
|
c4b73f6014 | ||
|
|
35d377ce31 | ||
|
|
5e330ac451 | ||
|
|
8188ef169c | ||
|
|
3496918ce6 | ||
|
|
c72f368f6a | ||
|
|
eb089847e0 | ||
|
|
be33401b6f |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
## Pending release...
|
## Pending release...
|
||||||
|
|
||||||
|
* chore: bringing in media service refactor previously reverted after more testing by @eddyizm in https://github.com/eddyizm/tempus/pull/286
|
||||||
|
* fix: refactor start queue to put the db writing in the background by @eddyizm in https://github.com/eddyizm/tempus/pull/287
|
||||||
|
* Feat: playerqueue fab allows playqueue actions -> saving to playlist, download all, load queue, shuffle, clean queue by @eddyizm in https://github.com/eddyizm/tempus/pull/288
|
||||||
|
|
||||||
|
|
||||||
|
## [4.3.0](https://github.com/eddyizm/tempo/releases/tag/v4.3.0) (2025-11-23)
|
||||||
|
## What's Changed
|
||||||
|
* chore: Add Obtainium badge to README by @mikaeldui in https://github.com/eddyizm/tempus/pull/280
|
||||||
|
* fix: Revert "refactor MediaService" by @eddyizm in https://github.com/eddyizm/tempus/pull/282
|
||||||
|
* feat: add play functionality to library folder/index items by @antebudimir in https://github.com/eddyizm/tempus/pull/276
|
||||||
|
* fix: start queue blocking UI by @eddyizm in https://github.com/eddyizm/tempus/pull/283
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @mikaeldui made their first contribution in https://github.com/eddyizm/tempus/pull/280
|
||||||
|
* @antebudimir made their first contribution in https://github.com/eddyizm/tempus/pull/276
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.6...v4.3.0
|
||||||
|
|
||||||
## [4.2.6](https://github.com/eddyizm/tempo/releases/tag/v4.2.6) (2025-11-22)
|
## [4.2.6](https://github.com/eddyizm/tempo/releases/tag/v4.2.6) (2025-11-22)
|
||||||
## What's Changed
|
## What's Changed
|
||||||
* fix: Fix player queue soft-lock by @shrapnelnet in https://github.com/eddyizm/tempus/pull/266
|
* fix: Fix player queue soft-lock by @shrapnelnet in https://github.com/eddyizm/tempus/pull/266
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
|
<a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
|
||||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
|
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
|
||||||
|
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.eddyizm.tempus%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Feddyizm%2Ftempus%22%2C%22author%22%3A%22eddyizm%22%2C%22name%22%3A%22Tempus%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22tempus%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22overrideSource%22%3A%22GitHub%22%7D"><img width="200" src="https://github.com/user-attachments/assets/119e7ff4-2636-43cb-ab7f-1b6a58ac3570" /></a>
|
||||||
</p>
|
</p>
|
||||||
<!--
|
<!--
|
||||||
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
|
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
|
||||||
@@ -123,4 +124,4 @@ Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel fr
|
|||||||
## Credits
|
## Credits
|
||||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
|
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
|
||||||
|
|
||||||
[Opensvg.org](https://opensvg.org) for the new turntable logo.
|
[Opensvg.org](https://opensvg.org) for the new turntable logo.
|
||||||
|
|||||||
15
USAGE.md
15
USAGE.md
@@ -69,6 +69,21 @@ However, if you want to limit or change libraries you could use a workaround, if
|
|||||||
|
|
||||||
You can create multiple users , one for each library, and save each of them in Tempus app.
|
You can create multiple users , one for each library, and save each of them in Tempus app.
|
||||||
|
|
||||||
|
### Folder or index playback
|
||||||
|
|
||||||
|
If your Subsonic-compatible server exposes the folder tree **or** provides an artist index (for example Gonic, Navidrome, or any backend with folder browsing enabled), Tempus lets you play an entire folder from anywhere in the library hierarchy:
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/music_folders_root.png" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/music_folders_playback.png" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
- The **Library ▸ Music folders** screen shows each top-level folder with a play icon only after you drill into it. The root entry remains a simple navigator.
|
||||||
|
- When viewing **inner folders** **or artist index entries**, tap the new play button to immediately enqueue every audio track inside that folder/index and all nested subfolders.
|
||||||
|
- Video files are excluded automatically, so only playable audio ends up in the queue.
|
||||||
|
|
||||||
|
No extra config is needed—Tempus adjusts based on the connected backend.
|
||||||
|
|
||||||
### Now Playing Screen
|
### Now Playing Screen
|
||||||
|
|
||||||
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
|
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 7
|
versionCode 9
|
||||||
versionName '4.2.6'
|
versionName '4.4.0'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ public interface ClickCallback {
|
|||||||
default void onInternetRadioStationClick(Bundle bundle) {}
|
default void onInternetRadioStationClick(Bundle bundle) {}
|
||||||
default void onInternetRadioStationLongClick(Bundle bundle) {}
|
default void onInternetRadioStationLongClick(Bundle bundle) {}
|
||||||
default void onMusicFolderClick(Bundle bundle) {}
|
default void onMusicFolderClick(Bundle bundle) {}
|
||||||
|
default void onMusicFolderPlay(Bundle bundle) {}
|
||||||
default void onMusicDirectoryClick(Bundle bundle) {}
|
default void onMusicDirectoryClick(Bundle bundle) {}
|
||||||
|
default void onMusicDirectoryPlay(Bundle bundle) {}
|
||||||
default void onMusicIndexClick(Bundle bundle) {}
|
default void onMusicIndexClick(Bundle bundle) {}
|
||||||
|
default void onMusicIndexPlay(Bundle bundle) {}
|
||||||
default void onDownloadGroupLongClick(Bundle bundle) {}
|
default void onDownloadGroupLongClick(Bundle bundle) {}
|
||||||
default void onShareClick(Bundle bundle) {}
|
default void onShareClick(Bundle bundle) {}
|
||||||
default void onShareLongClick(Bundle bundle) {}
|
default void onShareLongClick(Bundle bundle) {}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||||
@@ -52,6 +55,8 @@ public class QueueRepository {
|
|||||||
public MutableLiveData<PlayQueue> getPlayQueue() {
|
public MutableLiveData<PlayQueue> getPlayQueue() {
|
||||||
MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>();
|
MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>();
|
||||||
|
|
||||||
|
Log.d(TAG, "Getting play queue from server...");
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getBookmarksClient()
|
.getBookmarksClient()
|
||||||
.getPlayQueue()
|
.getPlayQueue()
|
||||||
@@ -59,12 +64,19 @@ public class QueueRepository {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) {
|
||||||
playQueue.setValue(response.body().getSubsonicResponse().getPlayQueue());
|
PlayQueue serverQueue = response.body().getSubsonicResponse().getPlayQueue();
|
||||||
|
Log.d(TAG, "Server returned play queue with " +
|
||||||
|
(serverQueue.getEntries() != null ? serverQueue.getEntries().size() : 0) + " items");
|
||||||
|
playQueue.setValue(serverQueue);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Server returned no play queue");
|
||||||
|
playQueue.setValue(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
Log.e(TAG, "Failed to get play queue", t);
|
||||||
playQueue.setValue(null);
|
playQueue.setValue(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -73,18 +85,24 @@ public class QueueRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void savePlayQueue(List<String> ids, String current, long position) {
|
public void savePlayQueue(List<String> ids, String current, long position) {
|
||||||
|
Log.d(TAG, "Saving play queue to server - Items: " + ids.size() + ", Current: " + current);
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getBookmarksClient()
|
.getBookmarksClient()
|
||||||
.savePlayQueue(ids, current, position)
|
.savePlayQueue(ids, current, position)
|
||||||
.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) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
Log.d(TAG, "Play queue saved successfully");
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Play queue save failed with code: " + response.code());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
Log.e(TAG, "Play queue save failed", t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -123,10 +141,9 @@ public class QueueRepository {
|
|||||||
|
|
||||||
private boolean isMediaInQueue(List<Queue> queue, Child media) {
|
private boolean isMediaInQueue(List<Queue> queue, Child media) {
|
||||||
if (queue == null || media == null) return false;
|
if (queue == null || media == null) return false;
|
||||||
|
return queue.stream().anyMatch(queueItem ->
|
||||||
return queue.stream().anyMatch(queueItem ->
|
queueItem != null && media.getId() != null &&
|
||||||
queueItem != null && media.getId() != null &&
|
queueItem.getId().equals(media.getId())
|
||||||
queueItem.getId().equals(media.getId())
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +163,8 @@ public class QueueRepository {
|
|||||||
List<Child> filteredToAdd = toAdd;
|
List<Child> filteredToAdd = toAdd;
|
||||||
final List<Queue> finalMedia = media;
|
final List<Queue> finalMedia = media;
|
||||||
filteredToAdd = toAdd.stream()
|
filteredToAdd = toAdd.stream()
|
||||||
.filter(child -> !isMediaInQueue(finalMedia, child))
|
.filter(child -> !isMediaInQueue(finalMedia, child))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
for (int i = 0; i < filteredToAdd.size(); i++) {
|
for (int i = 0; i < filteredToAdd.size(); i++) {
|
||||||
Queue queueItem = new Queue(filteredToAdd.get(i));
|
Queue queueItem = new Queue(filteredToAdd.get(i));
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package com.cappielloantonio.tempo.service;
|
package com.cappielloantonio.tempo.service;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
@@ -36,6 +37,8 @@ import com.google.common.util.concurrent.MoreExecutors;
|
|||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class MediaManager {
|
public class MediaManager {
|
||||||
@@ -43,6 +46,8 @@ public class MediaManager {
|
|||||||
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
||||||
public static AtomicBoolean justStarted = new AtomicBoolean(false);
|
public static AtomicBoolean justStarted = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private static final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
public static void registerPlaybackObserver(
|
public static void registerPlaybackObserver(
|
||||||
ListenableFuture<MediaBrowser> browserFuture,
|
ListenableFuture<MediaBrowser> browserFuture,
|
||||||
PlaybackViewModel playbackViewModel
|
PlaybackViewModel playbackViewModel
|
||||||
@@ -175,36 +180,44 @@ public class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
try {
|
try {
|
||||||
if (mediaBrowserListenableFuture.isDone()) {
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
justStarted.set(true);
|
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
||||||
browser.setMediaItems(MappingUtil.mapMediaItems(media), startIndex, 0);
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
browser.prepare();
|
justStarted.set(true);
|
||||||
|
browser.setMediaItems(items, startIndex, 0);
|
||||||
|
browser.prepare();
|
||||||
|
|
||||||
Player.Listener timelineListener = new Player.Listener() {
|
Player.Listener timelineListener = new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
int itemCount = browser.getMediaItemCount();
|
int itemCount = browser.getMediaItemCount();
|
||||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||||
browser.seekTo(startIndex, 0);
|
browser.seekTo(startIndex, 0);
|
||||||
browser.play();
|
browser.play();
|
||||||
browser.removeListener(this);
|
browser.removeListener(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
browser.addListener(timelineListener);
|
||||||
browser.addListener(timelineListener);
|
});
|
||||||
|
|
||||||
enqueueDatabase(media, true, 0);
|
backgroundExecutor.execute(() -> {
|
||||||
|
enqueueDatabase(media, true, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
e.printStackTrace();
|
Log.e(TAG, "Error executing startQueue logic: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
|||||||
.into(holder.item.musicDirectoryCoverImageView);
|
.into(holder.item.musicDirectoryCoverImageView);
|
||||||
|
|
||||||
holder.item.musicDirectoryMoreButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
|
holder.item.musicDirectoryMoreButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
|
||||||
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.INVISIBLE : View.VISIBLE);
|
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,6 +80,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
|||||||
itemView.setOnLongClickListener(v -> onLongClick());
|
itemView.setOnLongClickListener(v -> onLongClick());
|
||||||
|
|
||||||
item.musicDirectoryMoreButton.setOnClickListener(v -> onClick());
|
item.musicDirectoryMoreButton.setOnClickListener(v -> onClick());
|
||||||
|
item.musicDirectoryPlayButton.setOnClickListener(v -> onPlayClick());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
@@ -107,5 +108,13 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onPlayClick() {
|
||||||
|
if (children.get(getBindingAdapterPosition()).isDir()) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString(Constants.MUSIC_DIRECTORY_ID, children.get(getBindingAdapterPosition()).getId());
|
||||||
|
click.onMusicDirectoryPlay(bundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
|
|||||||
|
|
||||||
itemView.setOnClickListener(v -> onClick());
|
itemView.setOnClickListener(v -> onClick());
|
||||||
item.musicIndexMoreButton.setOnClickListener(v -> onClick());
|
item.musicIndexMoreButton.setOnClickListener(v -> onClick());
|
||||||
|
item.musicIndexPlayButton.setOnClickListener(v -> onPlayClick());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
@@ -83,5 +84,11 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
|
|||||||
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
|
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
|
||||||
click.onMusicIndexClick(bundle);
|
click.onMusicIndexClick(bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onPlayClick() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
|
||||||
|
click.onMusicIndexPlay(bundle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding;
|
|||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||||
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
|
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
|
||||||
|
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
@@ -94,6 +96,20 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(holder.itemView.getContext());
|
||||||
|
|
||||||
|
if (downloaderManager != null) {
|
||||||
|
boolean isDownloaded = downloaderManager.isDownloaded(song.getId());
|
||||||
|
|
||||||
|
if (isDownloaded) {
|
||||||
|
holder.item.downloadIndicatorIcon.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.item.downloadIndicatorIcon.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.item.downloadIndicatorIcon.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
if (Preferences.showItemRating()) {
|
if (Preferences.showItemRating()) {
|
||||||
if (song.getStarred() == null && song.getUserRating() == null) {
|
if (song.getStarred() == null && song.getUserRating() == null) {
|
||||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
|||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.repository.DirectoryRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Directory;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||||
@@ -53,6 +59,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
|||||||
private MusicDirectoryAdapter musicDirectoryAdapter;
|
private MusicDirectoryAdapter musicDirectoryAdapter;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private DirectoryRepository directoryRepository;
|
||||||
|
|
||||||
private MenuItem menuItem;
|
private MenuItem menuItem;
|
||||||
|
|
||||||
@@ -77,6 +84,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentDirectoryBinding.inflate(inflater, container, false);
|
bind = FragmentDirectoryBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
|
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
|
||||||
|
directoryRepository = new DirectoryRepository();
|
||||||
|
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initDirectoryListView();
|
initDirectoryListView();
|
||||||
@@ -197,4 +205,57 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMusicDirectoryClick(Bundle bundle) {
|
public void onMusicDirectoryClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMusicDirectoryPlay(Bundle bundle) {
|
||||||
|
String directoryId = bundle.getString(Constants.MUSIC_DIRECTORY_ID);
|
||||||
|
if (directoryId != null) {
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.folder_play_collecting), Toast.LENGTH_SHORT).show();
|
||||||
|
collectAndPlayDirectorySongs(directoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectAndPlayDirectorySongs(String directoryId) {
|
||||||
|
List<Child> allSongs = new ArrayList<>();
|
||||||
|
AtomicInteger pendingRequests = new AtomicInteger(0);
|
||||||
|
|
||||||
|
collectSongsFromDirectory(directoryId, allSongs, pendingRequests, () -> {
|
||||||
|
if (!allSongs.isEmpty()) {
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, allSongs, 0);
|
||||||
|
activity.setBottomSheetInPeek(true);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.folder_play_playing, allSongs.size()), Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.folder_play_no_songs), Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectSongsFromDirectory(String directoryId, List<Child> allSongs, AtomicInteger pendingRequests, Runnable onComplete) {
|
||||||
|
pendingRequests.incrementAndGet();
|
||||||
|
|
||||||
|
directoryRepository.getMusicDirectory(directoryId).observe(getViewLifecycleOwner(), directory -> {
|
||||||
|
if (directory != null && directory.getChildren() != null) {
|
||||||
|
for (Child child : directory.getChildren()) {
|
||||||
|
if (child.isDir()) {
|
||||||
|
// It's a subdirectory, recurse into it
|
||||||
|
collectSongsFromDirectory(child.getId(), allSongs, pendingRequests, onComplete);
|
||||||
|
} else if (!child.isVideo()) {
|
||||||
|
// It's a song, add it to the list
|
||||||
|
synchronized (allSongs) {
|
||||||
|
allSongs.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement pending requests and check if we're done
|
||||||
|
if (pendingRequests.decrementAndGet() == 0) {
|
||||||
|
onComplete.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,40 @@
|
|||||||
package com.cappielloantonio.tempo.ui.fragment;
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.FragmentIndexBinding;
|
import com.cappielloantonio.tempo.databinding.FragmentIndexBinding;
|
||||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||||
|
import com.cappielloantonio.tempo.repository.DirectoryRepository;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.MusicIndexAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.MusicIndexAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.IndexUtil;
|
import com.cappielloantonio.tempo.util.IndexUtil;
|
||||||
import com.cappielloantonio.tempo.viewmodel.IndexViewModel;
|
import com.cappielloantonio.tempo.viewmodel.IndexViewModel;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class IndexFragment extends Fragment implements ClickCallback {
|
public class IndexFragment extends Fragment implements ClickCallback {
|
||||||
@@ -32,6 +45,8 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
|||||||
private IndexViewModel indexViewModel;
|
private IndexViewModel indexViewModel;
|
||||||
|
|
||||||
private MusicIndexAdapter musicIndexAdapter;
|
private MusicIndexAdapter musicIndexAdapter;
|
||||||
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private DirectoryRepository directoryRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
@@ -40,6 +55,7 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentIndexBinding.inflate(inflater, container, false);
|
bind = FragmentIndexBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
indexViewModel = new ViewModelProvider(requireActivity()).get(IndexViewModel.class);
|
indexViewModel = new ViewModelProvider(requireActivity()).get(IndexViewModel.class);
|
||||||
|
directoryRepository = new DirectoryRepository();
|
||||||
|
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initDirectoryListView();
|
initDirectoryListView();
|
||||||
@@ -48,6 +64,18 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
initializeMediaBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
releaseMediaBrowser();
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -107,4 +135,65 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMusicIndexClick(Bundle bundle) {
|
public void onMusicIndexClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMusicIndexPlay(Bundle bundle) {
|
||||||
|
String directoryId = bundle.getString(Constants.MUSIC_DIRECTORY_ID);
|
||||||
|
if (directoryId != null) {
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.folder_play_collecting), Toast.LENGTH_SHORT).show();
|
||||||
|
collectAndPlayDirectorySongs(directoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeMediaBrowser() {
|
||||||
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseMediaBrowser() {
|
||||||
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectAndPlayDirectorySongs(String directoryId) {
|
||||||
|
List<Child> allSongs = new ArrayList<>();
|
||||||
|
AtomicInteger pendingRequests = new AtomicInteger(0);
|
||||||
|
|
||||||
|
collectSongsFromDirectory(directoryId, allSongs, pendingRequests, () -> {
|
||||||
|
if (!allSongs.isEmpty()) {
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, allSongs, 0);
|
||||||
|
activity.setBottomSheetInPeek(true);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.folder_play_playing, allSongs.size()), Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.folder_play_no_songs), Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectSongsFromDirectory(String directoryId, List<Child> allSongs, AtomicInteger pendingRequests, Runnable onComplete) {
|
||||||
|
pendingRequests.incrementAndGet();
|
||||||
|
|
||||||
|
directoryRepository.getMusicDirectory(directoryId).observe(getViewLifecycleOwner(), directory -> {
|
||||||
|
if (directory != null && directory.getChildren() != null) {
|
||||||
|
for (Child child : directory.getChildren()) {
|
||||||
|
if (child.isDir()) {
|
||||||
|
// It's a subdirectory, recurse into it
|
||||||
|
collectSongsFromDirectory(child.getId(), allSongs, pendingRequests, onComplete);
|
||||||
|
} else if (!child.isVideo()) {
|
||||||
|
// It's a song, add it to the list
|
||||||
|
synchronized (allSongs) {
|
||||||
|
allSongs.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement pending requests and check if we're done
|
||||||
|
if (pendingRequests.decrementAndGet() == 0) {
|
||||||
|
onComplete.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,11 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
@@ -31,6 +35,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.LibraryViewModel;
|
import com.cappielloantonio.tempo.viewmodel.LibraryViewModel;
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -49,6 +55,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
|
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
|
||||||
|
|
||||||
private MaterialToolbar materialToolbar;
|
private MaterialToolbar materialToolbar;
|
||||||
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
@@ -79,6 +86,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
initializeMediaBrowser();
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.setBottomNavigationBarVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,4 +300,8 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMusicFolderClick(Bundle bundle) {
|
public void onMusicFolderClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeMediaBrowser() {
|
||||||
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.session.SessionToken;
|
import androidx.media3.session.SessionToken;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -19,11 +23,17 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding;
|
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding;
|
||||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||||
|
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
@@ -31,6 +41,7 @@ import com.google.common.util.concurrent.MoreExecutors;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -39,6 +50,18 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private InnerFragmentPlayerQueueBinding bind;
|
private InnerFragmentPlayerQueueBinding bind;
|
||||||
|
|
||||||
|
private com.google.android.material.floatingactionbutton.FloatingActionButton fabMenuToggle;
|
||||||
|
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabClearQueue;
|
||||||
|
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabShuffleQueue;
|
||||||
|
|
||||||
|
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabSaveToPlaylist;
|
||||||
|
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabDownloadAll;
|
||||||
|
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabLoadQueue;
|
||||||
|
|
||||||
|
private boolean isMenuOpen = false;
|
||||||
|
private final int ANIMATION_DURATION = 250;
|
||||||
|
private final float FAB_VERTICAL_SPACING_DP = 70f;
|
||||||
|
|
||||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||||
private PlaybackViewModel playbackViewModel;
|
private PlaybackViewModel playbackViewModel;
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
@@ -53,6 +76,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
|
fabMenuToggle = bind.fabMenuToggle;
|
||||||
|
fabClearQueue = bind.fabClearQueue;
|
||||||
|
fabShuffleQueue = bind.fabShuffleQueue;
|
||||||
|
|
||||||
|
fabSaveToPlaylist = bind.fabSaveToPlaylist;
|
||||||
|
fabDownloadAll = bind.fabDownloadAll;
|
||||||
|
fabLoadQueue = bind.fabLoadQueue;
|
||||||
|
|
||||||
|
fabMenuToggle.setOnClickListener(v -> toggleFabMenu());
|
||||||
|
fabClearQueue.setOnClickListener(v -> handleClearQueueClick());
|
||||||
|
fabShuffleQueue.setOnClickListener(v -> handleShuffleQueueClick());
|
||||||
|
|
||||||
|
fabSaveToPlaylist.setOnClickListener(v -> handleSaveToPlaylistClick());
|
||||||
|
fabDownloadAll.setOnClickListener(v -> handleDownloadAllClick());
|
||||||
|
fabLoadQueue.setOnClickListener(v -> handleLoadQueueClick());
|
||||||
|
|
||||||
|
// Hide Load Queue FAB if sync is disabled
|
||||||
|
if (!Preferences.isSyncronizationEnabled()) {
|
||||||
|
fabLoadQueue.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
initQueueRecyclerView();
|
initQueueRecyclerView();
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
@@ -62,8 +106,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
initializeBrowser();
|
initializeBrowser();
|
||||||
bindMediaController();
|
|
||||||
|
|
||||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
observePlayback();
|
observePlayback();
|
||||||
}
|
}
|
||||||
@@ -105,18 +147,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindMediaController() {
|
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
|
||||||
try {
|
|
||||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
|
||||||
initShuffleButton(mediaBrowser);
|
|
||||||
initCleanButton(mediaBrowser);
|
|
||||||
} catch (Exception exception) {
|
|
||||||
exception.printStackTrace();
|
|
||||||
}
|
|
||||||
}, MoreExecutors.directExecutor());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setMediaBrowserListenableFuture() {
|
private void setMediaBrowserListenableFuture() {
|
||||||
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
@@ -149,18 +179,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
fromPosition = viewHolder.getBindingAdapterPosition();
|
fromPosition = viewHolder.getBindingAdapterPosition();
|
||||||
toPosition = target.getBindingAdapterPosition();
|
toPosition = target.getBindingAdapterPosition();
|
||||||
|
|
||||||
/*
|
|
||||||
* Per spostare un elemento nella coda devo:
|
|
||||||
* - Spostare graficamente la traccia da una posizione all'altra con Collections.swap()
|
|
||||||
* - Spostare nel db la traccia, tramite QueueRepository
|
|
||||||
* - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong()
|
|
||||||
*
|
|
||||||
* In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento
|
|
||||||
* In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db
|
|
||||||
* Al rilascio dell'elemento chiamo il metodo clearView()
|
|
||||||
*/
|
|
||||||
|
|
||||||
Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition);
|
Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition);
|
||||||
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
|
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
|
||||||
|
|
||||||
@@ -188,46 +206,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
}).attachToRecyclerView(bind.playerQueueRecyclerView);
|
}).attachToRecyclerView(bind.playerQueueRecyclerView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initShuffleButton(MediaBrowser mediaBrowser) {
|
|
||||||
bind.playerShuffleQueueFab.setOnClickListener(view -> {
|
|
||||||
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
|
||||||
int endPosition = playerSongQueueAdapter.getItems().size() - 1;
|
|
||||||
|
|
||||||
if (startPosition < endPosition) {
|
|
||||||
ArrayList<Integer> pool = new ArrayList<>();
|
|
||||||
|
|
||||||
for (int i = startPosition; i <= endPosition; i++) {
|
|
||||||
pool.add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (pool.size() >= 2) {
|
|
||||||
int fromPosition = (int) (Math.random() * (pool.size()));
|
|
||||||
int positionA = pool.get(fromPosition);
|
|
||||||
pool.remove(fromPosition);
|
|
||||||
|
|
||||||
int toPosition = (int) (Math.random() * (pool.size()));
|
|
||||||
int positionB = pool.get(toPosition);
|
|
||||||
pool.remove(toPosition);
|
|
||||||
|
|
||||||
Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB);
|
|
||||||
bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB);
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initCleanButton(MediaBrowser mediaBrowser) {
|
|
||||||
bind.playerCleanQueueButton.setOnClickListener(view -> {
|
|
||||||
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
|
||||||
int endPosition = playerSongQueueAdapter.getItems().size();
|
|
||||||
|
|
||||||
MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
|
||||||
bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNowPlayingItem() {
|
private void updateNowPlayingItem() {
|
||||||
playerSongQueueAdapter.notifyDataSetChanged();
|
playerSongQueueAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -259,4 +237,216 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility and animates all six secondary FABs.
|
||||||
|
*/
|
||||||
|
private void toggleFabMenu() {
|
||||||
|
if (isMenuOpen) {
|
||||||
|
// CLOSE MENU (Reverse order for visual effect)
|
||||||
|
if (Preferences.isSyncronizationEnabled()) {
|
||||||
|
closeFab(fabLoadQueue, 4);
|
||||||
|
}
|
||||||
|
closeFab(fabSaveToPlaylist, 3);
|
||||||
|
closeFab(fabClearQueue, 2);
|
||||||
|
closeFab(fabDownloadAll, 1);
|
||||||
|
closeFab(fabShuffleQueue, 0);
|
||||||
|
|
||||||
|
fabMenuToggle.animate().rotation(0f).setDuration(ANIMATION_DURATION).start();
|
||||||
|
} else {
|
||||||
|
// OPEN MENU (lowest index at bottom)
|
||||||
|
openFab(fabShuffleQueue, 0);
|
||||||
|
openFab(fabDownloadAll, 1);
|
||||||
|
openFab(fabClearQueue, 2);
|
||||||
|
openFab(fabSaveToPlaylist, 3);
|
||||||
|
if (Preferences.isSyncronizationEnabled()) {
|
||||||
|
openFab(fabLoadQueue, 4);
|
||||||
|
}
|
||||||
|
fabMenuToggle.animate().rotation(45f).setDuration(ANIMATION_DURATION).start();
|
||||||
|
}
|
||||||
|
isMenuOpen = !isMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openFab(View fab, int index) {
|
||||||
|
final float displacement = getResources().getDisplayMetrics().density * (FAB_VERTICAL_SPACING_DP * (index + 1));
|
||||||
|
|
||||||
|
fab.setVisibility(View.VISIBLE);
|
||||||
|
fab.setAlpha(0f);
|
||||||
|
fab.setTranslationY(displacement); // Start at the hidden (closed) position
|
||||||
|
|
||||||
|
fab.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeFab(View fab, int index) {
|
||||||
|
final float displacement = getResources().getDisplayMetrics().density * (FAB_VERTICAL_SPACING_DP * (index + 1));
|
||||||
|
|
||||||
|
fab.animate()
|
||||||
|
.translationY(displacement)
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.withEndAction(() -> fab.setVisibility(View.GONE))
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleShuffleQueueClick() {
|
||||||
|
Log.d(TAG, "Shuffle Queue Clicked!");
|
||||||
|
|
||||||
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
try {
|
||||||
|
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||||
|
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
||||||
|
int endPosition = playerSongQueueAdapter.getItems().size() - 1;
|
||||||
|
|
||||||
|
if (startPosition < endPosition) {
|
||||||
|
ArrayList<Integer> pool = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = startPosition; i <= endPosition; i++) {
|
||||||
|
pool.add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pool.size() >= 2) {
|
||||||
|
int fromPosition = (int) (Math.random() * (pool.size()));
|
||||||
|
int positionA = pool.get(fromPosition);
|
||||||
|
pool.remove(fromPosition);
|
||||||
|
|
||||||
|
int toPosition = (int) (Math.random() * (pool.size()));
|
||||||
|
int positionB = pool.get(toPosition);
|
||||||
|
pool.remove(toPosition);
|
||||||
|
|
||||||
|
Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB);
|
||||||
|
bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB);
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error shuffling queue", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFabMenu();
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClearQueueClick() {
|
||||||
|
Log.d(TAG, "Clear Queue Clicked!");
|
||||||
|
|
||||||
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
try {
|
||||||
|
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||||
|
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
||||||
|
int endPosition = playerSongQueueAdapter.getItems().size();
|
||||||
|
|
||||||
|
MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
||||||
|
bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition - startPosition);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error clearing queue", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFabMenu();
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSaveToPlaylistClick() {
|
||||||
|
Log.d(TAG, "Save to Playlist Clicked!");
|
||||||
|
|
||||||
|
List<Child> queueSongs = playerSongQueueAdapter.getItems();
|
||||||
|
|
||||||
|
if (queueSongs == null || queueSongs.isEmpty()) {
|
||||||
|
Toast.makeText(requireContext(), "Queue is empty", Toast.LENGTH_SHORT).show();
|
||||||
|
toggleFabMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(queueSongs));
|
||||||
|
|
||||||
|
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||||
|
dialog.setArguments(bundle);
|
||||||
|
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||||
|
|
||||||
|
toggleFabMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDownloadAllClick() {
|
||||||
|
Log.d(TAG, "Download All Clicked!");
|
||||||
|
|
||||||
|
List<Child> queueSongs = playerSongQueueAdapter.getItems();
|
||||||
|
|
||||||
|
if (queueSongs == null || queueSongs.isEmpty()) {
|
||||||
|
Toast.makeText(requireContext(), "Queue is empty", Toast.LENGTH_SHORT).show();
|
||||||
|
toggleFabMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MediaItem> mediaItemsToDownload = MappingUtil.mapMediaItems(queueSongs);
|
||||||
|
|
||||||
|
List<com.cappielloantonio.tempo.model.Download> downloadModels = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Child child : queueSongs) {
|
||||||
|
com.cappielloantonio.tempo.model.Download downloadModel =
|
||||||
|
new com.cappielloantonio.tempo.model.Download(child);
|
||||||
|
downloadModel.setArtist(child.getArtist());
|
||||||
|
downloadModel.setAlbum(child.getAlbum());
|
||||||
|
downloadModel.setCoverArtId(child.getCoverArtId());
|
||||||
|
downloadModels.add(downloadModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
|
||||||
|
if (downloaderManager != null) {
|
||||||
|
downloaderManager.download(mediaItemsToDownload, downloadModels);
|
||||||
|
Toast.makeText(requireContext(), "Starting download of " + queueSongs.size() + " songs in the background.", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "DownloaderManager not initialized. Check DownloadUtil.");
|
||||||
|
Toast.makeText(requireContext(), "Download service unavailable.", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
toggleFabMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLoadQueueClick() {
|
||||||
|
Log.d(TAG, "Load Queue Clicked!");
|
||||||
|
if (!Preferences.isSyncronizationEnabled()) {
|
||||||
|
toggleFabMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerBottomSheetViewModel playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||||
|
|
||||||
|
playerBottomSheetViewModel.getPlayQueue().observe(getViewLifecycleOwner(), new Observer<PlayQueue>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(PlayQueue playQueue) {
|
||||||
|
playerBottomSheetViewModel.getPlayQueue().removeObserver(this);
|
||||||
|
|
||||||
|
if (playQueue != null && playQueue.getEntries() != null && !playQueue.getEntries().isEmpty()) {
|
||||||
|
int currentIndex = 0;
|
||||||
|
for (int i = 0; i < playQueue.getEntries().size(); i++) {
|
||||||
|
if (playQueue.getEntries().get(i).getId().equals(playQueue.getCurrent())) {
|
||||||
|
currentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, playQueue.getEntries(), currentIndex);
|
||||||
|
|
||||||
|
Toast.makeText(requireContext(), "Queue loaded", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "No saved queue found", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFabMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Handler().postDelayed(() -> {
|
||||||
|
if (isMenuOpen) {
|
||||||
|
toggleFabMenu();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
@@ -21,7 +20,6 @@ import java.util.List;
|
|||||||
|
|
||||||
public class AlbumListPageViewModel extends AndroidViewModel {
|
public class AlbumListPageViewModel extends AndroidViewModel {
|
||||||
private final AlbumRepository albumRepository;
|
private final AlbumRepository albumRepository;
|
||||||
private final DownloadRepository downloadRepository;
|
|
||||||
|
|
||||||
public String title;
|
public String title;
|
||||||
public ArtistID3 artist;
|
public ArtistID3 artist;
|
||||||
@@ -32,9 +30,7 @@ public class AlbumListPageViewModel extends AndroidViewModel {
|
|||||||
|
|
||||||
public AlbumListPageViewModel(@NonNull Application application) {
|
public AlbumListPageViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
||||||
albumRepository = new AlbumRepository();
|
albumRepository = new AlbumRepository();
|
||||||
downloadRepository = new DownloadRepository();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<AlbumID3>> getAlbumList(LifecycleOwner owner) {
|
public LiveData<List<AlbumID3>> getAlbumList(LifecycleOwner owner) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.viewmodel;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
@@ -12,6 +13,7 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
@@ -291,13 +293,13 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
List<String> ids = queue.stream().map(Child::getId).collect(Collectors.toList());
|
List<String> ids = queue.stream().map(Child::getId).collect(Collectors.toList());
|
||||||
|
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
queueRepository.savePlayQueue(ids, media.getId(), 0);
|
// TODO: We need to get the actual playback position here
|
||||||
|
Log.d(TAG, "Saving play queue - Current: " + media.getId() + ", Items: " + ids.size());
|
||||||
|
queueRepository.savePlayQueue(ids, media.getId(), 0); // Still hardcoded to 0 for now
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
||||||
if (TextUtils.isEmpty(songId)) {
|
if (TextUtils.isEmpty(songId)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/player_clean_queue_button"
|
|
||||||
style="@style/TitleMedium"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="@string/player_queue_clean_all_button" />
|
|
||||||
|
|
||||||
<com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost
|
<com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
@@ -21,20 +14,74 @@
|
|||||||
android:id="@+id/player_queue_recycler_view"
|
android:id="@+id/player_queue_recycler_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="40dp"
|
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
</com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost>
|
</com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<LinearLayout
|
||||||
android:id="@+id/player_shuffle_queue_fab"
|
android:id="@+id/fab_menu_container"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="end"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
android:contentDescription="@string/content_description_shuffle_button"
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
|
||||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
|
||||||
app:srcCompat="@drawable/ic_shuffle" />
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/fab_save_to_playlist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/player_queue_save_to_playlist"
|
||||||
|
app:icon="@android:drawable/ic_menu_edit" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/fab_clear_queue"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/player_queue_clean_all_button"
|
||||||
|
app:icon="@android:drawable/ic_menu_delete" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/fab_download_all"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/menu_download_all_button"
|
||||||
|
app:icon="@android:drawable/stat_sys_download_done" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/fab_load_queue"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/player_queue_load_queue"
|
||||||
|
app:icon="@android:drawable/ic_menu_revert" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/fab_shuffle_queue"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/content_description_shuffle_button"
|
||||||
|
app:icon="@drawable/ic_shuffle" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab_menu_toggle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="Toggle FAB Action menu"
|
||||||
|
tools:ignore="HardcodedText"
|
||||||
|
app:srcCompat="@drawable/ic_add" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -19,12 +19,15 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<View
|
<ImageView
|
||||||
android:id="@+id/cover_image_separator"
|
android:id="@+id/music_directory_play_button"
|
||||||
android:layout_width="12dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:background="@drawable/ic_play"
|
||||||
|
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/music_directory_cover_image_view"
|
app:layout_constraintBottom_toBottomOf="@+id/music_directory_cover_image_view"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/music_directory_title_text_view"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/music_directory_cover_image_view"
|
app:layout_constraintStart_toEndOf="@+id/music_directory_cover_image_view"
|
||||||
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
|
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
|
||||||
|
|
||||||
@@ -33,13 +36,14 @@
|
|||||||
style="@style/LabelMedium"
|
style="@style/LabelMedium"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="@string/label_placeholder"
|
android:text="@string/label_placeholder"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/music_directory_cover_image_view"
|
app:layout_constraintBottom_toBottomOf="@id/music_directory_cover_image_view"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/music_directory_more_button"
|
app:layout_constraintEnd_toStartOf="@+id/music_directory_more_button"
|
||||||
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
|
app:layout_constraintStart_toEndOf="@+id/music_directory_play_button"
|
||||||
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
|
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -54,17 +58,4 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="@id/music_directory_title_text_view"
|
app:layout_constraintBottom_toBottomOf="@id/music_directory_title_text_view"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view" />
|
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/music_directory_play_button"
|
|
||||||
android:layout_width="22dp"
|
|
||||||
android:layout_height="22dp"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/ic_play"
|
|
||||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/music_directory_title_text_view"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<View
|
<ImageView
|
||||||
android:id="@+id/cover_image_separator"
|
android:id="@+id/music_index_play_button"
|
||||||
android:layout_width="12dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="52dp"
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:background="@drawable/ic_play"
|
||||||
|
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/music_index_cover_image_view"
|
app:layout_constraintBottom_toBottomOf="@+id/music_index_cover_image_view"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/music_index_title_text_view"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/music_index_cover_image_view"
|
app:layout_constraintStart_toEndOf="@+id/music_index_cover_image_view"
|
||||||
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
|
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
|
||||||
|
|
||||||
@@ -34,13 +36,14 @@
|
|||||||
style="@style/LabelMedium"
|
style="@style/LabelMedium"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="@string/label_placeholder"
|
android:text="@string/label_placeholder"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/music_index_cover_image_view"
|
app:layout_constraintBottom_toBottomOf="@id/music_index_cover_image_view"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/music_index_more_button"
|
app:layout_constraintEnd_toStartOf="@+id/music_index_more_button"
|
||||||
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
|
app:layout_constraintStart_toEndOf="@+id/music_index_play_button"
|
||||||
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
|
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|||||||
@@ -139,6 +139,17 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/download_indicator_icon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:src="@drawable/ic_download" app:layout_constraintBottom_toBottomOf="@+id/queue_song_cover_image_view"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/queue_song_holder_image"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/queue_song_cover_image_view"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/queue_song_holder_image"
|
android:id="@+id/queue_song_holder_image"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
<item name="android:statusBarColor">?attr/colorSurface</item>
|
<item name="android:statusBarColor">?attr/colorSurface</item>
|
||||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||||
<item name="android:scrollbars">none</item>
|
<item name="android:scrollbars">none</item>
|
||||||
|
|
||||||
|
<item name="floatingActionButtonStyle">@style/FloatingActionButtonStyle</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Divider">
|
<style name="Divider">
|
||||||
@@ -40,6 +42,21 @@
|
|||||||
<item name="android:background">@color/dividerColor</item>
|
<item name="android:background">@color/dividerColor</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="FloatingActionButtonStyle" parent="Widget.MaterialComponents.FloatingActionButton">
|
||||||
|
<item name="backgroundTint">?attr/colorSecondary</item>
|
||||||
|
<item name="tint">?attr/colorOnPrimary</item>
|
||||||
|
<item name="shapeAppearanceOverlay">@style/FabShapeStyle</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="FabShapeStyle" parent="ShapeAppearance.MaterialComponents.SmallComponent">
|
||||||
|
<item name="cornerSize">50%</item>
|
||||||
|
<item name="cornerSizeBottomLeft">0dp</item>
|
||||||
|
<item name="cornerFamilyTopLeft">rounded</item>
|
||||||
|
<item name="cornerFamilyTopRight">rounded</item>
|
||||||
|
<item name="cornerFamilyBottomLeft">rounded</item>
|
||||||
|
<item name="cornerFamilyBottomRight">rounded</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="NoConnectionTextView">
|
<style name="NoConnectionTextView">
|
||||||
<item name="background">?attr/colorErrorContainer</item>
|
<item name="background">?attr/colorErrorContainer</item>
|
||||||
<item name="android:textColor">?attr/colorOnErrorContainer</item>
|
<item name="android:textColor">?attr/colorOnErrorContainer</item>
|
||||||
|
|||||||
@@ -201,7 +201,9 @@
|
|||||||
<string name="menu_sort_year">Rok</string>
|
<string name="menu_sort_year">Rok</string>
|
||||||
<string name="player_playback_speed">%1$.2fx</string>
|
<string name="player_playback_speed">%1$.2fx</string>
|
||||||
<string name="player_queue_clean_all_button">Wyczyść kolejkę odtwarzania</string>
|
<string name="player_queue_clean_all_button">Wyczyść kolejkę odtwarzania</string>
|
||||||
<string name="player_queue_save_queue_success">Zapisana kolejka odtwarzania</string>
|
<string name="player_queue_save_queue_success">Zapisano kolejkę odtwarzania</string>
|
||||||
|
<string name="player_queue_save_to_playlist">Zapisz kolejkę do playlisty</string>
|
||||||
|
<string name="player_queue_load_queue">Wczytaj kolejkę</string>
|
||||||
<string name="player_lyrics_download_content_description">Pobierz teksty do odtwarzania offline</string>
|
<string name="player_lyrics_download_content_description">Pobierz teksty do odtwarzania offline</string>
|
||||||
<string name="player_lyrics_downloaded_content_description">Teksty pobrane do odtwarzania offline</string>
|
<string name="player_lyrics_downloaded_content_description">Teksty pobrane do odtwarzania offline</string>
|
||||||
<string name="player_lyrics_download_success">Zapisano tekst do odtwarzania offline.</string>
|
<string name="player_lyrics_download_success">Zapisano tekst do odtwarzania offline.</string>
|
||||||
@@ -522,4 +524,7 @@
|
|||||||
<string name="settings_album_detail_summary">Jeżeli włączone, pokaż szczegóły albumu takie jak gatunek, ilość piosenek itp. na stronie albumu</string>
|
<string name="settings_album_detail_summary">Jeżeli włączone, pokaż szczegóły albumu takie jak gatunek, ilość piosenek itp. na stronie albumu</string>
|
||||||
<string name="settings_artist_sort_by_album_count">Sortuj wykonawców po ilości albumów</string>
|
<string name="settings_artist_sort_by_album_count">Sortuj wykonawców po ilości albumów</string>
|
||||||
<string name="settings_artist_sort_by_album_count_summary">Jeżeli włączone, sortuje wykonawców po ilości albumów. Jeżeli wyłączone, sortuje albumy po nazwach.</string>
|
<string name="settings_artist_sort_by_album_count_summary">Jeżeli włączone, sortuje wykonawców po ilości albumów. Jeżeli wyłączone, sortuje albumy po nazwach.</string>
|
||||||
|
<string name="folder_play_collecting">Zbieranie piosenek z folderu…</string>
|
||||||
|
<string name="folder_play_playing">Odtwarzanie %d piosenek</string>
|
||||||
|
<string name="folder_play_no_songs">Nie znaleziono piosenek w folderze</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -212,6 +212,8 @@
|
|||||||
<string name="player_playback_speed">%1$.2fx</string>
|
<string name="player_playback_speed">%1$.2fx</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_load_queue">Load Queue</string>
|
||||||
<string name="player_lyrics_download_content_description">Download lyrics for offline playback</string>
|
<string name="player_lyrics_download_content_description">Download lyrics for offline playback</string>
|
||||||
<string name="player_lyrics_downloaded_content_description">Lyrics downloaded for offline playback</string>
|
<string name="player_lyrics_downloaded_content_description">Lyrics downloaded for offline playback</string>
|
||||||
<string name="player_lyrics_download_success">Lyrics saved for offline playback.</string>
|
<string name="player_lyrics_download_success">Lyrics saved for offline playback.</string>
|
||||||
@@ -533,4 +535,8 @@
|
|||||||
<string name="settings_album_detail_summary">If enabled, show the album details like genre, song count etc. on the album page</string>
|
<string name="settings_album_detail_summary">If enabled, show the album details like genre, song count etc. on the album page</string>
|
||||||
<string name="settings_artist_sort_by_album_count">Sort artists by album count</string>
|
<string name="settings_artist_sort_by_album_count">Sort artists by album count</string>
|
||||||
<string name="settings_artist_sort_by_album_count_summary">If enabled, sort the artists by album count. Sort by name if disabled.</string>
|
<string name="settings_artist_sort_by_album_count_summary">If enabled, sort the artists by album count. Sort by name if disabled.</string>
|
||||||
|
|
||||||
|
<string name="folder_play_collecting">Collecting songs from folder…</string>
|
||||||
|
<string name="folder_play_playing">Playing %d songs</string>
|
||||||
|
<string name="folder_play_no_songs">No songs found in folder</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
<item name="android:statusBarColor">?attr/colorSurface</item>
|
<item name="android:statusBarColor">?attr/colorSurface</item>
|
||||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||||
<item name="android:scrollbars">none</item>
|
<item name="android:scrollbars">none</item>
|
||||||
|
|
||||||
|
<item name="floatingActionButtonStyle">@style/FloatingActionButtonStyle</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Divider">
|
<style name="Divider">
|
||||||
@@ -47,6 +49,21 @@
|
|||||||
<item name="android:background">@color/dividerColor</item>
|
<item name="android:background">@color/dividerColor</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="FloatingActionButtonStyle" parent="Widget.MaterialComponents.FloatingActionButton">
|
||||||
|
<item name="backgroundTint">?attr/colorSecondary</item>
|
||||||
|
<item name="tint">?attr/colorOnPrimary</item>
|
||||||
|
<item name="shapeAppearanceOverlay">@style/FabShapeStyle</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="FabShapeStyle" parent="ShapeAppearance.MaterialComponents.SmallComponent">
|
||||||
|
<item name="cornerSize">50%</item>
|
||||||
|
<item name="cornerSizeBottomLeft">0dp</item>
|
||||||
|
<item name="cornerFamilyTopLeft">rounded</item>
|
||||||
|
<item name="cornerFamilyTopRight">rounded</item>
|
||||||
|
<item name="cornerFamilyBottomLeft">rounded</item>
|
||||||
|
<item name="cornerFamilyBottomRight">rounded</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="NoConnectionTextView">
|
<style name="NoConnectionTextView">
|
||||||
<item name="background">?attr/colorErrorContainer</item>
|
<item name="background">?attr/colorErrorContainer</item>
|
||||||
<item name="android:textColor">?attr/colorOnErrorContainer</item>
|
<item name="android:textColor">?attr/colorOnErrorContainer</item>
|
||||||
|
|||||||
4
fastlane/metadata/android/en-US/changelogs/8.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/8.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
* chore: Add Obtainium badge to README
|
||||||
|
* fix: Revert "refactor MediaService"
|
||||||
|
* feat: add play functionality to library folder/index items
|
||||||
|
* fix: start queue blocking UI
|
||||||
3
fastlane/metadata/android/en-US/changelogs/9.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/9.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* chore: bringing in media service refactor previously reverted after more testing
|
||||||
|
* fix: refactor start queue to put the db writing in the background to address instant mix bug
|
||||||
|
* Feat: playerqueue fab allows playqueue actions -> saving to playlist, download all, load queue, shuffle, clean queue
|
||||||
BIN
mockup/usage/music_folders_playback.png
Executable file
BIN
mockup/usage/music_folders_playback.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
mockup/usage/music_folders_root.png
Executable file
BIN
mockup/usage/music_folders_root.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
Reference in New Issue
Block a user