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...
|
||||
|
||||
* 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)
|
||||
## What's Changed
|
||||
* fix: Fix player queue soft-lock by @shrapnelnet in https://github.com/eddyizm/tempus/pull/266
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<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://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>
|
||||
<!--
|
||||
<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
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
targetSdk 35
|
||||
|
||||
versionCode 7
|
||||
versionName '4.2.6'
|
||||
versionCode 9
|
||||
versionName '4.4.0'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
|
||||
@@ -27,8 +27,11 @@ public interface ClickCallback {
|
||||
default void onInternetRadioStationClick(Bundle bundle) {}
|
||||
default void onInternetRadioStationLongClick(Bundle bundle) {}
|
||||
default void onMusicFolderClick(Bundle bundle) {}
|
||||
default void onMusicFolderPlay(Bundle bundle) {}
|
||||
default void onMusicDirectoryClick(Bundle bundle) {}
|
||||
default void onMusicDirectoryPlay(Bundle bundle) {}
|
||||
default void onMusicIndexClick(Bundle bundle) {}
|
||||
default void onMusicIndexPlay(Bundle bundle) {}
|
||||
default void onDownloadGroupLongClick(Bundle bundle) {}
|
||||
default void onShareClick(Bundle bundle) {}
|
||||
default void onShareLongClick(Bundle bundle) {}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||
@@ -52,6 +55,8 @@ public class QueueRepository {
|
||||
public MutableLiveData<PlayQueue> getPlayQueue() {
|
||||
MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>();
|
||||
|
||||
Log.d(TAG, "Getting play queue from server...");
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBookmarksClient()
|
||||
.getPlayQueue()
|
||||
@@ -59,12 +64,19 @@ public class QueueRepository {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
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
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Failed to get play queue", t);
|
||||
playQueue.setValue(null);
|
||||
}
|
||||
});
|
||||
@@ -73,18 +85,24 @@ public class QueueRepository {
|
||||
}
|
||||
|
||||
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)
|
||||
.getBookmarksClient()
|
||||
.savePlayQueue(ids, current, position)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
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
|
||||
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) {
|
||||
if (queue == null || media == null) return false;
|
||||
|
||||
return queue.stream().anyMatch(queueItem ->
|
||||
queueItem != null && media.getId() != null &&
|
||||
queueItem.getId().equals(media.getId())
|
||||
return queue.stream().anyMatch(queueItem ->
|
||||
queueItem != null && media.getId() != null &&
|
||||
queueItem.getId().equals(media.getId())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,8 +163,8 @@ public class QueueRepository {
|
||||
List<Child> filteredToAdd = toAdd;
|
||||
final List<Queue> finalMedia = media;
|
||||
filteredToAdd = toAdd.stream()
|
||||
.filter(child -> !isMediaInQueue(finalMedia, child))
|
||||
.collect(Collectors.toList());
|
||||
.filter(child -> !isMediaInQueue(finalMedia, child))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (int i = 0; i < filteredToAdd.size(); i++) {
|
||||
Queue queueItem = new Queue(filteredToAdd.get(i));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.cappielloantonio.tempo.service;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.MediaItem;
|
||||
@@ -36,6 +37,8 @@ import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class MediaManager {
|
||||
@@ -43,6 +46,8 @@ public class MediaManager {
|
||||
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
||||
public static AtomicBoolean justStarted = new AtomicBoolean(false);
|
||||
|
||||
private static final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
public static void registerPlaybackObserver(
|
||||
ListenableFuture<MediaBrowser> browserFuture,
|
||||
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) {
|
||||
if (mediaBrowserListenableFuture != null) {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItems(MappingUtil.mapMediaItems(media), startIndex, 0);
|
||||
browser.prepare();
|
||||
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
justStarted.set(true);
|
||||
browser.setMediaItems(items, startIndex, 0);
|
||||
browser.prepare();
|
||||
|
||||
Player.Listener timelineListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
int itemCount = browser.getMediaItemCount();
|
||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||
browser.seekTo(startIndex, 0);
|
||||
browser.play();
|
||||
browser.removeListener(this);
|
||||
Player.Listener timelineListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
int itemCount = browser.getMediaItemCount();
|
||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||
browser.seekTo(startIndex, 0);
|
||||
browser.play();
|
||||
browser.removeListener(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
browser.addListener(timelineListener);
|
||||
};
|
||||
browser.addListener(timelineListener);
|
||||
});
|
||||
|
||||
enqueueDatabase(media, true, 0);
|
||||
backgroundExecutor.execute(() -> {
|
||||
enqueueDatabase(media, true, 0);
|
||||
});
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error executing startQueue logic: " + e.getMessage(), e);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
||||
.into(holder.item.musicDirectoryCoverImageView);
|
||||
|
||||
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
|
||||
@@ -80,6 +80,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
||||
itemView.setOnLongClickListener(v -> onLongClick());
|
||||
|
||||
item.musicDirectoryMoreButton.setOnClickListener(v -> onClick());
|
||||
item.musicDirectoryPlayButton.setOnClickListener(v -> onPlayClick());
|
||||
}
|
||||
|
||||
public void onClick() {
|
||||
@@ -107,5 +108,13 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
||||
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());
|
||||
item.musicIndexMoreButton.setOnClickListener(v -> onClick());
|
||||
item.musicIndexPlayButton.setOnClickListener(v -> onPlayClick());
|
||||
}
|
||||
|
||||
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());
|
||||
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.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
|
||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
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 (song.getStarred() == null && song.getUserRating() == null) {
|
||||
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.service.MediaManager;
|
||||
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.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.adapter.MusicDirectoryAdapter;
|
||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||
@@ -53,6 +59,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
private MusicDirectoryAdapter musicDirectoryAdapter;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private DirectoryRepository directoryRepository;
|
||||
|
||||
private MenuItem menuItem;
|
||||
|
||||
@@ -77,6 +84,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentDirectoryBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
|
||||
directoryRepository = new DirectoryRepository();
|
||||
|
||||
initAppBar();
|
||||
initDirectoryListView();
|
||||
@@ -197,4 +205,57 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
public void onMusicDirectoryClick(Bundle 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;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.Navigation;
|
||||
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.databinding.FragmentIndexBinding;
|
||||
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.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.MusicIndexAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.IndexUtil;
|
||||
import com.cappielloantonio.tempo.viewmodel.IndexViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@UnstableApi
|
||||
public class IndexFragment extends Fragment implements ClickCallback {
|
||||
@@ -32,6 +45,8 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
private IndexViewModel indexViewModel;
|
||||
|
||||
private MusicIndexAdapter musicIndexAdapter;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private DirectoryRepository directoryRepository;
|
||||
|
||||
@Override
|
||||
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);
|
||||
View view = bind.getRoot();
|
||||
indexViewModel = new ViewModelProvider(requireActivity()).get(IndexViewModel.class);
|
||||
directoryRepository = new DirectoryRepository();
|
||||
|
||||
initAppBar();
|
||||
initDirectoryListView();
|
||||
@@ -48,6 +64,18 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
releaseMediaBrowser();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@@ -107,4 +135,65 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
public void onMusicIndexClick(Bundle 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.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
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.viewmodel.LibraryViewModel;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -49,6 +55,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
|
||||
|
||||
private MaterialToolbar materialToolbar;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -79,6 +86,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
activity.setBottomNavigationBarVisibility(true);
|
||||
}
|
||||
|
||||
@@ -292,4 +300,8 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
public void onMusicFolderClick(Bundle 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.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -19,11 +23,17 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding;
|
||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||
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.PlayQueue;
|
||||
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.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@UnstableApi
|
||||
@@ -39,6 +50,18 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
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 PlaybackViewModel playbackViewModel;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
@@ -53,6 +76,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.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();
|
||||
|
||||
return view;
|
||||
@@ -62,8 +106,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeBrowser();
|
||||
bindMediaController();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
@@ -105,18 +147,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
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() {
|
||||
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
@@ -149,18 +179,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
fromPosition = viewHolder.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);
|
||||
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
|
||||
|
||||
@@ -188,46 +206,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
}).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() {
|
||||
playerSongQueueAdapter.notifyDataSetChanged();
|
||||
}
|
||||
@@ -259,4 +237,216 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
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 com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
@@ -21,7 +20,6 @@ import java.util.List;
|
||||
|
||||
public class AlbumListPageViewModel extends AndroidViewModel {
|
||||
private final AlbumRepository albumRepository;
|
||||
private final DownloadRepository downloadRepository;
|
||||
|
||||
public String title;
|
||||
public ArtistID3 artist;
|
||||
@@ -32,9 +30,7 @@ public class AlbumListPageViewModel extends AndroidViewModel {
|
||||
|
||||
public AlbumListPageViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
||||
albumRepository = new AlbumRepository();
|
||||
downloadRepository = new DownloadRepository();
|
||||
}
|
||||
|
||||
public LiveData<List<AlbumID3>> getAlbumList(LifecycleOwner owner) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.viewmodel;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
@@ -12,6 +13,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
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());
|
||||
|
||||
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 false;
|
||||
}
|
||||
|
||||
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
||||
if (TextUtils.isEmpty(songId)) {
|
||||
return;
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
<?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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -21,20 +14,74 @@
|
||||
android:id="@+id/player_queue_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="40dp"
|
||||
android:paddingTop="8dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/player_shuffle_queue_fab"
|
||||
<LinearLayout
|
||||
android:id="@+id/fab_menu_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/content_description_shuffle_button"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||
app:srcCompat="@drawable/ic_shuffle" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
|
||||
|
||||
<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_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/cover_image_separator"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="52dp"
|
||||
<ImageView
|
||||
android:id="@+id/music_directory_play_button"
|
||||
android:layout_width="32dp"
|
||||
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_constraintEnd_toStartOf="@+id/music_directory_title_text_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/music_directory_cover_image_view"
|
||||
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
|
||||
|
||||
@@ -33,13 +36,14 @@
|
||||
style="@style/LabelMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="marquee"
|
||||
android:paddingEnd="12dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/label_placeholder"
|
||||
app:layout_constraintBottom_toBottomOf="@id/music_directory_cover_image_view"
|
||||
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" />
|
||||
|
||||
<ImageView
|
||||
@@ -54,17 +58,4 @@
|
||||
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" />
|
||||
|
||||
<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>
|
||||
@@ -20,12 +20,14 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/cover_image_separator"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="52dp"
|
||||
<ImageView
|
||||
android:id="@+id/music_index_play_button"
|
||||
android:layout_width="32dp"
|
||||
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_constraintEnd_toStartOf="@+id/music_index_title_text_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/music_index_cover_image_view"
|
||||
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
|
||||
|
||||
@@ -34,13 +36,14 @@
|
||||
style="@style/LabelMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="marquee"
|
||||
android:paddingEnd="12dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/label_placeholder"
|
||||
app:layout_constraintBottom_toBottomOf="@id/music_index_cover_image_view"
|
||||
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" />
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -139,6 +139,17 @@
|
||||
</LinearLayout>
|
||||
</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
|
||||
android:id="@+id/queue_song_holder_image"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
<item name="android:statusBarColor">?attr/colorSurface</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
<item name="android:scrollbars">none</item>
|
||||
|
||||
<item name="floatingActionButtonStyle">@style/FloatingActionButtonStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="Divider">
|
||||
@@ -40,6 +42,21 @@
|
||||
<item name="android:background">@color/dividerColor</item>
|
||||
</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">
|
||||
<item name="background">?attr/colorErrorContainer</item>
|
||||
<item name="android:textColor">?attr/colorOnErrorContainer</item>
|
||||
|
||||
@@ -201,7 +201,9 @@
|
||||
<string name="menu_sort_year">Rok</string>
|
||||
<string name="player_playback_speed">%1$.2fx</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_downloaded_content_description">Teksty pobrane 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_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="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>
|
||||
|
||||
@@ -212,6 +212,8 @@
|
||||
<string name="player_playback_speed">%1$.2fx</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_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_downloaded_content_description">Lyrics downloaded 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_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="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>
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
<item name="android:statusBarColor">?attr/colorSurface</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
<item name="android:scrollbars">none</item>
|
||||
|
||||
<item name="floatingActionButtonStyle">@style/FloatingActionButtonStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="Divider">
|
||||
@@ -47,6 +49,21 @@
|
||||
<item name="android:background">@color/dividerColor</item>
|
||||
</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">
|
||||
<item name="background">?attr/colorErrorContainer</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