mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +00:00
Merge branch 'development'
This commit is contained in:
commit
4ab122a9d7
87 changed files with 6091 additions and 326 deletions
|
|
@ -24,6 +24,9 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
|
||||||
|
|
||||||
## Fork
|
## Fork
|
||||||
|
|
||||||
|
sha256 signing key fingerprint
|
||||||
|
`SHA256: B7:85:01:B9:34:D0:4E:0A:CA:8D:94:AF:D6:72:6A:4D:1D:CE:65:79:7F:1D:41:71:0F:64:3C:29:00:EB:1D:1D`
|
||||||
|
|
||||||
This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see.
|
This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see.
|
||||||
|
|
||||||
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ android {
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 32
|
versionCode 33
|
||||||
versionName '3.15.0'
|
versionName '3.16.0'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|
|
||||||
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -73,5 +73,20 @@
|
||||||
android:name="autoStoreLocales"
|
android:name="autoStoreLocales"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.WidgetProvider4x1"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/widget_label">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
|
||||||
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
||||||
|
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
|
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||||
|
|
@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
|
||||||
import com.cappielloantonio.tempo.model.Chronology;
|
import com.cappielloantonio.tempo.model.Chronology;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.model.Favorite;
|
import com.cappielloantonio.tempo.model.Favorite;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
import com.cappielloantonio.tempo.model.Queue;
|
import com.cappielloantonio.tempo.model.Queue;
|
||||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||||
import com.cappielloantonio.tempo.model.Server;
|
import com.cappielloantonio.tempo.model.Server;
|
||||||
|
|
@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Database(
|
@Database(
|
||||||
version = 11,
|
version = 12,
|
||||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
|
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
||||||
autoMigrations = {@AutoMigration(from = 10, to = 11)}
|
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
||||||
)
|
)
|
||||||
@TypeConverters({DateConverters.class})
|
@TypeConverters({DateConverters.class})
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
public abstract SessionMediaItemDao sessionMediaItemDao();
|
public abstract SessionMediaItemDao sessionMediaItemDao();
|
||||||
|
|
||||||
public abstract PlaylistDao playlistDao();
|
public abstract PlaylistDao playlistDao();
|
||||||
|
|
||||||
|
public abstract LyricsDao lyricsDao();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ public interface DownloadDao {
|
||||||
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||||
LiveData<List<Download>> getAll();
|
LiveData<List<Download>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||||
|
List<Download> getAllSync();
|
||||||
|
|
||||||
@Query("SELECT * FROM download WHERE id = :id")
|
@Query("SELECT * FROM download WHERE id = :id")
|
||||||
Download getOne(String id);
|
Download getOne(String id);
|
||||||
|
|
||||||
|
|
@ -30,6 +33,9 @@ public interface DownloadDao {
|
||||||
@Query("DELETE FROM download WHERE id = :id")
|
@Query("DELETE FROM download WHERE id = :id")
|
||||||
void delete(String id);
|
void delete(String id);
|
||||||
|
|
||||||
|
@Query("DELETE FROM download WHERE id IN (:ids)")
|
||||||
|
void deleteByIds(List<String> ids);
|
||||||
|
|
||||||
@Query("DELETE FROM download")
|
@Query("DELETE FROM download")
|
||||||
void deleteAll();
|
void deleteAll();
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.cappielloantonio.tempo.database.dao;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface LyricsDao {
|
||||||
|
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
LyricsCache getOne(String songId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
LiveData<LyricsCache> observeOne(String songId);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insert(LyricsCache lyricsCache);
|
||||||
|
|
||||||
|
@Query("DELETE FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
void delete(String songId);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.cappielloantonio.tempo.glide;
|
package com.cappielloantonio.tempo.glide;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
@ -16,6 +17,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
@ -109,6 +111,18 @@ public class CustomGlideRequest {
|
||||||
return uri.toString();
|
return uri.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void loadAlbumArtBitmap(Context context,
|
||||||
|
String coverId,
|
||||||
|
int size,
|
||||||
|
CustomTarget<Bitmap> target) {
|
||||||
|
String url = createUrl(coverId, size);
|
||||||
|
Glide.with(context)
|
||||||
|
.asBitmap()
|
||||||
|
.load(url)
|
||||||
|
.apply(createRequestOptions(context, coverId, ResourceType.Album))
|
||||||
|
.into(target);
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private final RequestManager requestManager;
|
private final RequestManager requestManager;
|
||||||
private Object item;
|
private Object item;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.cappielloantonio.tempo.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlin.jvm.JvmOverloads
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@Entity(tableName = "lyrics_cache")
|
||||||
|
data class LyricsCache @JvmOverloads constructor(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "song_id")
|
||||||
|
var songId: String,
|
||||||
|
@ColumnInfo(name = "artist")
|
||||||
|
var artist: String? = null,
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
var title: String? = null,
|
||||||
|
@ColumnInfo(name = "lyrics")
|
||||||
|
var lyrics: String? = null,
|
||||||
|
@ColumnInfo(name = "structured_lyrics")
|
||||||
|
var structuredLyrics: String? = null,
|
||||||
|
@ColumnInfo(name = "updated_at")
|
||||||
|
var updatedAt: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.model
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaItem.RequestMetadata
|
import androidx.media3.common.MediaItem.RequestMetadata
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
|
|
@ -243,6 +244,13 @@ class SessionMediaItem() {
|
||||||
.setAlbumTitle(album)
|
.setAlbumTitle(album)
|
||||||
.setArtist(artist)
|
.setArtist(artist)
|
||||||
.setArtworkUri(artworkUri)
|
.setArtworkUri(artworkUri)
|
||||||
|
.setUserRating(HeartRating(starred != null))
|
||||||
|
.setSupportedCommands(
|
||||||
|
listOf(
|
||||||
|
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||||
|
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||||
|
)
|
||||||
|
)
|
||||||
.setExtras(bundle)
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||||
|
|
@ -13,12 +15,92 @@ import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class ArtistRepository {
|
public class ArtistRepository {
|
||||||
|
private final AlbumRepository albumRepository;
|
||||||
|
|
||||||
|
public ArtistRepository() {
|
||||||
|
this.albumRepository = new AlbumRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getArtistAllSongs(String artistId, ArtistSongsCallback callback) {
|
||||||
|
Log.d("ArtistSync", "Getting albums for artist: " + artistId);
|
||||||
|
|
||||||
|
// Get the artist info first, which contains the albums
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getBrowsingClient()
|
||||||
|
.getArtist(artistId)
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null &&
|
||||||
|
response.body().getSubsonicResponse().getArtist() != null &&
|
||||||
|
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
|
||||||
|
|
||||||
|
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
|
||||||
|
Log.d("ArtistSync", "Got albums directly: " + albums.size());
|
||||||
|
|
||||||
|
if (!albums.isEmpty()) {
|
||||||
|
fetchAllAlbumSongsWithCallback(albums, callback);
|
||||||
|
} else {
|
||||||
|
Log.d("ArtistSync", "No albums found in artist response");
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d("ArtistSync", "Failed to get artist info");
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
Log.d("ArtistSync", "Error getting artist info: " + t.getMessage());
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchAllAlbumSongsWithCallback(List<AlbumID3> albums, ArtistSongsCallback callback) {
|
||||||
|
if (albums == null || albums.isEmpty()) {
|
||||||
|
Log.d("ArtistSync", "No albums to process");
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Child> allSongs = new ArrayList<>();
|
||||||
|
AtomicInteger remainingAlbums = new AtomicInteger(albums.size());
|
||||||
|
Log.d("ArtistSync", "Processing " + albums.size() + " albums");
|
||||||
|
|
||||||
|
for (AlbumID3 album : albums) {
|
||||||
|
Log.d("ArtistSync", "Getting tracks for album: " + album.getName());
|
||||||
|
MutableLiveData<List<Child>> albumTracks = albumRepository.getAlbumTracks(album.getId());
|
||||||
|
albumTracks.observeForever(songs -> {
|
||||||
|
Log.d("ArtistSync", "Got " + (songs != null ? songs.size() : 0) + " songs from album");
|
||||||
|
if (songs != null) {
|
||||||
|
allSongs.addAll(songs);
|
||||||
|
}
|
||||||
|
albumTracks.removeObservers(null);
|
||||||
|
|
||||||
|
int remaining = remainingAlbums.decrementAndGet();
|
||||||
|
Log.d("ArtistSync", "Remaining albums: " + remaining);
|
||||||
|
|
||||||
|
if (remaining == 0) {
|
||||||
|
Log.d("ArtistSync", "All albums processed. Total songs: " + allSongs.size());
|
||||||
|
callback.onSongsCollected(allSongs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ArtistSongsCallback {
|
||||||
|
void onSongsCollected(List<Child> songs);
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<ArtistID3>> getStarredArtists(boolean random, int size) {
|
public MutableLiveData<List<ArtistID3>> getStarredArtists(boolean random, int size) {
|
||||||
MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(new ArrayList<>());
|
MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
|
|
@ -89,7 +171,7 @@ public class ArtistRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Metodo che mi restituisce le informazioni essenzionali dell'artista (cover, numero di album...)
|
* Method that returns essential artist information (cover, album number, etc.)
|
||||||
*/
|
*/
|
||||||
public void getArtistInfo(List<ArtistID3> artists, MutableLiveData<List<ArtistID3>> list) {
|
public void getArtistInfo(List<ArtistID3> artists, MutableLiveData<List<ArtistID3>> list) {
|
||||||
List<ArtistID3> liveArtists = list.getValue();
|
List<ArtistID3> liveArtists = list.getValue();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ public class DownloadRepository {
|
||||||
return downloadDao.getAll();
|
return downloadDao.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Download> getAllDownloads() {
|
||||||
|
GetAllDownloadsThreadSafe getDownloads = new GetAllDownloadsThreadSafe(downloadDao);
|
||||||
|
Thread thread = new Thread(getDownloads);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
thread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDownloads.getDownloads();
|
||||||
|
}
|
||||||
|
|
||||||
public Download getDownload(String id) {
|
public Download getDownload(String id) {
|
||||||
Download download = null;
|
Download download = null;
|
||||||
|
|
||||||
|
|
@ -35,6 +49,24 @@ public class DownloadRepository {
|
||||||
return download;
|
return download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class GetAllDownloadsThreadSafe implements Runnable {
|
||||||
|
private final DownloadDao downloadDao;
|
||||||
|
private List<Download> downloads;
|
||||||
|
|
||||||
|
public GetAllDownloadsThreadSafe(DownloadDao downloadDao) {
|
||||||
|
this.downloadDao = downloadDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloads = downloadDao.getAllSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Download> getDownloads() {
|
||||||
|
return downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class GetDownloadThreadSafe implements Runnable {
|
private static class GetDownloadThreadSafe implements Runnable {
|
||||||
private final DownloadDao downloadDao;
|
private final DownloadDao downloadDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
@ -143,6 +175,12 @@ public class DownloadRepository {
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
DeleteMultipleThreadSafe delete = new DeleteMultipleThreadSafe(downloadDao, ids);
|
||||||
|
Thread thread = new Thread(delete);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
private static class DeleteThreadSafe implements Runnable {
|
private static class DeleteThreadSafe implements Runnable {
|
||||||
private final DownloadDao downloadDao;
|
private final DownloadDao downloadDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
@ -157,4 +195,19 @@ public class DownloadRepository {
|
||||||
downloadDao.delete(id);
|
downloadDao.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class DeleteMultipleThreadSafe implements Runnable {
|
||||||
|
private final DownloadDao downloadDao;
|
||||||
|
private final List<String> ids;
|
||||||
|
|
||||||
|
public DeleteMultipleThreadSafe(DownloadDao downloadDao, List<String> ids) {
|
||||||
|
this.downloadDao = downloadDao;
|
||||||
|
this.ids = ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloadDao.deleteByIds(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||||
|
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
|
|
||||||
|
public class LyricsRepository {
|
||||||
|
private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao();
|
||||||
|
|
||||||
|
public LyricsCache getLyrics(String songId) {
|
||||||
|
GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId);
|
||||||
|
Thread thread = new Thread(getLyricsThreadSafe);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
thread.join();
|
||||||
|
return getLyricsThreadSafe.getLyrics();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<LyricsCache> observeLyrics(String songId) {
|
||||||
|
return lyricsDao.observeOne(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insert(LyricsCache lyricsCache) {
|
||||||
|
InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache);
|
||||||
|
Thread thread = new Thread(insert);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String songId) {
|
||||||
|
DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId);
|
||||||
|
Thread thread = new Thread(delete);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GetLyricsThreadSafe implements Runnable {
|
||||||
|
private final LyricsDao lyricsDao;
|
||||||
|
private final String songId;
|
||||||
|
private LyricsCache lyricsCache;
|
||||||
|
|
||||||
|
public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||||
|
this.lyricsDao = lyricsDao;
|
||||||
|
this.songId = songId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
lyricsCache = lyricsDao.getOne(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LyricsCache getLyrics() {
|
||||||
|
return lyricsCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InsertThreadSafe implements Runnable {
|
||||||
|
private final LyricsDao lyricsDao;
|
||||||
|
private final LyricsCache lyricsCache;
|
||||||
|
|
||||||
|
public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) {
|
||||||
|
this.lyricsDao = lyricsDao;
|
||||||
|
this.lyricsCache = lyricsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
lyricsDao.insert(lyricsCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DeleteThreadSafe implements Runnable {
|
||||||
|
private final LyricsDao lyricsDao;
|
||||||
|
private final String songId;
|
||||||
|
|
||||||
|
public DeleteThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||||
|
this.lyricsDao = lyricsDao;
|
||||||
|
this.songId = songId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
lyricsDao.delete(songId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -81,6 +81,9 @@ public class PlaylistRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
||||||
|
if (songsId.isEmpty()) {
|
||||||
|
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||||
|
} else{
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
.updatePlaylist(playlistId, null, true, songsId, null)
|
||||||
|
|
@ -96,6 +99,7 @@ public class PlaylistRepository {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
|
|
@ -131,23 +135,6 @@ public class PlaylistRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList<String> songIdToAdd, ArrayList<Integer> songIndexToRemove) {
|
|
||||||
App.getSubsonicClientInstance(false)
|
|
||||||
.getPlaylistClient()
|
|
||||||
.updatePlaylist(playlistId, name, isPublic, songIdToAdd, songIndexToRemove)
|
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deletePlaylist(String playlistId) {
|
public void deletePlaylist(String playlistId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,25 @@ public class MediaManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void playDownloadedMediaItem(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, MediaItem mediaItem) {
|
||||||
|
if (mediaBrowserListenableFuture != null && mediaItem != null) {
|
||||||
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
try {
|
||||||
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
|
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||||
|
mediaBrowser.clearMediaItems();
|
||||||
|
mediaBrowser.setMediaItem(mediaItem);
|
||||||
|
mediaBrowser.prepare();
|
||||||
|
mediaBrowser.play();
|
||||||
|
clearDatabase();
|
||||||
|
}
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import android.util.Log;
|
||||||
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
|
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
|
||||||
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
|
|
||||||
|
|
@ -21,7 +24,15 @@ public class SystemClient {
|
||||||
|
|
||||||
public Call<ApiResponse> ping() {
|
public Call<ApiResponse> ping() {
|
||||||
Log.d(TAG, "ping()");
|
Log.d(TAG, "ping()");
|
||||||
return systemService.ping(subsonic.getParams());
|
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
||||||
|
if (Preferences.isInUseServerAddressLocal()) {
|
||||||
|
pingCall.timeout()
|
||||||
|
.timeout(1, TimeUnit.SECONDS);
|
||||||
|
} else {
|
||||||
|
pingCall.timeout()
|
||||||
|
.timeout(3, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
return pingCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Call<ApiResponse> getLicense() {
|
public Call<ApiResponse> getLicense() {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package com.cappielloantonio.tempo.ui.activity;
|
package com.cappielloantonio.tempo.ui.activity;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
|
@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
|
@ -56,6 +62,7 @@ public class MainActivity extends BaseActivity {
|
||||||
private BottomSheetBehavior bottomSheetBehavior;
|
private BottomSheetBehavior bottomSheetBehavior;
|
||||||
|
|
||||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||||
|
private Intent pendingDownloadPlaybackIntent;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -77,12 +84,16 @@ public class MainActivity extends BaseActivity {
|
||||||
checkConnectionType();
|
checkConnectionType();
|
||||||
getOpenSubsonicExtensions();
|
getOpenSubsonicExtensions();
|
||||||
checkTempoUpdate();
|
checkTempoUpdate();
|
||||||
|
|
||||||
|
maybeSchedulePlaybackIntent(getIntent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
pingServer();
|
||||||
initService();
|
initService();
|
||||||
|
consumePendingPlaybackIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -98,6 +109,14 @@ public class MainActivity extends BaseActivity {
|
||||||
bind = null;
|
bind = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
maybeSchedulePlaybackIntent(intent);
|
||||||
|
consumePendingPlaybackIntent();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
||||||
|
|
@ -351,6 +370,7 @@ public class MainActivity extends BaseActivity {
|
||||||
Preferences.switchInUseServerAddress();
|
Preferences.switchInUseServerAddress();
|
||||||
App.refreshSubsonicClient();
|
App.refreshSubsonicClient();
|
||||||
pingServer();
|
pingServer();
|
||||||
|
resetView();
|
||||||
} else {
|
} else {
|
||||||
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
|
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
|
||||||
}
|
}
|
||||||
|
|
@ -361,6 +381,7 @@ public class MainActivity extends BaseActivity {
|
||||||
Preferences.switchInUseServerAddress();
|
Preferences.switchInUseServerAddress();
|
||||||
App.refreshSubsonicClient();
|
App.refreshSubsonicClient();
|
||||||
pingServer();
|
pingServer();
|
||||||
|
resetView();
|
||||||
} else {
|
} else {
|
||||||
mainViewModel.ping().observe(this, subsonicResponse -> {
|
mainViewModel.ping().observe(this, subsonicResponse -> {
|
||||||
if (subsonicResponse == null) {
|
if (subsonicResponse == null) {
|
||||||
|
|
@ -376,6 +397,13 @@ public class MainActivity extends BaseActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resetView() {
|
||||||
|
resetViewModel();
|
||||||
|
int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
|
||||||
|
navController.popBackStack(id, true);
|
||||||
|
navController.navigate(id);
|
||||||
|
}
|
||||||
|
|
||||||
private void getOpenSubsonicExtensions() {
|
private void getOpenSubsonicExtensions() {
|
||||||
if (Preferences.getToken() != null) {
|
if (Preferences.getToken() != null) {
|
||||||
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
||||||
|
|
@ -408,4 +436,68 @@ public class MainActivity extends BaseActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void maybeSchedulePlaybackIntent(Intent intent) {
|
||||||
|
if (intent == null) return;
|
||||||
|
if (Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD.equals(intent.getAction())
|
||||||
|
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
|
||||||
|
pendingDownloadPlaybackIntent = new Intent(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumePendingPlaybackIntent() {
|
||||||
|
if (pendingDownloadPlaybackIntent == null) return;
|
||||||
|
Intent intent = pendingDownloadPlaybackIntent;
|
||||||
|
pendingDownloadPlaybackIntent = null;
|
||||||
|
playDownloadedMedia(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playDownloadedMedia(Intent intent) {
|
||||||
|
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
|
||||||
|
if (TextUtils.isEmpty(uriString)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = Uri.parse(uriString);
|
||||||
|
String mediaId = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID);
|
||||||
|
if (TextUtils.isEmpty(mediaId)) {
|
||||||
|
mediaId = uri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_TITLE);
|
||||||
|
String artist = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ARTIST);
|
||||||
|
String album = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ALBUM);
|
||||||
|
int duration = intent.getIntExtra(Constants.EXTRA_DOWNLOAD_DURATION, 0);
|
||||||
|
|
||||||
|
Bundle extras = new Bundle();
|
||||||
|
extras.putString("id", mediaId);
|
||||||
|
extras.putString("title", title);
|
||||||
|
extras.putString("artist", artist);
|
||||||
|
extras.putString("album", album);
|
||||||
|
extras.putString("uri", uri.toString());
|
||||||
|
extras.putString("type", Constants.MEDIA_TYPE_MUSIC);
|
||||||
|
extras.putInt("duration", duration);
|
||||||
|
|
||||||
|
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder()
|
||||||
|
.setExtras(extras)
|
||||||
|
.setIsBrowsable(false)
|
||||||
|
.setIsPlayable(true);
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(title)) metadataBuilder.setTitle(title);
|
||||||
|
if (!TextUtils.isEmpty(artist)) metadataBuilder.setArtist(artist);
|
||||||
|
if (!TextUtils.isEmpty(album)) metadataBuilder.setAlbumTitle(album);
|
||||||
|
|
||||||
|
MediaItem mediaItem = new MediaItem.Builder()
|
||||||
|
.setMediaId(mediaId)
|
||||||
|
.setMediaMetadata(metadataBuilder.build())
|
||||||
|
.setUri(uri)
|
||||||
|
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||||
|
.setRequestMetadata(new MediaItem.RequestMetadata.Builder()
|
||||||
|
.setMediaUri(uri)
|
||||||
|
.setExtras(extras)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import android.widget.Filterable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
@ -24,6 +25,8 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
@ -89,7 +92,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
public SongHorizontalAdapter(LifecycleOwner lifecycleOwner, ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.showCoverArt = showCoverArt;
|
this.showCoverArt = showCoverArt;
|
||||||
this.showAlbum = showAlbum;
|
this.showAlbum = showAlbum;
|
||||||
|
|
@ -98,6 +101,10 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||||
this.currentFilter = "";
|
this.currentFilter = "";
|
||||||
this.album = album;
|
this.album = album;
|
||||||
setHasStableIds(false);
|
setHasStableIds(false);
|
||||||
|
|
||||||
|
if (lifecycleOwner != null) {
|
||||||
|
MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
@ -135,11 +142,19 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||||
|
|
||||||
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
||||||
|
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (ExternalAudioReader.getUri(song) != null) {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showCoverArt) CustomGlideRequest.Builder
|
if (showCoverArt) CustomGlideRequest.Builder
|
||||||
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
|
|
@ -195,6 +210,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||||
bindPlaybackState(holder, song);
|
bindPlaybackState(holder, song);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleExternalAudioRefresh() {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) {
|
private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) {
|
||||||
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
|
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
|
@ -12,6 +15,9 @@ import androidx.media3.common.util.UnstableApi;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
|
@ -42,7 +48,21 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
||||||
if (dialog != null) {
|
if (dialog != null) {
|
||||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString != null) {
|
||||||
|
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
|
||||||
|
if (directory != null && directory.canWrite()) {
|
||||||
|
for (DocumentFile file : directory.listFiles()) {
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
|
}
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.cappielloantonio.tempo.ui.dialog;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
|
public class DownloadDirectoryPickerDialog extends DialogFragment {
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> folderPickerLauncher;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public android.app.Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
// Register launcher *before* button triggers
|
||||||
|
folderPickerLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == android.app.Activity.RESULT_OK) {
|
||||||
|
Intent data = result.getData();
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
);
|
||||||
|
|
||||||
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
|
||||||
|
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("Set Download Directory")
|
||||||
|
.setMessage("Choose a folder where downloaded songs will be stored.")
|
||||||
|
.setPositiveButton("Choose Folder", (dialog, which) -> {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
folderPickerLauncher.launch(intent);
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ public class DownloadStorageDialog extends DialogFragment {
|
||||||
.setTitle(R.string.download_storage_dialog_title)
|
.setTitle(R.string.download_storage_dialog_title)
|
||||||
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
||||||
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
||||||
|
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
|
||||||
.create();
|
.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||||
|
neutralButton.setOnClickListener(v -> {
|
||||||
|
int currentPreference = Preferences.getDownloadStoragePreference();
|
||||||
|
int newPreference = 2;
|
||||||
|
|
||||||
|
if (currentPreference != newPreference) {
|
||||||
|
Preferences.setDownloadStoragePreference(newPreference);
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||||
|
dialogClickCallback.onNeutralClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||||
|
|
||||||
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
|
@ -100,8 +101,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||||
public void onPlaylistClick(Bundle bundle) {
|
public void onPlaylistClick(Bundle bundle) {
|
||||||
if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) {
|
if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) {
|
||||||
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
||||||
playlistChooserViewModel.addSongsToPlaylist(playlist.getId());
|
playlistChooserViewModel.addSongsToPlaylist(this, getDialog(), playlist.getId());
|
||||||
dismiss();
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package com.cappielloantonio.tempo.ui.dialog;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.databinding.DialogStarredArtistSyncBinding;
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.StarredArtistsSyncViewModel;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public class StarredArtistSyncDialog extends DialogFragment {
|
||||||
|
private StarredArtistsSyncViewModel starredArtistsSyncViewModel;
|
||||||
|
|
||||||
|
private Runnable onCancel;
|
||||||
|
|
||||||
|
public StarredArtistSyncDialog(Runnable onCancel) {
|
||||||
|
this.onCancel = onCancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
DialogStarredArtistSyncBinding bind = DialogStarredArtistSyncBinding.inflate(getLayoutInflater());
|
||||||
|
|
||||||
|
starredArtistsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredArtistsSyncViewModel.class);
|
||||||
|
|
||||||
|
return new MaterialAlertDialogBuilder(getActivity())
|
||||||
|
.setView(bind.getRoot())
|
||||||
|
.setTitle(R.string.starred_artist_sync_dialog_title)
|
||||||
|
.setPositiveButton(R.string.starred_sync_dialog_positive_button, null)
|
||||||
|
.setNeutralButton(R.string.starred_sync_dialog_neutral_button, null)
|
||||||
|
.setNegativeButton(R.string.starred_sync_dialog_negative_button, null)
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
setButtonAction(requireContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setButtonAction(Context context) {
|
||||||
|
androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog();
|
||||||
|
|
||||||
|
if (dialog != null) {
|
||||||
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
|
positiveButton.setOnClickListener(v -> {
|
||||||
|
starredArtistsSyncViewModel.getStarredArtistSongs(requireActivity()).observe(this, allSongs -> {
|
||||||
|
if (allSongs != null && !allSongs.isEmpty()) {
|
||||||
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
|
MappingUtil.mapDownloads(allSongs),
|
||||||
|
allSongs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||||
|
neutralButton.setOnClickListener(v -> {
|
||||||
|
Preferences.setStarredArtistsSyncEnabled(true);
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
|
||||||
|
negativeButton.setOnClickListener(v -> {
|
||||||
|
Preferences.setStarredArtistsSyncEnabled(false);
|
||||||
|
if (onCancel != null) onCancel.run();
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,7 +61,7 @@ public class StarredSyncDialog extends DialogFragment {
|
||||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
||||||
if (songs != null) {
|
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
@ -130,7 +132,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
if (item.getItemId() == R.id.action_download_album) {
|
if (item.getItemId() == R.id.action_download_album) {
|
||||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
|
MappingUtil.mapDownloads(songs),
|
||||||
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +289,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.songRecyclerView.setHasFixedSize(true);
|
bind.songRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, false, false, album);
|
||||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
setMediaBrowserListenableFuture();
|
setMediaBrowserListenableFuture();
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
bind.artistPageRadioButton.setOnClickListener(v -> {
|
bind.artistPageRadioButton.setOnClickListener(v -> {
|
||||||
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (!songs.isEmpty()) {
|
if (songs != null && !songs.isEmpty()) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -178,7 +178,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||||
private void initTopSongsView() {
|
private void initTopSongsView() {
|
||||||
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, true, null);
|
||||||
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
setMediaBrowserListenableFuture();
|
setMediaBrowserListenableFuture();
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
|
@ -109,10 +111,14 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||||
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,17 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -40,6 +46,7 @@ import java.util.Objects;
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class DownloadFragment extends Fragment implements ClickCallback {
|
public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
private static final String TAG = "DownloadFragment";
|
private static final String TAG = "DownloadFragment";
|
||||||
|
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
|
||||||
|
|
||||||
private FragmentDownloadBinding bind;
|
private FragmentDownloadBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
|
|
@ -129,8 +136,27 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
downloadViewModel.getRefreshResult().observe(getViewLifecycleOwner(), count -> {
|
||||||
|
if (count == null || bind == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == -1) {
|
||||||
|
Toast.makeText(requireContext(), R.string.download_refresh_no_directory, Toast.LENGTH_SHORT).show();
|
||||||
|
} else if (count == 0) {
|
||||||
|
Toast.makeText(requireContext(), R.string.download_refresh_no_changes, Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
getResources().getQuantityString(R.plurals.download_refresh_removed, count, count),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
||||||
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
||||||
|
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishDownloadView(List<Child> songs) {
|
private void finishDownloadView(List<Child> songs) {
|
||||||
|
|
@ -216,6 +242,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
||||||
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (menuItem.getItemId() == R.id.menu_download_set_directory) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -267,4 +297,21 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
public void onDownloadGroupLongClick(Bundle bundle) {
|
public void onDownloadGroupLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == REQUEST_CODE_PICK_DIRECTORY && resultCode == Activity.RESULT_OK) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
);
|
||||||
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -40,6 +41,7 @@ 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.Share;
|
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||||
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.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
||||||
|
|
@ -64,6 +66,8 @@ import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
@ -116,6 +120,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
initSyncStarredView();
|
initSyncStarredView();
|
||||||
initSyncStarredAlbumsView();
|
initSyncStarredAlbumsView();
|
||||||
|
initSyncStarredArtistsView();
|
||||||
initDiscoverSongSlideView();
|
initDiscoverSongSlideView();
|
||||||
initSimilarSongView();
|
initSimilarSongView();
|
||||||
initArtistRadio();
|
initArtistRadio();
|
||||||
|
|
@ -274,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSyncStarredView() {
|
private void initSyncStarredView() {
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> songs) {
|
public void onChanged(List<Child> songs) {
|
||||||
|
|
@ -327,32 +332,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
private void initSyncStarredAlbumsView() {
|
private void initSyncStarredAlbumsView() {
|
||||||
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
||||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observeForever(new Observer<List<AlbumID3>>() {
|
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<AlbumID3>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<AlbumID3> albums) {
|
public void onChanged(List<AlbumID3> albums) {
|
||||||
if (albums != null) {
|
if (albums != null && !albums.isEmpty()) {
|
||||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
checkIfAlbumsNeedSync(albums);
|
||||||
List<String> albumsToSync = new ArrayList<>();
|
|
||||||
int albumCount = 0;
|
|
||||||
|
|
||||||
for (AlbumID3 album : albums) {
|
|
||||||
boolean needsSync = false;
|
|
||||||
albumCount++;
|
|
||||||
albumsToSync.add(album.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (albumCount > 0) {
|
|
||||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
|
||||||
String message = getResources().getQuantityString(
|
|
||||||
R.plurals.home_sync_starred_albums_count,
|
|
||||||
albumCount,
|
|
||||||
albumCount
|
|
||||||
);
|
|
||||||
bind.homeSyncStarredAlbumsToSync.setText(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).removeObserver(this);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -362,26 +347,157 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
});
|
});
|
||||||
|
|
||||||
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
||||||
homeViewModel.getAllStarredAlbumSongs().observeForever(new Observer<List<Child>>() {
|
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> allSongs) {
|
public void onChanged(List<Child> allSongs) {
|
||||||
if (allSongs != null) {
|
if (allSongs != null && !allSongs.isEmpty()) {
|
||||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
|
||||||
for (Child song : allSongs) {
|
for (Child song : allSongs) {
|
||||||
if (!manager.isDownloaded(song.getId())) {
|
if (!manager.isDownloaded(song.getId())) {
|
||||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||||
}
|
songsToDownload++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songsToDownload > 0) {
|
||||||
|
Toast.makeText(requireContext(),
|
||||||
|
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
homeViewModel.getAllStarredAlbumSongs().removeObserver(this);
|
|
||||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkIfAlbumsNeedSync(List<AlbumID3> albums) {
|
||||||
|
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> allSongs) {
|
||||||
|
if (allSongs != null) {
|
||||||
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
List<String> albumsNeedingSync = new ArrayList<>();
|
||||||
|
|
||||||
|
for (AlbumID3 album : albums) {
|
||||||
|
boolean albumNeedsSync = false;
|
||||||
|
// Check if any songs from this album need downloading
|
||||||
|
for (Child song : allSongs) {
|
||||||
|
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
|
||||||
|
!manager.isDownloaded(song.getId())) {
|
||||||
|
songsToDownload++;
|
||||||
|
albumNeedsSync = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (albumNeedsSync) {
|
||||||
|
albumsNeedingSync.add(album.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songsToDownload > 0) {
|
||||||
|
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
||||||
|
String message = getResources().getQuantityString(
|
||||||
|
R.plurals.home_sync_starred_albums_count,
|
||||||
|
albumsNeedingSync.size(),
|
||||||
|
albumsNeedingSync.size()
|
||||||
|
);
|
||||||
|
bind.homeSyncStarredAlbumsToSync.setText(message);
|
||||||
|
} else {
|
||||||
|
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSyncStarredArtistsView() {
|
||||||
|
if (Preferences.isStarredArtistsSyncEnabled()) {
|
||||||
|
homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<ArtistID3>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<ArtistID3> artists) {
|
||||||
|
if (artists != null && !artists.isEmpty()) {
|
||||||
|
checkIfArtistsNeedSync(artists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.homeSyncStarredArtistsCancel.setOnClickListener(v -> {
|
||||||
|
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
bind.homeSyncStarredArtistsDownload.setOnClickListener(v -> {
|
||||||
|
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> allSongs) {
|
||||||
|
if (allSongs != null && !allSongs.isEmpty()) {
|
||||||
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
|
||||||
|
for (Child song : allSongs) {
|
||||||
|
if (!manager.isDownloaded(song.getId())) {
|
||||||
|
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||||
|
songsToDownload++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songsToDownload > 0) {
|
||||||
|
Toast.makeText(requireContext(),
|
||||||
|
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkIfArtistsNeedSync(List<ArtistID3> artists) {
|
||||||
|
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> allSongs) {
|
||||||
|
if (allSongs != null) {
|
||||||
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
List<String> artistsNeedingSync = new ArrayList<>();
|
||||||
|
|
||||||
|
for (ArtistID3 artist : artists) {
|
||||||
|
boolean artistNeedsSync = false;
|
||||||
|
// Check if any songs from this artist need downloading
|
||||||
|
for (Child song : allSongs) {
|
||||||
|
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
|
||||||
|
!manager.isDownloaded(song.getId())) {
|
||||||
|
songsToDownload++;
|
||||||
|
artistNeedsSync = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (artistNeedsSync) {
|
||||||
|
artistsNeedingSync.add(artist.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songsToDownload > 0) {
|
||||||
|
bind.homeSyncStarredArtistsCard.setVisibility(View.VISIBLE);
|
||||||
|
String message = getResources().getQuantityString(
|
||||||
|
R.plurals.home_sync_starred_artists_count,
|
||||||
|
artistsNeedingSync.size(),
|
||||||
|
artistsNeedingSync.size()
|
||||||
|
);
|
||||||
|
bind.homeSyncStarredArtistsToSync.setText(message);
|
||||||
|
} else {
|
||||||
|
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void initDiscoverSongSlideView() {
|
private void initDiscoverSongSlideView() {
|
||||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||||
|
|
||||||
|
|
@ -484,7 +600,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
bind.topSongsRecyclerView.setHasFixedSize(true);
|
bind.topSongsRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
topSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
|
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
|
||||||
setTopSongsMediaBrowserListenableFuture();
|
setTopSongsMediaBrowserListenableFuture();
|
||||||
reapplyTopSongsPlayback();
|
reapplyTopSongsPlayback();
|
||||||
|
|
@ -525,7 +641,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
bind.starredTracksRecyclerView.setHasFixedSize(true);
|
bind.starredTracksRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
starredSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
|
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
|
||||||
setStarredSongsMediaBrowserListenableFuture();
|
setStarredSongsMediaBrowserListenableFuture();
|
||||||
reapplyStarredSongsPlayback();
|
reapplyStarredSongsPlayback();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import java.util.ArrayList;
|
||||||
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.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
@ -115,10 +117,14 @@ public class PlayerCoverFragment extends Fragment {
|
||||||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
||||||
if (song != null && bind != null) {
|
if (song != null && bind != null) {
|
||||||
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownload(song),
|
MappingUtil.mapDownload(song),
|
||||||
new Download(song)
|
new Download(song)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bind.innerButtonTopRight.setOnClickListener(view -> {
|
bind.innerButtonTopRight.setOnClickListener(view -> {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Layout;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
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.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Line;
|
import com.cappielloantonio.tempo.subsonic.models.Line;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
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;
|
||||||
|
import com.google.android.material.button.MaterialButton;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
private MediaBrowser mediaBrowser;
|
private MediaBrowser mediaBrowser;
|
||||||
private Handler syncLyricsHandler;
|
private Handler syncLyricsHandler;
|
||||||
private Runnable syncLyricsRunnable;
|
private Runnable syncLyricsRunnable;
|
||||||
|
private String currentLyrics;
|
||||||
|
private LyricsList currentLyricsList;
|
||||||
|
private String currentDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
|
@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
initPanelContent();
|
initPanelContent();
|
||||||
|
observeDownloadState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
bind = null;
|
bind = null;
|
||||||
|
currentLyrics = null;
|
||||||
|
currentLyricsList = null;
|
||||||
|
currentDescription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initOverlay() {
|
private void initOverlay() {
|
||||||
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
||||||
playerBottomSheetViewModel.changeSyncLyricsState();
|
playerBottomSheetViewModel.changeSyncLyricsState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bind.downloadLyricsButton.setOnClickListener(view -> {
|
||||||
|
boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics();
|
||||||
|
if (getContext() != null) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeBrowser() {
|
private void initializeBrowser() {
|
||||||
|
|
@ -136,48 +155,89 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPanelContent() {
|
private void initPanelContent() {
|
||||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
|
||||||
setPanelContent(null, lyricsList);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||||
setPanelContent(lyrics, null);
|
currentLyrics = lyrics;
|
||||||
|
updatePanelContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
|
currentLyricsList = lyricsList;
|
||||||
|
updatePanelContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||||
|
currentDescription = description;
|
||||||
|
updatePanelContent();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPanelContent(String lyrics, LyricsList lyricsList) {
|
private void observeDownloadState() {
|
||||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
|
||||||
if (bind != null) {
|
if (bind != null) {
|
||||||
|
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
|
||||||
|
if (cached != null && cached) {
|
||||||
|
downloadButton.setIconResource(R.drawable.ic_done);
|
||||||
|
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
|
||||||
|
} else {
|
||||||
|
downloadButton.setIconResource(R.drawable.ic_download);
|
||||||
|
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePanelContent() {
|
||||||
|
if (bind == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||||
|
|
||||||
if (lyrics != null && !lyrics.trim().equals("")) {
|
if (hasStructuredLyrics(currentLyricsList)) {
|
||||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
|
setSyncLirics(currentLyricsList);
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
|
||||||
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
|
|
||||||
setSyncLirics(lyricsList);
|
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||||
} else if (description != null && !description.trim().equals("")) {
|
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
|
bind.downloadLyricsButton.setEnabled(true);
|
||||||
|
} else if (hasText(currentLyrics)) {
|
||||||
|
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics));
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(true);
|
||||||
|
} else if (hasText(currentDescription)) {
|
||||||
|
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription));
|
||||||
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
|
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
private boolean hasText(String value) {
|
||||||
|
return value != null && !value.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||||
|
return lyricsList != null
|
||||||
|
&& lyricsList.getStructuredLyrics() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
|
|
@ -198,9 +258,12 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
|
|
||||||
private void defineProgressHandler() {
|
private void defineProgressHandler() {
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
if (lyricsList != null) {
|
if (!hasStructuredLyrics(lyricsList)) {
|
||||||
|
releaseHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
if (!lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||||
releaseHandler();
|
releaseHandler();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -217,9 +280,6 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
};
|
};
|
||||||
|
|
||||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||||
} else {
|
|
||||||
releaseHandler();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
||||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||||
|
|
||||||
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (hasStructuredLyrics(lyricsList)) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
@ -140,6 +142,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
if (item.getItemId() == R.id.action_download_playlist) {
|
if (item.getItemId() == R.id.action_download_playlist) {
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(child -> {
|
songs.stream().map(child -> {
|
||||||
|
|
@ -149,6 +152,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
return toDownload;
|
return toDownload;
|
||||||
}).collect(Collectors.toList())
|
}).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -258,7 +264,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.songRecyclerView.setHasFixedSize(true);
|
bind.songRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
setMediaBrowserListenableFuture();
|
setMediaBrowserListenableFuture();
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||||
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
|
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
setMediaBrowserListenableFuture();
|
setMediaBrowserListenableFuture();
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
||||||
|
|
@ -254,7 +254,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isQueryValid(String query) {
|
private boolean isQueryValid(String query) {
|
||||||
return !query.equals("") && query.trim().length() > 2;
|
return !query.equals("") && query.trim().length() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inputFocus() {
|
private void inputFocus() {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
package com.cappielloantonio.tempo.ui.fragment;
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.media.audiofx.AudioEffect;
|
import android.media.audiofx.AudioEffect;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
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.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
|
@ -42,10 +44,12 @@ import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -58,7 +62,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SettingViewModel settingViewModel;
|
private SettingViewModel settingViewModel;
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> someActivityResultLauncher;
|
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||||
|
|
||||||
private MediaService.LocalBinder mediaServiceBinder;
|
private MediaService.LocalBinder mediaServiceBinder;
|
||||||
private boolean isServiceBound = false;
|
private boolean isServiceBound = false;
|
||||||
|
|
@ -67,9 +72,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
someActivityResultLauncher = registerForActivityResult(
|
equalizerResultLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {}
|
||||||
|
);
|
||||||
|
|
||||||
|
directoryPickerLauncher = registerForActivityResult(
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
result -> {
|
result -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||||
|
Intent data = result.getData();
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
);
|
||||||
|
|
||||||
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), "Download folder set.", Toast.LENGTH_SHORT).show();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +128,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
checkSystemEqualizer();
|
checkSystemEqualizer();
|
||||||
checkCacheStorage();
|
checkCacheStorage();
|
||||||
checkStorage();
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
|
||||||
setStreamingCacheSize();
|
setStreamingCacheSize();
|
||||||
setAppLanguage();
|
setAppLanguage();
|
||||||
|
|
@ -110,10 +138,14 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
actionScan();
|
actionScan();
|
||||||
actionSyncStarredAlbums();
|
actionSyncStarredAlbums();
|
||||||
actionSyncStarredTracks();
|
actionSyncStarredTracks();
|
||||||
|
actionSyncStarredArtists();
|
||||||
actionChangeStreamingCacheStorage();
|
actionChangeStreamingCacheStorage();
|
||||||
actionChangeDownloadStorage();
|
actionChangeDownloadStorage();
|
||||||
|
actionSetDownloadDirectory();
|
||||||
actionDeleteDownloadStorage();
|
actionDeleteDownloadStorage();
|
||||||
actionKeepScreenOn();
|
actionKeepScreenOn();
|
||||||
|
actionAutoDownloadLyrics();
|
||||||
|
actionMiniPlayerHeart();
|
||||||
|
|
||||||
bindMediaService();
|
bindMediaService();
|
||||||
actionAppEqualizer();
|
actionAppEqualizer();
|
||||||
|
|
@ -148,7 +180,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||||
equalizer.setOnPreferenceClickListener(preference -> {
|
equalizer.setOnPreferenceClickListener(preference -> {
|
||||||
someActivityResultLauncher.launch(intent);
|
equalizerResultLauncher.launch(intent);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -165,7 +197,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
|
|
@ -181,13 +213,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
int pref = Preferences.getDownloadStoragePreference();
|
||||||
|
if (pref == 0) {
|
||||||
|
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||||
|
} else if (pref == 1) {
|
||||||
|
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
} else {
|
||||||
|
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkDownloadDirectory() {
|
||||||
|
Preference storage = findPreference("download_storage");
|
||||||
|
Preference directory = findPreference("set_download_directory");
|
||||||
|
|
||||||
|
if (directory == null) return;
|
||||||
|
|
||||||
|
String current = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (current != null) {
|
||||||
|
if (storage != null) storage.setVisible(false);
|
||||||
|
directory.setVisible(true);
|
||||||
|
directory.setIcon(R.drawable.ic_close);
|
||||||
|
directory.setTitle("Clear download folder");
|
||||||
|
directory.setSummary(current);
|
||||||
|
} else {
|
||||||
|
if (storage != null) storage.setVisible(true);
|
||||||
|
if (Preferences.getDownloadStoragePreference() == 2) {
|
||||||
|
directory.setVisible(true);
|
||||||
|
directory.setIcon(R.drawable.ic_folder);
|
||||||
|
directory.setTitle("Set download folder");
|
||||||
|
directory.setSummary("Choose a folder for downloaded music files");
|
||||||
|
} else {
|
||||||
|
directory.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setStreamingCacheSize() {
|
private void setStreamingCacheSize() {
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||||
|
|
||||||
|
|
@ -297,6 +362,20 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredArtists() {
|
||||||
|
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void actionChangeStreamingCacheStorage() {
|
private void actionChangeStreamingCacheStorage() {
|
||||||
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
||||||
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
||||||
|
|
@ -321,11 +400,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
@Override
|
@Override
|
||||||
public void onPositiveClick() {
|
public void onPositiveClick() {
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNegativeClick() {
|
public void onNegativeClick() {
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNeutralClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
|
@ -333,6 +420,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void actionSetDownloadDirectory() {
|
||||||
|
Preference pref = findPreference("set_download_directory");
|
||||||
|
if (pref != null) {
|
||||||
|
pref.setOnPreferenceClickListener(preference -> {
|
||||||
|
String current = Preferences.getDownloadDirectoryUri();
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
Preferences.setDownloadDirectoryUri(null);
|
||||||
|
Preferences.setDownloadStoragePreference(0);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
|
||||||
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
} else {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
directoryPickerLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void actionDeleteDownloadStorage() {
|
private void actionDeleteDownloadStorage() {
|
||||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
||||||
|
|
@ -341,6 +453,36 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void actionMiniPlayerHeart() {
|
||||||
|
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionAutoDownloadLyrics() {
|
||||||
|
SwitchPreference preference = findPreference("auto_download_lyrics");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void getScanStatus() {
|
private void getScanStatus() {
|
||||||
settingViewModel.getScanStatus(new ScanCallback() {
|
settingViewModel.getScanStatus(new ScanCallback() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||||
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.songListRecyclerView.setHasFixedSize(true);
|
bind.songListRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
setMediaBrowserListenableFuture();
|
setMediaBrowserListenableFuture();
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
@ -37,6 +38,8 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
|
@ -54,6 +57,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
|
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
|
||||||
private AlbumID3 album;
|
private AlbumID3 album;
|
||||||
|
|
||||||
|
private TextView removeAllTextView;
|
||||||
|
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||||
|
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -72,6 +79,12 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateRemoveAllVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
@ -163,7 +176,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||||
|
|
||||||
downloadAll.setOnClickListener(v -> {
|
downloadAll.setOnClickListener(v -> {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -182,19 +199,23 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
currentAlbumTracks = songs != null ? songs : Collections.emptyList();
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
currentAlbumMediaItems = MappingUtil.mapDownloads(currentAlbumTracks);
|
||||||
|
|
||||||
removeAll.setOnClickListener(v -> {
|
removeAllTextView.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
List<Download> downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList());
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
currentAlbumTracks.forEach(ExternalAudioReader::delete);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
updateRemoveAllVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
initDownloadUI(removeAll);
|
|
||||||
|
|
||||||
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
|
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
|
||||||
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
|
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
|
||||||
if (artist != null) {
|
if (artist != null) {
|
||||||
|
|
@ -234,14 +255,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initDownloadUI(TextView removeAll) {
|
private void updateRemoveAllVisibility() {
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
if (removeAllTextView == null) {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
return;
|
||||||
|
}
|
||||||
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
|
||||||
removeAll.setVisibility(View.VISIBLE);
|
if (currentAlbumTracks == null || currentAlbumTracks.isEmpty()) {
|
||||||
|
removeAllTextView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
List<MediaItem> mediaItems = currentAlbumMediaItems;
|
||||||
|
if (mediaItems == null || mediaItems.isEmpty()) {
|
||||||
|
removeAllTextView.setVisibility(View.GONE);
|
||||||
|
} else if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||||
|
removeAllTextView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
removeAllTextView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boolean hasLocal = currentAlbumTracks.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null);
|
||||||
|
removeAllTextView.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeMediaBrowser() {
|
private void initializeMediaBrowser() {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||||
super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Utilizzare il viewmodel come tramite ed evitare le chiamate dirette
|
// TODO Use the viewmodel as a conduit and avoid direct calls
|
||||||
private void init(View view) {
|
private void init(View view) {
|
||||||
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
|
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
|
|
@ -81,7 +81,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
|
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> {
|
favoriteToggle.setOnClickListener(v -> {
|
||||||
artistBottomSheetViewModel.setFavorite();
|
artistBottomSheetViewModel.setFavorite(requireContext());
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
|
@ -117,10 +119,13 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
|
||||||
|
|
||||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||||
removeAll.setOnClickListener(v -> {
|
removeAll.setOnClickListener(v -> {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||||
|
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
songs.forEach(ExternalAudioReader::delete);
|
||||||
|
}
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
@ -39,6 +41,10 @@ import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
|
@ -48,6 +54,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
private SongBottomSheetViewModel songBottomSheetViewModel;
|
private SongBottomSheetViewModel songBottomSheetViewModel;
|
||||||
private Child song;
|
private Child song;
|
||||||
|
|
||||||
|
private TextView downloadButton;
|
||||||
|
private TextView removeButton;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -66,6 +75,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateDownloadButtons);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
@ -157,25 +172,33 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView download = view.findViewById(R.id.download_text_view);
|
downloadButton = view.findViewById(R.id.download_text_view);
|
||||||
download.setOnClickListener(v -> {
|
downloadButton.setOnClickListener(v -> {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownload(song),
|
MappingUtil.mapDownload(song),
|
||||||
new Download(song)
|
new Download(song)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView remove = view.findViewById(R.id.remove_text_view);
|
removeButton = view.findViewById(R.id.remove_text_view);
|
||||||
remove.setOnClickListener(v -> {
|
removeButton.setOnClickListener(v -> {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||||
MappingUtil.mapDownload(song),
|
MappingUtil.mapDownload(song),
|
||||||
new Download(song)
|
new Download(song)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioReader.delete(song);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
initDownloadUI(download, remove);
|
updateDownloadButtons();
|
||||||
|
|
||||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> {
|
||||||
|
|
@ -243,12 +266,19 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initDownloadUI(TextView download, TextView remove) {
|
private void updateDownloadButtons() {
|
||||||
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
|
if (downloadButton == null || removeButton == null) {
|
||||||
remove.setVisibility(View.VISIBLE);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
boolean downloaded = DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId());
|
||||||
|
downloadButton.setVisibility(downloaded ? View.GONE : View.VISIBLE);
|
||||||
|
removeButton.setVisibility(downloaded ? View.VISIBLE : View.GONE);
|
||||||
} else {
|
} else {
|
||||||
download.setVisibility(View.VISIBLE);
|
boolean hasLocal = ExternalAudioReader.getUri(song) != null;
|
||||||
remove.setVisibility(View.GONE);
|
downloadButton.setVisibility(hasLocal ? View.GONE : View.VISIBLE);
|
||||||
|
removeButton.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,13 @@ object Constants {
|
||||||
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
||||||
|
|
||||||
const val DOWNLOAD_URI = "rest/download"
|
const val DOWNLOAD_URI = "rest/download"
|
||||||
|
const val ACTION_PLAY_EXTERNAL_DOWNLOAD = "com.cappielloantonio.tempo.action.PLAY_EXTERNAL_DOWNLOAD"
|
||||||
|
const val EXTRA_DOWNLOAD_URI = "EXTRA_DOWNLOAD_URI"
|
||||||
|
const val EXTRA_DOWNLOAD_MEDIA_ID = "EXTRA_DOWNLOAD_MEDIA_ID"
|
||||||
|
const val EXTRA_DOWNLOAD_TITLE = "EXTRA_DOWNLOAD_TITLE"
|
||||||
|
const val EXTRA_DOWNLOAD_ARTIST = "EXTRA_DOWNLOAD_ARTIST"
|
||||||
|
const val EXTRA_DOWNLOAD_ALBUM = "EXTRA_DOWNLOAD_ALBUM"
|
||||||
|
const val EXTRA_DOWNLOAD_DURATION = "EXTRA_DOWNLOAD_DURATION"
|
||||||
|
|
||||||
const val DOWNLOAD_TYPE_TRACK = "download_type_track"
|
const val DOWNLOAD_TYPE_TRACK = "download_type_track"
|
||||||
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
||||||
|
|
@ -116,4 +123,13 @@ object Constants {
|
||||||
const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED"
|
const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED"
|
||||||
const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS"
|
const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS"
|
||||||
const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED"
|
const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED"
|
||||||
|
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = "android.media3.session.demo.SHUFFLE_ON"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_HEART_ON = "android.media3.session.demo.HEART_ON"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = "android.media3.session.demo.HEART_OFF"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_HEART_LOADING = "android.media3.session.demo.HEART_LOADING"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE"
|
||||||
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
||||||
}
|
}
|
||||||
|
|
@ -187,19 +187,21 @@ public final class DownloadUtil {
|
||||||
|
|
||||||
private static synchronized File getDownloadDirectory(Context context) {
|
private static synchronized File getDownloadDirectory(Context context) {
|
||||||
if (downloadDirectory == null) {
|
if (downloadDirectory == null) {
|
||||||
if (Preferences.getDownloadStoragePreference() == 0) {
|
int pref = Preferences.getDownloadStoragePreference();
|
||||||
|
if (pref == 0) {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
if (downloadDirectory == null) {
|
if (downloadDirectory == null) {
|
||||||
downloadDirectory = context.getFilesDir();
|
downloadDirectory = context.getFilesDir();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (pref == 1) {
|
||||||
try {
|
try {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
Preferences.setDownloadStoragePreference(0);
|
Preferences.setDownloadStoragePreference(0);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.App;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
|
||||||
|
import java.text.Normalizer;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class ExternalAudioReader {
|
||||||
|
|
||||||
|
private static final Map<String, DocumentFile> cache = new ConcurrentHashMap<>();
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
private static final ExecutorService REFRESH_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
private static final MutableLiveData<Long> refreshEvents = new MutableLiveData<>();
|
||||||
|
|
||||||
|
private static volatile String cachedDirUri;
|
||||||
|
private static volatile boolean refreshInProgress = false;
|
||||||
|
private static volatile boolean refreshQueued = false;
|
||||||
|
|
||||||
|
private static String sanitizeFileName(String name) {
|
||||||
|
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||||
|
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeForComparison(String name) {
|
||||||
|
String s = sanitizeFileName(name);
|
||||||
|
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
|
||||||
|
s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
|
||||||
|
return s.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureCache() {
|
||||||
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString == null) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
cache.clear();
|
||||||
|
cachedDirUri = null;
|
||||||
|
}
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uriString.equals(cachedDirUri)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean runSynchronously = false;
|
||||||
|
synchronized (LOCK) {
|
||||||
|
if (refreshInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
scheduleRefreshLocked();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshInProgress = true;
|
||||||
|
runSynchronously = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runSynchronously) {
|
||||||
|
try {
|
||||||
|
rebuildCache();
|
||||||
|
} finally {
|
||||||
|
onRefreshFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void refreshCache() {
|
||||||
|
refreshCacheAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void refreshCacheAsync() {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
cachedDirUri = null;
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
requestRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LiveData<Long> getRefreshEvents() {
|
||||||
|
return refreshEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildKey(String artist, String title, String album) {
|
||||||
|
String name = artist != null && !artist.isEmpty() ? artist + " - " + title : title;
|
||||||
|
if (album != null && !album.isEmpty()) name += " (" + album + ")";
|
||||||
|
return normalizeForComparison(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri findUri(String artist, String title, String album) {
|
||||||
|
ensureCache();
|
||||||
|
if (cachedDirUri == null) return null;
|
||||||
|
|
||||||
|
DocumentFile file = cache.get(buildKey(artist, title, album));
|
||||||
|
return file != null && file.exists() ? file.getUri() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri getUri(Child media) {
|
||||||
|
return findUri(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri getUri(PodcastEpisode episode) {
|
||||||
|
return findUri(episode.getArtist(), episode.getTitle(), episode.getAlbum());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void removeMetadata(Child media) {
|
||||||
|
if (media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||||
|
cache.remove(key);
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean delete(Child media) {
|
||||||
|
ensureCache();
|
||||||
|
if (cachedDirUri == null) return false;
|
||||||
|
|
||||||
|
String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||||
|
DocumentFile file = cache.get(key);
|
||||||
|
boolean deleted = false;
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
deleted = file.delete();
|
||||||
|
}
|
||||||
|
if (deleted) {
|
||||||
|
cache.remove(key);
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requestRefresh() {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
scheduleRefreshLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scheduleRefreshLocked() {
|
||||||
|
if (refreshInProgress) {
|
||||||
|
refreshQueued = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshInProgress = true;
|
||||||
|
REFRESH_EXECUTOR.execute(() -> {
|
||||||
|
try {
|
||||||
|
rebuildCache();
|
||||||
|
} finally {
|
||||||
|
onRefreshFinished();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rebuildCache() {
|
||||||
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString == null) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
cache.clear();
|
||||||
|
cachedDirUri = null;
|
||||||
|
}
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile directory = DocumentFile.fromTreeUri(App.getContext(), Uri.parse(uriString));
|
||||||
|
Map<String, Long> expectedSizes = ExternalDownloadMetadataStore.snapshot();
|
||||||
|
Set<String> verifiedKeys = new HashSet<>();
|
||||||
|
Map<String, DocumentFile> newEntries = new HashMap<>();
|
||||||
|
|
||||||
|
if (directory != null && directory.canRead()) {
|
||||||
|
for (DocumentFile file : directory.listFiles()) {
|
||||||
|
if (file == null || file.isDirectory()) continue;
|
||||||
|
String existing = file.getName();
|
||||||
|
if (existing == null) continue;
|
||||||
|
|
||||||
|
String base = existing.replaceFirst("\\.[^\\.]+$", "");
|
||||||
|
String key = normalizeForComparison(base);
|
||||||
|
Long expected = expectedSizes.get(key);
|
||||||
|
long actualLength = file.length();
|
||||||
|
|
||||||
|
if (expected != null && expected > 0 && actualLength == expected) {
|
||||||
|
newEntries.put(key, file);
|
||||||
|
verifiedKeys.add(key);
|
||||||
|
} else {
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedSizes.isEmpty()) {
|
||||||
|
if (verifiedKeys.isEmpty()) {
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
|
} else {
|
||||||
|
for (String key : expectedSizes.keySet()) {
|
||||||
|
if (!verifiedKeys.contains(key)) {
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (LOCK) {
|
||||||
|
cache.clear();
|
||||||
|
cache.putAll(newEntries);
|
||||||
|
cachedDirUri = uriString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onRefreshFinished() {
|
||||||
|
boolean runAgain;
|
||||||
|
synchronized (LOCK) {
|
||||||
|
refreshInProgress = false;
|
||||||
|
runAgain = refreshQueued;
|
||||||
|
refreshQueued = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshEvents.postValue(SystemClock.elapsedRealtime());
|
||||||
|
|
||||||
|
if (runAgain) {
|
||||||
|
requestRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.text.Normalizer;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class ExternalAudioWriter {
|
||||||
|
|
||||||
|
private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
private static final int CONNECT_TIMEOUT_MS = 15_000;
|
||||||
|
private static final int READ_TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
|
private ExternalAudioWriter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sanitizeFileName(String name) {
|
||||||
|
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||||
|
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeForComparison(String name) {
|
||||||
|
String s = sanitizeFileName(name);
|
||||||
|
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
|
||||||
|
s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
|
||||||
|
return s.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DocumentFile findFile(DocumentFile dir, String fileName) {
|
||||||
|
String normalized = normalizeForComparison(fileName);
|
||||||
|
for (DocumentFile file : dir.listFiles()) {
|
||||||
|
if (file.isDirectory()) continue;
|
||||||
|
String existing = file.getName();
|
||||||
|
if (existing != null && normalizeForComparison(existing).equals(normalized)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void downloadToUserDirectory(Context context, Child child) {
|
||||||
|
if (context == null || child == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
MediaItem mediaItem = MappingUtil.mapDownload(child);
|
||||||
|
String fallbackName = child.getTitle() != null ? child.getTitle() : child.getId();
|
||||||
|
EXECUTOR.execute(() -> performDownload(appContext, mediaItem, fallbackName, child));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performDownload(Context context, MediaItem mediaItem, String fallbackName, Child child) {
|
||||||
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString == null) {
|
||||||
|
notifyUnavailable(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile directory = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
|
||||||
|
if (directory == null || !directory.canWrite()) {
|
||||||
|
notifyFailure(context, "Cannot write to folder.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String artist = child.getArtist() != null ? child.getArtist() : "";
|
||||||
|
String title = child.getTitle() != null ? child.getTitle() : fallbackName;
|
||||||
|
String album = child.getAlbum() != null ? child.getAlbum() : "";
|
||||||
|
String baseName = artist.isEmpty() ? title : artist + " - " + title;
|
||||||
|
if (!album.isEmpty()) baseName += " (" + album + ")";
|
||||||
|
if (baseName.isEmpty()) {
|
||||||
|
baseName = fallbackName != null ? fallbackName : "download";
|
||||||
|
}
|
||||||
|
String metadataKey = normalizeForComparison(baseName);
|
||||||
|
|
||||||
|
Uri mediaUri = mediaItem != null && mediaItem.requestMetadata != null
|
||||||
|
? mediaItem.requestMetadata.mediaUri
|
||||||
|
: null;
|
||||||
|
if (mediaUri == null) {
|
||||||
|
notifyFailure(context, "Invalid media URI.");
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String scheme = mediaUri.getScheme();
|
||||||
|
if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
|
||||||
|
notifyFailure(context, "Unsupported media URI.");
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection connection = null;
|
||||||
|
DocumentFile targetFile = null;
|
||||||
|
try {
|
||||||
|
connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection();
|
||||||
|
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||||
|
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||||
|
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) {
|
||||||
|
notifyFailure(context, "Server returned " + responseCode);
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mimeType = connection.getContentType();
|
||||||
|
if (mimeType == null || mimeType.isEmpty()) {
|
||||||
|
mimeType = "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||||
|
if (extension == null || extension.isEmpty()) {
|
||||||
|
String suffix = child.getSuffix();
|
||||||
|
if (suffix != null && !suffix.isEmpty()) {
|
||||||
|
extension = suffix;
|
||||||
|
} else {
|
||||||
|
extension = "bin";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String sanitized = sanitizeFileName(baseName);
|
||||||
|
if (sanitized.isEmpty()) sanitized = sanitizeFileName(fallbackName);
|
||||||
|
if (sanitized.isEmpty()) sanitized = "download";
|
||||||
|
String fileName = sanitized + "." + extension;
|
||||||
|
|
||||||
|
DocumentFile existingFile = findFile(directory, fileName);
|
||||||
|
long remoteLength = connection.getContentLengthLong();
|
||||||
|
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
|
||||||
|
if (existingFile != null && existingFile.exists()) {
|
||||||
|
long localLength = existingFile.length();
|
||||||
|
boolean matches = false;
|
||||||
|
if (remoteLength > 0 && localLength == remoteLength) {
|
||||||
|
matches = true;
|
||||||
|
} else if (remoteLength <= 0 && recordedSize != null && localLength == recordedSize) {
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
ExternalDownloadMetadataStore.recordSize(metadataKey, localLength);
|
||||||
|
recordDownload(child, existingFile.getUri());
|
||||||
|
ExternalAudioReader.refreshCacheAsync();
|
||||||
|
notifyExists(context, fileName);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
existingFile.delete();
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFile = directory.createFile(mimeType, fileName);
|
||||||
|
if (targetFile == null) {
|
||||||
|
notifyFailure(context, "Failed to create file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri targetUri = targetFile.getUri();
|
||||||
|
try (InputStream in = connection.getInputStream();
|
||||||
|
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
|
||||||
|
if (out == null) {
|
||||||
|
notifyFailure(context, "Cannot open output stream.");
|
||||||
|
targetFile.delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int len;
|
||||||
|
long total = 0;
|
||||||
|
while ((len = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, len);
|
||||||
|
total += len;
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (total <= 0) {
|
||||||
|
targetFile.delete();
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
notifyFailure(context, "Empty download.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteLength > 0 && total != remoteLength) {
|
||||||
|
targetFile.delete();
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
notifyFailure(context, "Incomplete download.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalDownloadMetadataStore.recordSize(metadataKey, total);
|
||||||
|
recordDownload(child, targetUri);
|
||||||
|
notifySuccess(context, fileName, child, targetUri);
|
||||||
|
ExternalAudioReader.refreshCacheAsync();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (targetFile != null) {
|
||||||
|
targetFile.delete();
|
||||||
|
}
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
notifyFailure(context, e.getMessage() != null ? e.getMessage() : "Download failed");
|
||||||
|
} finally {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notifyUnavailable(Context context) {
|
||||||
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", context.getPackageName(), null));
|
||||||
|
PendingIntent openSettings = PendingIntent.getActivity(context, 0, settingsIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle("No download folder set")
|
||||||
|
.setContentText("Tap to set one in settings")
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setSilent(true)
|
||||||
|
.setContentIntent(openSettings)
|
||||||
|
.setAutoCancel(true);
|
||||||
|
|
||||||
|
manager.notify(1011, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notifyFailure(Context context, String message) {
|
||||||
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle("Download failed")
|
||||||
|
.setContentText(message)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setAutoCancel(true);
|
||||||
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notifySuccess(Context context, String name, Child child, Uri fileUri) {
|
||||||
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle("Download complete")
|
||||||
|
.setContentText(name)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setAutoCancel(true);
|
||||||
|
|
||||||
|
PendingIntent playIntent = buildPlayIntent(context, child, fileUri);
|
||||||
|
if (playIntent != null) {
|
||||||
|
builder.setContentIntent(playIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void recordDownload(Child child, Uri fileUri) {
|
||||||
|
if (child == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Download download = new Download(child);
|
||||||
|
download.setDownloadState(1);
|
||||||
|
if (fileUri != null) {
|
||||||
|
download.setDownloadUri(fileUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
new DownloadRepository().insert(download);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notifyExists(Context context, String name) {
|
||||||
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle("Already downloaded")
|
||||||
|
.setContentText(name)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
.setAutoCancel(true);
|
||||||
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PendingIntent buildPlayIntent(Context context, Child child, Uri fileUri) {
|
||||||
|
if (fileUri == null) return null;
|
||||||
|
Intent intent = new Intent(context, MainActivity.class)
|
||||||
|
.setAction(Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD)
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_URI, fileUri.toString())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID, child.getId())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_TITLE, child.getTitle())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_ARTIST, child.getArtist())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_ALBUM, child.getAlbum())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_DURATION, child.getDuration() != null ? child.getDuration() : 0)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
|
||||||
|
int requestCode;
|
||||||
|
if (child.getId() != null) {
|
||||||
|
requestCode = Math.abs(child.getId().hashCode());
|
||||||
|
} else {
|
||||||
|
requestCode = Math.abs(fileUri.toString().hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
requestCode,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.App;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ExternalDownloadMetadataStore {
|
||||||
|
|
||||||
|
private static final String PREF_KEY = "external_download_metadata";
|
||||||
|
|
||||||
|
private ExternalDownloadMetadataStore() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences preferences() {
|
||||||
|
return App.getInstance().getPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject readAll() {
|
||||||
|
String raw = preferences().getString(PREF_KEY, "{}");
|
||||||
|
try {
|
||||||
|
return new JSONObject(raw);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return new JSONObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAll(JSONObject object) {
|
||||||
|
preferences().edit().putString(PREF_KEY, object.toString()).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void clear() {
|
||||||
|
writeAll(new JSONObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void recordSize(String key, long size) {
|
||||||
|
if (key == null || size <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
try {
|
||||||
|
object.put(key, size);
|
||||||
|
} catch (JSONException ignored) {
|
||||||
|
}
|
||||||
|
writeAll(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void remove(String key) {
|
||||||
|
if (key == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
object.remove(key);
|
||||||
|
writeAll(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static synchronized Long getSize(String key) {
|
||||||
|
if (key == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (!object.has(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long size = object.optLong(key, -1L);
|
||||||
|
return size > 0 ? size : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized Map<String, Long> snapshot() {
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (object.length() == 0) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Long> sizes = new HashMap<>();
|
||||||
|
Iterator<String> keys = object.keys();
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
String key = keys.next();
|
||||||
|
long size = object.optLong(key, -1L);
|
||||||
|
if (size > 0) {
|
||||||
|
sizes.put(key, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void retainOnly(Set<String> keysToKeep) {
|
||||||
|
if (keysToKeep == null || keysToKeep.isEmpty()) {
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (object.length() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> keys = new HashSet<>();
|
||||||
|
Iterator<String> iterator = object.keys();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
keys.add(iterator.next());
|
||||||
|
}
|
||||||
|
boolean changed = false;
|
||||||
|
for (String key : keys) {
|
||||||
|
if (!keysToKeep.contains(key)) {
|
||||||
|
object.remove(key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
writeAll(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,12 @@ import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.HeartRating;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
|
@ -16,6 +18,7 @@ import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -83,6 +86,13 @@ public class MappingUtil {
|
||||||
.setAlbumTitle(media.getAlbum())
|
.setAlbumTitle(media.getAlbum())
|
||||||
.setArtist(media.getArtist())
|
.setArtist(media.getArtist())
|
||||||
.setArtworkUri(artworkUri)
|
.setArtworkUri(artworkUri)
|
||||||
|
.setUserRating(new HeartRating(media.getStarred() != null))
|
||||||
|
.setSupportedCommands(
|
||||||
|
ImmutableList.of(
|
||||||
|
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||||
|
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||||
|
)
|
||||||
|
)
|
||||||
.setExtras(bundle)
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
|
|
@ -217,12 +227,20 @@ public class MappingUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(Child media) {
|
private static Uri getUri(Child media) {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||||
|
Uri local = ExternalAudioReader.getUri(media);
|
||||||
|
return local != null ? local : MusicUtil.getStreamUri(media.getId());
|
||||||
|
}
|
||||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
||||||
? getDownloadUri(media.getId())
|
? getDownloadUri(media.getId())
|
||||||
: MusicUtil.getStreamUri(media.getId());
|
: MusicUtil.getStreamUri(media.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
||||||
|
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||||
|
Uri local = ExternalAudioReader.getUri(podcastEpisode);
|
||||||
|
return local != null ? local : MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||||
|
}
|
||||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
|
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
|
||||||
? getDownloadUri(podcastEpisode.getStreamId())
|
? getDownloadUri(podcastEpisode.getStreamId())
|
||||||
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||||
|
|
@ -232,4 +250,11 @@ public class MappingUtil {
|
||||||
Download download = new DownloadRepository().getDownload(id);
|
Download download = new DownloadRepository().getDownload(id);
|
||||||
return download != null && !download.getDownloadUri().isEmpty() ? Uri.parse(download.getDownloadUri()) : MusicUtil.getDownloadUri(id);
|
return download != null && !download.getDownloadUri().isEmpty() ? Uri.parse(download.getDownloadUri()) : MusicUtil.getDownloadUri(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void observeExternalAudioRefresh(LifecycleOwner owner, Runnable onRefresh) {
|
||||||
|
if (owner == null || onRefresh == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExternalAudioReader.getRefreshEvents().observe(owner, event -> onRefresh.run());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ object Preferences {
|
||||||
private const val WIFI_ONLY = "wifi_only"
|
private const val WIFI_ONLY = "wifi_only"
|
||||||
private const val DATA_SAVING_MODE = "data_saving_mode"
|
private const val DATA_SAVING_MODE = "data_saving_mode"
|
||||||
private const val SERVER_UNREACHABLE = "server_unreachable"
|
private const val SERVER_UNREACHABLE = "server_unreachable"
|
||||||
|
private const val SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE = "sync_starred_artists_for_offline_use"
|
||||||
private const val SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE = "sync_starred_albums_for_offline_use"
|
private const val SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE = "sync_starred_albums_for_offline_use"
|
||||||
private const val SYNC_STARRED_TRACKS_FOR_OFFLINE_USE = "sync_starred_tracks_for_offline_use"
|
private const val SYNC_STARRED_TRACKS_FOR_OFFLINE_USE = "sync_starred_tracks_for_offline_use"
|
||||||
private const val QUEUE_SYNCING = "queue_syncing"
|
private const val QUEUE_SYNCING = "queue_syncing"
|
||||||
|
|
@ -45,11 +46,13 @@ object Preferences {
|
||||||
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
||||||
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
||||||
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
|
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
|
||||||
|
private const val AUTO_DOWNLOAD_LYRICS = "auto_download_lyrics"
|
||||||
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
||||||
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
||||||
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||||
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
||||||
private const val DOWNLOAD_STORAGE = "download_storage"
|
private const val DOWNLOAD_STORAGE = "download_storage"
|
||||||
|
private const val DOWNLOAD_DIRECTORY_URI = "download_directory_uri"
|
||||||
private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
|
private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
|
||||||
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
||||||
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
||||||
|
|
@ -69,8 +72,10 @@ object Preferences {
|
||||||
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
||||||
private const val CONTINUOUS_PLAY = "continuous_play"
|
private const val CONTINUOUS_PLAY = "continuous_play"
|
||||||
private const val LAST_INSTANT_MIX = "last_instant_mix"
|
private const val LAST_INSTANT_MIX = "last_instant_mix"
|
||||||
|
private const val ALLOW_PLAYLIST_DUPLICATES = "allow_playlist_duplicates"
|
||||||
private const val EQUALIZER_ENABLED = "equalizer_enabled"
|
private const val EQUALIZER_ENABLED = "equalizer_enabled"
|
||||||
private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels"
|
private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels"
|
||||||
|
private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServer(): String? {
|
fun getServer(): String? {
|
||||||
|
|
@ -162,6 +167,24 @@ object Preferences {
|
||||||
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAutoDownloadLyricsEnabled(): Boolean {
|
||||||
|
val preferences = App.getInstance().preferences
|
||||||
|
|
||||||
|
if (preferences.contains(AUTO_DOWNLOAD_LYRICS)) {
|
||||||
|
return preferences.getBoolean(AUTO_DOWNLOAD_LYRICS, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setAutoDownloadLyricsEnabled(isEnabled: Boolean) {
|
||||||
|
App.getInstance().preferences.edit()
|
||||||
|
.putBoolean(AUTO_DOWNLOAD_LYRICS, isEnabled)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getLocalAddress(): String? {
|
fun getLocalAddress(): String? {
|
||||||
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
||||||
|
|
@ -303,6 +326,18 @@ object Preferences {
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isStarredArtistsSyncEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setStarredArtistsSyncEnabled(isStarredSyncEnabled: Boolean) {
|
||||||
|
App.getInstance().preferences.edit().putBoolean(
|
||||||
|
SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE, isStarredSyncEnabled
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isStarredAlbumsSyncEnabled(): Boolean {
|
fun isStarredAlbumsSyncEnabled(): Boolean {
|
||||||
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false)
|
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false)
|
||||||
|
|
@ -327,6 +362,16 @@ object Preferences {
|
||||||
).apply()
|
).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun showShuffleInsteadOfHeart(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setShuffleInsteadOfHeart(enabled: Boolean) {
|
||||||
|
App.getInstance().preferences.edit().putBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun showServerUnreachableDialog(): Boolean {
|
fun showServerUnreachableDialog(): Boolean {
|
||||||
return App.getInstance().preferences.getLong(
|
return App.getInstance().preferences.getLong(
|
||||||
|
|
@ -420,6 +465,20 @@ object Preferences {
|
||||||
).apply()
|
).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getDownloadDirectoryUri(): String? {
|
||||||
|
return App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setDownloadDirectoryUri(uri: String?) {
|
||||||
|
val current = App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null)
|
||||||
|
if (current != uri) {
|
||||||
|
ExternalDownloadMetadataStore.clear()
|
||||||
|
}
|
||||||
|
App.getInstance().preferences.edit().putString(DOWNLOAD_DIRECTORY_URI, uri).apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getDefaultDownloadViewType(): String {
|
fun getDefaultDownloadViewType(): String {
|
||||||
return App.getInstance().preferences.getString(
|
return App.getInstance().preferences.getString(
|
||||||
|
|
@ -540,6 +599,19 @@ object Preferences {
|
||||||
) + 5000 < System.currentTimeMillis()
|
) + 5000 < System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setAllowPlaylistDuplicates(allowDuplicates: Boolean) {
|
||||||
|
return App.getInstance().preferences.edit().putString(
|
||||||
|
ALLOW_PLAYLIST_DUPLICATES,
|
||||||
|
allowDuplicates.toString()
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun allowPlaylistDuplicates(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(ALLOW_PLAYLIST_DUPLICATES, false)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setEqualizerEnabled(enabled: Boolean) {
|
fun setEqualizerEnabled(enabled: Boolean) {
|
||||||
App.getInstance().preferences.edit().putBoolean(EQUALIZER_ENABLED, enabled).apply()
|
App.getInstance().preferences.edit().putBoolean(EQUALIZER_ENABLED, enabled).apply()
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||||
private final ArtistRepository artistRepository;
|
private final ArtistRepository artistRepository;
|
||||||
|
|
@ -34,7 +42,7 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||||
this.artist = artist;
|
this.artist = artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFavorite() {
|
public void setFavorite(Context context) {
|
||||||
if (artist.getStarred() != null) {
|
if (artist.getStarred() != null) {
|
||||||
if (NetworkUtil.isOffline()) {
|
if (NetworkUtil.isOffline()) {
|
||||||
removeFavoriteOffline();
|
removeFavoriteOffline();
|
||||||
|
|
@ -43,9 +51,9 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (NetworkUtil.isOffline()) {
|
if (NetworkUtil.isOffline()) {
|
||||||
setFavoriteOffline();
|
setFavoriteOffline(context);
|
||||||
} else {
|
} else {
|
||||||
setFavoriteOnline();
|
setFavoriteOnline(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +67,6 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||||
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
|
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onError() {
|
public void onError() {
|
||||||
// artist.setStarred(new Date());
|
|
||||||
favoriteRepository.starLater(null, null, artist.getId(), false);
|
favoriteRepository.starLater(null, null, artist.getId(), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -67,20 +74,45 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||||
artist.setStarred(null);
|
artist.setStarred(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFavoriteOffline() {
|
private void setFavoriteOffline(Context context) {
|
||||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||||
artist.setStarred(new Date());
|
artist.setStarred(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFavoriteOnline() {
|
private void setFavoriteOnline(Context context) {
|
||||||
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
|
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onError() {
|
public void onError() {
|
||||||
// artist.setStarred(null);
|
|
||||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
artist.setStarred(new Date());
|
artist.setStarred(new Date());
|
||||||
|
|
||||||
|
Log.d("ArtistSync", "Checking preference: " + Preferences.isStarredArtistsSyncEnabled());
|
||||||
|
|
||||||
|
if (Preferences.isStarredArtistsSyncEnabled()) {
|
||||||
|
Log.d("ArtistSync", "Starting artist sync for: " + artist.getName());
|
||||||
|
|
||||||
|
artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSongsCollected(List<Child> songs) {
|
||||||
|
Log.d("ArtistSync", "Callback triggered with songs: " + (songs != null ? songs.size() : 0));
|
||||||
|
if (songs != null && !songs.isEmpty()) {
|
||||||
|
Log.d("ArtistSync", "Starting download of " + songs.size() + " songs");
|
||||||
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
|
MappingUtil.mapDownloads(songs),
|
||||||
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
Log.d("ArtistSync", "Download started successfully");
|
||||||
|
} else {
|
||||||
|
Log.d("ArtistSync", "No songs to download");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Log.d("ArtistSync", "Artist sync preference is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
@ -8,10 +9,13 @@ import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.model.DownloadStack;
|
import com.cappielloantonio.tempo.model.DownloadStack;
|
||||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -25,6 +29,7 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Integer> refreshResult = new MutableLiveData<>();
|
||||||
|
|
||||||
public DownloadViewModel(@NonNull Application application) {
|
public DownloadViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
@ -43,6 +48,10 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||||
return viewStack;
|
return viewStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Integer> getRefreshResult() {
|
||||||
|
return refreshResult;
|
||||||
|
}
|
||||||
|
|
||||||
public void initViewStack(DownloadStack level) {
|
public void initViewStack(DownloadStack level) {
|
||||||
ArrayList<DownloadStack> stack = new ArrayList<>();
|
ArrayList<DownloadStack> stack = new ArrayList<>();
|
||||||
stack.add(level);
|
stack.add(level);
|
||||||
|
|
@ -60,4 +69,59 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||||
stack.remove(stack.size() - 1);
|
stack.remove(stack.size() - 1);
|
||||||
viewStack.setValue(stack);
|
viewStack.setValue(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refreshExternalDownloads() {
|
||||||
|
new Thread(() -> {
|
||||||
|
String directoryUri = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (directoryUri == null) {
|
||||||
|
refreshResult.postValue(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Download> downloads = downloadRepository.getAllDownloads();
|
||||||
|
if (downloads == null || downloads.isEmpty()) {
|
||||||
|
refreshResult.postValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Download> toRemove = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Download download : downloads) {
|
||||||
|
String uriString = download.getDownloadUri();
|
||||||
|
if (uriString == null || uriString.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = Uri.parse(uriString);
|
||||||
|
if (uri.getScheme() == null || !uri.getScheme().equalsIgnoreCase("content")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile file;
|
||||||
|
try {
|
||||||
|
file = DocumentFile.fromSingleUri(getApplication(), uri);
|
||||||
|
} catch (SecurityException exception) {
|
||||||
|
file = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null || !file.exists()) {
|
||||||
|
toRemove.add(download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toRemove.isEmpty()) {
|
||||||
|
ArrayList<String> ids = new ArrayList<>();
|
||||||
|
for (Download download : toRemove) {
|
||||||
|
ids.add(download.getId());
|
||||||
|
ExternalAudioReader.removeMetadata(download);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadRepository.delete(ids);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
refreshResult.postValue(ids.size());
|
||||||
|
} else {
|
||||||
|
refreshResult.postValue(0);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ public class HomeViewModel extends AndroidViewModel {
|
||||||
private final SharingRepository sharingRepository;
|
private final SharingRepository sharingRepository;
|
||||||
|
|
||||||
private final StarredAlbumsSyncViewModel albumsSyncViewModel;
|
private final StarredAlbumsSyncViewModel albumsSyncViewModel;
|
||||||
|
private final StarredArtistsSyncViewModel artistSyncViewModel;
|
||||||
|
|
||||||
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
|
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
|
||||||
|
|
@ -85,6 +86,7 @@ public class HomeViewModel extends AndroidViewModel {
|
||||||
sharingRepository = new SharingRepository();
|
sharingRepository = new SharingRepository();
|
||||||
|
|
||||||
albumsSyncViewModel = new StarredAlbumsSyncViewModel(application);
|
albumsSyncViewModel = new StarredAlbumsSyncViewModel(application);
|
||||||
|
artistSyncViewModel = new StarredArtistsSyncViewModel(application);
|
||||||
|
|
||||||
setOfflineFavorite();
|
setOfflineFavorite();
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +176,10 @@ public class HomeViewModel extends AndroidViewModel {
|
||||||
return albumsSyncViewModel.getAllStarredAlbumSongs();
|
return albumsSyncViewModel.getAllStarredAlbumSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Child>> getAllStarredArtistSongs() {
|
||||||
|
return artistSyncViewModel.getAllStarredArtistSongs();
|
||||||
|
}
|
||||||
|
|
||||||
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
||||||
if (starredArtists.getValue() == null) {
|
if (starredArtists.getValue() == null) {
|
||||||
artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue);
|
artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,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 androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
|
@ -9,14 +10,17 @@ import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
import com.cappielloantonio.tempo.model.Queue;
|
import com.cappielloantonio.tempo.model.Queue;
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.LyricsRepository;
|
||||||
import com.cappielloantonio.tempo.repository.OpenRepository;
|
import com.cappielloantonio.tempo.repository.OpenRepository;
|
||||||
import com.cappielloantonio.tempo.repository.QueueRepository;
|
import com.cappielloantonio.tempo.repository.QueueRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
|
|
@ -31,6 +35,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -47,14 +52,20 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
private final QueueRepository queueRepository;
|
private final QueueRepository queueRepository;
|
||||||
private final FavoriteRepository favoriteRepository;
|
private final FavoriteRepository favoriteRepository;
|
||||||
private final OpenRepository openRepository;
|
private final OpenRepository openRepository;
|
||||||
|
private final LyricsRepository lyricsRepository;
|
||||||
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> lyricsCachedLiveData = new MutableLiveData<>(false);
|
||||||
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
||||||
|
private final Gson gson = new Gson();
|
||||||
private boolean lyricsSyncState = true;
|
private boolean lyricsSyncState = true;
|
||||||
|
private LiveData<LyricsCache> cachedLyricsSource;
|
||||||
|
private String currentSongId;
|
||||||
|
private final Observer<LyricsCache> cachedLyricsObserver = this::onCachedLyricsChanged;
|
||||||
|
|
||||||
|
|
||||||
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
||||||
|
|
@ -66,6 +77,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
queueRepository = new QueueRepository();
|
queueRepository = new QueueRepository();
|
||||||
favoriteRepository = new FavoriteRepository();
|
favoriteRepository = new FavoriteRepository();
|
||||||
openRepository = new OpenRepository();
|
openRepository = new OpenRepository();
|
||||||
|
lyricsRepository = new LyricsRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Queue>> getQueueSong() {
|
public LiveData<List<Queue>> getQueueSong() {
|
||||||
|
|
@ -122,7 +134,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
media.setStarred(new Date());
|
media.setStarred(new Date());
|
||||||
|
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownload(media),
|
MappingUtil.mapDownload(media),
|
||||||
new Download(media)
|
new Download(media)
|
||||||
|
|
@ -139,12 +151,49 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
|
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
|
||||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
|
||||||
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue);
|
|
||||||
lyricsLiveData.postValue(null);
|
lyricsLiveData.postValue(null);
|
||||||
} else {
|
|
||||||
songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue);
|
|
||||||
lyricsListLiveData.postValue(null);
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsCachedLiveData.postValue(false);
|
||||||
|
|
||||||
|
clearCachedLyricsObserver();
|
||||||
|
|
||||||
|
String songId = media != null ? media.getId() : currentSongId;
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(songId) || owner == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSongId = songId;
|
||||||
|
|
||||||
|
observeCachedLyrics(owner, songId);
|
||||||
|
|
||||||
|
LyricsCache cachedLyrics = lyricsRepository.getLyrics(songId);
|
||||||
|
if (cachedLyrics != null) {
|
||||||
|
onCachedLyricsChanged(cachedLyrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetworkUtil.isOffline() || media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||||
|
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsList -> {
|
||||||
|
lyricsListLiveData.postValue(lyricsList);
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
|
||||||
|
if (shouldAutoDownloadLyrics() && hasStructuredLyrics(lyricsList)) {
|
||||||
|
saveLyricsToCache(media, null, lyricsList);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
songRepository.getSongLyrics(media).observe(owner, lyrics -> {
|
||||||
|
lyricsLiveData.postValue(lyrics);
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
|
||||||
|
if (shouldAutoDownloadLyrics() && !TextUtils.isEmpty(lyrics)) {
|
||||||
|
saveLyricsToCache(media, lyrics, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +202,17 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) {
|
public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) {
|
||||||
|
currentSongId = mediaId;
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(mediaId)) {
|
||||||
|
refreshMediaInfo(owner, null);
|
||||||
|
} else {
|
||||||
|
clearCachedLyricsObserver();
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsCachedLiveData.postValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaType != null) {
|
if (mediaType != null) {
|
||||||
switch (mediaType) {
|
switch (mediaType) {
|
||||||
case Constants.MEDIA_TYPE_MUSIC:
|
case Constants.MEDIA_TYPE_MUSIC:
|
||||||
|
|
@ -162,7 +222,12 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
case Constants.MEDIA_TYPE_PODCAST:
|
case Constants.MEDIA_TYPE_PODCAST:
|
||||||
liveMedia.postValue(null);
|
liveMedia.postValue(null);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
liveMedia.postValue(null);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
liveMedia.postValue(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +298,105 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
||||||
|
if (TextUtils.isEmpty(songId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedLyricsSource = lyricsRepository.observeLyrics(songId);
|
||||||
|
cachedLyricsSource.observe(owner, cachedLyricsObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearCachedLyricsObserver() {
|
||||||
|
if (cachedLyricsSource != null) {
|
||||||
|
cachedLyricsSource.removeObserver(cachedLyricsObserver);
|
||||||
|
cachedLyricsSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCachedLyricsChanged(LyricsCache lyricsCache) {
|
||||||
|
if (lyricsCache == null) {
|
||||||
|
lyricsCachedLiveData.postValue(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricsCachedLiveData.postValue(true);
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(lyricsCache.getStructuredLyrics())) {
|
||||||
|
try {
|
||||||
|
LyricsList cachedList = gson.fromJson(lyricsCache.getStructuredLyrics(), LyricsList.class);
|
||||||
|
lyricsListLiveData.postValue(cachedList);
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsLiveData.postValue(lyricsCache.getLyrics());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsLiveData.postValue(lyricsCache.getLyrics());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveLyricsToCache(Child media, String lyrics, LyricsList lyricsList) {
|
||||||
|
if (media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsCache lyricsCache = new LyricsCache(media.getId());
|
||||||
|
lyricsCache.setArtist(media.getArtist());
|
||||||
|
lyricsCache.setTitle(media.getTitle());
|
||||||
|
lyricsCache.setUpdatedAt(System.currentTimeMillis());
|
||||||
|
|
||||||
|
if (lyricsList != null && hasStructuredLyrics(lyricsList)) {
|
||||||
|
lyricsCache.setStructuredLyrics(gson.toJson(lyricsList));
|
||||||
|
lyricsCache.setLyrics(null);
|
||||||
|
} else {
|
||||||
|
lyricsCache.setLyrics(lyrics);
|
||||||
|
lyricsCache.setStructuredLyrics(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricsRepository.insert(lyricsCache);
|
||||||
|
lyricsCachedLiveData.postValue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||||
|
return lyricsList != null
|
||||||
|
&& lyricsList.getStructuredLyrics() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldAutoDownloadLyrics() {
|
||||||
|
return Preferences.isAutoDownloadLyricsEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean downloadCurrentLyrics() {
|
||||||
|
Child media = getLiveMedia().getValue();
|
||||||
|
if (media == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsList lyricsList = lyricsListLiveData.getValue();
|
||||||
|
String lyrics = lyricsLiveData.getValue();
|
||||||
|
|
||||||
|
if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLyricsToCache(media, lyrics, lyricsList);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getLyricsCachedState() {
|
||||||
|
return lyricsCachedLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
public void changeSyncLyricsState() {
|
public void changeSyncLyricsState() {
|
||||||
lyricsSyncState = !lyricsSyncState;
|
lyricsSyncState = !lyricsSyncState;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
|
@ -11,10 +13,10 @@ import androidx.lifecycle.MutableLiveData;
|
||||||
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PlaylistChooserViewModel extends AndroidViewModel {
|
public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
|
|
@ -34,8 +36,21 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
return playlists;
|
return playlists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongsToPlaylist(String playlistId) {
|
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
||||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(Lists.transform(toAdd, Child::getId)));
|
List<String> songIds = Lists.transform(toAdd, Child::getId);
|
||||||
|
if (Preferences.allowPlaylistDuplicates()) {
|
||||||
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
||||||
|
dialog.dismiss();
|
||||||
|
} else {
|
||||||
|
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
|
||||||
|
if (playlistSongs != null) {
|
||||||
|
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
||||||
|
songIds.removeAll(playlistSongIds);
|
||||||
|
}
|
||||||
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSongsToAdd(ArrayList<Child> songs) {
|
public void setSongsToAdd(ArrayList<Child> songs) {
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
media.setStarred(new Date());
|
media.setStarred(new Date());
|
||||||
|
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownload(media),
|
MappingUtil.mapDownload(media),
|
||||||
new Download(media)
|
new Download(media)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public class StarredArtistsSyncViewModel extends AndroidViewModel {
|
||||||
|
private final ArtistRepository artistRepository;
|
||||||
|
|
||||||
|
private final MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<List<Child>> starredArtistSongs = new MutableLiveData<>(null);
|
||||||
|
|
||||||
|
public StarredArtistsSyncViewModel(@NonNull Application application) {
|
||||||
|
super(application);
|
||||||
|
artistRepository = new ArtistRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
||||||
|
artistRepository.getStarredArtists(false, -1).observe(owner, starredArtists::postValue);
|
||||||
|
return starredArtists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Child>> getAllStarredArtistSongs() {
|
||||||
|
artistRepository.getStarredArtists(false, -1).observeForever(new Observer<List<ArtistID3>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<ArtistID3> artists) {
|
||||||
|
if (artists != null && !artists.isEmpty()) {
|
||||||
|
collectAllArtistSongs(artists, starredArtistSongs::postValue);
|
||||||
|
} else {
|
||||||
|
starredArtistSongs.postValue(new ArrayList<>());
|
||||||
|
}
|
||||||
|
artistRepository.getStarredArtists(false, -1).removeObserver(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return starredArtistSongs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Child>> getStarredArtistSongs(Activity activity) {
|
||||||
|
artistRepository.getStarredArtists(false, -1).observe((LifecycleOwner) activity, artists -> {
|
||||||
|
if (artists != null && !artists.isEmpty()) {
|
||||||
|
collectAllArtistSongs(artists, starredArtistSongs::postValue);
|
||||||
|
} else {
|
||||||
|
starredArtistSongs.postValue(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return starredArtistSongs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectAllArtistSongs(List<ArtistID3> artists, ArtistSongsCallback callback) {
|
||||||
|
if (artists == null || artists.isEmpty()) {
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Child> allSongs = new ArrayList<>();
|
||||||
|
AtomicInteger remainingArtists = new AtomicInteger(artists.size());
|
||||||
|
|
||||||
|
for (ArtistID3 artist : artists) {
|
||||||
|
artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSongsCollected(List<Child> songs) {
|
||||||
|
if (songs != null) {
|
||||||
|
allSongs.addAll(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
int remaining = remainingArtists.decrementAndGet();
|
||||||
|
if (remaining == 0) {
|
||||||
|
callback.onSongsCollected(allSongs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ArtistSongsCallback {
|
||||||
|
void onSongsCollected(List<Child> songs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.cappielloantonio.tempo.widget;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.session.MediaController;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
public final class WidgetActions {
|
||||||
|
public static void dispatchToMediaSession(Context ctx, String action) {
|
||||||
|
Log.d("TempoWidget", "dispatch action=" + action);
|
||||||
|
Context appCtx = ctx.getApplicationContext();
|
||||||
|
SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class));
|
||||||
|
ListenableFuture<MediaController> future = new MediaController.Builder(appCtx, token).buildAsync();
|
||||||
|
future.addListener(() -> {
|
||||||
|
try {
|
||||||
|
if (!future.isDone()) return;
|
||||||
|
MediaController c = future.get();
|
||||||
|
Log.d("TempoWidget", "controller connected, isPlaying=" + c.isPlaying());
|
||||||
|
switch (action) {
|
||||||
|
case WidgetProvider.ACT_PLAY_PAUSE:
|
||||||
|
if (c.isPlaying()) c.pause();
|
||||||
|
else c.play();
|
||||||
|
break;
|
||||||
|
case WidgetProvider.ACT_NEXT:
|
||||||
|
c.seekToNext();
|
||||||
|
break;
|
||||||
|
case WidgetProvider.ACT_PREV:
|
||||||
|
c.seekToPrevious();
|
||||||
|
break;
|
||||||
|
case WidgetProvider.ACT_TOGGLE_SHUFFLE:
|
||||||
|
c.setShuffleModeEnabled(!c.getShuffleModeEnabled());
|
||||||
|
break;
|
||||||
|
case WidgetProvider.ACT_CYCLE_REPEAT:
|
||||||
|
int repeatMode = c.getRepeatMode();
|
||||||
|
int nextMode;
|
||||||
|
if (repeatMode == Player.REPEAT_MODE_OFF) {
|
||||||
|
nextMode = Player.REPEAT_MODE_ALL;
|
||||||
|
} else if (repeatMode == Player.REPEAT_MODE_ALL) {
|
||||||
|
nextMode = Player.REPEAT_MODE_ONE;
|
||||||
|
} else {
|
||||||
|
nextMode = Player.REPEAT_MODE_OFF;
|
||||||
|
}
|
||||||
|
c.setRepeatMode(nextMode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
WidgetUpdateManager.refreshFromController(ctx);
|
||||||
|
c.release();
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
Log.e("TempoWidget", "dispatch failed", e);
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package com.cappielloantonio.tempo.widget;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.appwidget.AppWidgetManager;
|
||||||
|
import android.appwidget.AppWidgetProvider;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
||||||
|
import android.app.TaskStackBuilder;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class WidgetProvider extends AppWidgetProvider {
|
||||||
|
private static final String TAG = "TempoWidget";
|
||||||
|
public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE";
|
||||||
|
public static final String ACT_NEXT = "tempo.widget.NEXT";
|
||||||
|
public static final String ACT_PREV = "tempo.widget.PREV";
|
||||||
|
public static final String ACT_TOGGLE_SHUFFLE = "tempo.widget.SHUFFLE";
|
||||||
|
public static final String ACT_CYCLE_REPEAT = "tempo.widget.REPEAT";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
|
||||||
|
for (int id : ids) {
|
||||||
|
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
|
||||||
|
attachIntents(ctx, rv, id);
|
||||||
|
mgr.updateAppWidget(id, rv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context ctx, Intent intent) {
|
||||||
|
super.onReceive(ctx, intent);
|
||||||
|
String a = intent.getAction();
|
||||||
|
Log.d(TAG, "onReceive action=" + a);
|
||||||
|
if (ACT_PLAY_PAUSE.equals(a) || ACT_NEXT.equals(a) || ACT_PREV.equals(a)
|
||||||
|
|| ACT_TOGGLE_SHUFFLE.equals(a) || ACT_CYCLE_REPEAT.equals(a)) {
|
||||||
|
WidgetActions.dispatchToMediaSession(ctx, a);
|
||||||
|
} else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(a)) {
|
||||||
|
WidgetUpdateManager.refreshFromController(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) {
|
||||||
|
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||||
|
RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId);
|
||||||
|
attachIntents(context, rv, appWidgetId);
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, rv);
|
||||||
|
WidgetUpdateManager.refreshFromController(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void attachIntents(Context ctx, RemoteViews rv) {
|
||||||
|
attachIntents(ctx, rv, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) {
|
||||||
|
PendingIntent playPause = PendingIntent.getBroadcast(
|
||||||
|
ctx,
|
||||||
|
requestCodeBase + 0,
|
||||||
|
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PLAY_PAUSE),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
);
|
||||||
|
PendingIntent next = PendingIntent.getBroadcast(
|
||||||
|
ctx,
|
||||||
|
requestCodeBase + 1,
|
||||||
|
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_NEXT),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
);
|
||||||
|
PendingIntent prev = PendingIntent.getBroadcast(
|
||||||
|
ctx,
|
||||||
|
requestCodeBase + 2,
|
||||||
|
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PREV),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
);
|
||||||
|
PendingIntent shuffle = PendingIntent.getBroadcast(
|
||||||
|
ctx,
|
||||||
|
requestCodeBase + 3,
|
||||||
|
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_TOGGLE_SHUFFLE),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
);
|
||||||
|
PendingIntent repeat = PendingIntent.getBroadcast(
|
||||||
|
ctx,
|
||||||
|
requestCodeBase + 4,
|
||||||
|
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_CYCLE_REPEAT),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
);
|
||||||
|
|
||||||
|
rv.setOnClickPendingIntent(R.id.btn_play_pause, playPause);
|
||||||
|
rv.setOnClickPendingIntent(R.id.btn_next, next);
|
||||||
|
rv.setOnClickPendingIntent(R.id.btn_prev, prev);
|
||||||
|
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
|
||||||
|
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
|
||||||
|
|
||||||
|
PendingIntent launch = TaskStackBuilder.create(ctx)
|
||||||
|
.addNextIntentWithParentStack(new Intent(ctx, MainActivity.class))
|
||||||
|
.getPendingIntent(requestCodeBase + 10, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
rv.setOnClickPendingIntent(R.id.root, launch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.cappielloantonio.tempo.widget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppWidget provider entry for the 4x1 widget card. Inherits all behavior
|
||||||
|
* from {@link WidgetProvider}.
|
||||||
|
*/
|
||||||
|
public class WidgetProvider4x1 extends WidgetProvider {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
package com.cappielloantonio.tempo.widget;
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.session.MediaController;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
public final class WidgetUpdateManager {
|
||||||
|
|
||||||
|
public static void updateFromState(Context ctx,
|
||||||
|
String title,
|
||||||
|
String artist,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode,
|
||||||
|
long positionMs,
|
||||||
|
long durationMs) {
|
||||||
|
if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing);
|
||||||
|
if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle);
|
||||||
|
if (TextUtils.isEmpty(album)) album = "";
|
||||||
|
|
||||||
|
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
|
||||||
|
|
||||||
|
AppWidgetManager mgr = AppWidgetManager.getInstance(ctx);
|
||||||
|
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
|
||||||
|
for (int id : ids) {
|
||||||
|
android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing,
|
||||||
|
timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id);
|
||||||
|
WidgetProvider.attachIntents(ctx, rv, id);
|
||||||
|
mgr.updateAppWidget(id, rv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pushNow(Context ctx) {
|
||||||
|
AppWidgetManager mgr = AppWidgetManager.getInstance(ctx);
|
||||||
|
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
|
||||||
|
for (int id : ids) {
|
||||||
|
android.widget.RemoteViews rv = chooseBuild(ctx, id);
|
||||||
|
WidgetProvider.attachIntents(ctx, rv, id);
|
||||||
|
mgr.updateAppWidget(id, rv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateFromState(Context ctx,
|
||||||
|
String title,
|
||||||
|
String artist,
|
||||||
|
String album,
|
||||||
|
String coverArtId,
|
||||||
|
boolean playing,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode,
|
||||||
|
long positionMs,
|
||||||
|
long durationMs) {
|
||||||
|
final Context appCtx = ctx.getApplicationContext();
|
||||||
|
final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title;
|
||||||
|
final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist;
|
||||||
|
final String alb = !TextUtils.isEmpty(album) ? album : "";
|
||||||
|
final boolean p = playing;
|
||||||
|
final boolean sh = shuffleEnabled;
|
||||||
|
final int rep = repeatMode;
|
||||||
|
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(coverArtId)) {
|
||||||
|
CustomGlideRequest.loadAlbumArtBitmap(
|
||||||
|
appCtx,
|
||||||
|
coverArtId,
|
||||||
|
com.cappielloantonio.tempo.util.Preferences.getImageSize(),
|
||||||
|
new CustomTarget<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
|
||||||
|
AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx);
|
||||||
|
int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class));
|
||||||
|
for (int id : ids) {
|
||||||
|
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p,
|
||||||
|
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||||
|
WidgetProvider.attachIntents(appCtx, rv, id);
|
||||||
|
mgr.updateAppWidget(id, rv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCleared(Drawable placeholder) {
|
||||||
|
AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx);
|
||||||
|
int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class));
|
||||||
|
for (int id : ids) {
|
||||||
|
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
||||||
|
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||||
|
WidgetProvider.attachIntents(appCtx, rv, id);
|
||||||
|
mgr.updateAppWidget(id, rv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx);
|
||||||
|
int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class));
|
||||||
|
for (int id : ids) {
|
||||||
|
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
||||||
|
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||||
|
WidgetProvider.attachIntents(appCtx, rv, id);
|
||||||
|
mgr.updateAppWidget(id, rv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void refreshFromController(Context ctx) {
|
||||||
|
final Context appCtx = ctx.getApplicationContext();
|
||||||
|
SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class));
|
||||||
|
ListenableFuture<MediaController> future = new MediaController.Builder(appCtx, token).buildAsync();
|
||||||
|
future.addListener(() -> {
|
||||||
|
try {
|
||||||
|
if (!future.isDone()) return;
|
||||||
|
MediaController c = future.get();
|
||||||
|
androidx.media3.common.MediaItem mi = c.getCurrentMediaItem();
|
||||||
|
String title = null, artist = null, album = null, coverId = null;
|
||||||
|
if (mi != null && mi.mediaMetadata != null) {
|
||||||
|
if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString();
|
||||||
|
if (mi.mediaMetadata.artist != null)
|
||||||
|
artist = mi.mediaMetadata.artist.toString();
|
||||||
|
if (mi.mediaMetadata.albumTitle != null)
|
||||||
|
album = mi.mediaMetadata.albumTitle.toString();
|
||||||
|
if (mi.mediaMetadata.extras != null) {
|
||||||
|
if (title == null) title = mi.mediaMetadata.extras.getString("title");
|
||||||
|
if (artist == null) artist = mi.mediaMetadata.extras.getString("artist");
|
||||||
|
if (album == null) album = mi.mediaMetadata.extras.getString("album");
|
||||||
|
coverId = mi.mediaMetadata.extras.getString("coverArtId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long position = c.getCurrentPosition();
|
||||||
|
long duration = c.getDuration();
|
||||||
|
if (position == C.TIME_UNSET) position = 0;
|
||||||
|
if (duration == C.TIME_UNSET) duration = 0;
|
||||||
|
updateFromState(appCtx,
|
||||||
|
title != null ? title : appCtx.getString(R.string.widget_not_playing),
|
||||||
|
artist != null ? artist : appCtx.getString(R.string.widget_placeholder_subtitle),
|
||||||
|
album,
|
||||||
|
coverId,
|
||||||
|
c.isPlaying(),
|
||||||
|
c.getShuffleModeEnabled(),
|
||||||
|
c.getRepeatMode(),
|
||||||
|
position,
|
||||||
|
duration);
|
||||||
|
c.release();
|
||||||
|
} catch (ExecutionException | InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimingInfo createTimingInfo(long positionMs, long durationMs) {
|
||||||
|
long safePosition = Math.max(0L, positionMs);
|
||||||
|
long safeDuration = durationMs > 0 ? durationMs : 0L;
|
||||||
|
if (safeDuration > 0 && safePosition > safeDuration) {
|
||||||
|
safePosition = safeDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
String elapsed = (safeDuration > 0 || safePosition > 0)
|
||||||
|
? MusicUtil.getReadableDurationString(safePosition, true)
|
||||||
|
: null;
|
||||||
|
String total = safeDuration > 0
|
||||||
|
? MusicUtil.getReadableDurationString(safeDuration, true)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
int progress = 0;
|
||||||
|
if (safeDuration > 0) {
|
||||||
|
long scaled = safePosition * WidgetViewsFactory.PROGRESS_MAX;
|
||||||
|
long progressLong = scaled / safeDuration;
|
||||||
|
if (progressLong < 0) {
|
||||||
|
progress = 0;
|
||||||
|
} else if (progressLong > WidgetViewsFactory.PROGRESS_MAX) {
|
||||||
|
progress = WidgetViewsFactory.PROGRESS_MAX;
|
||||||
|
} else {
|
||||||
|
progress = (int) progressLong;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TimingInfo(elapsed, total, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static android.widget.RemoteViews chooseBuild(Context ctx, int appWidgetId) {
|
||||||
|
LayoutSize size = resolveLayoutSize(ctx, appWidgetId);
|
||||||
|
switch (size) {
|
||||||
|
case MEDIUM:
|
||||||
|
return WidgetViewsFactory.buildMedium(ctx);
|
||||||
|
case LARGE:
|
||||||
|
return WidgetViewsFactory.buildLarge(ctx);
|
||||||
|
case EXPANDED:
|
||||||
|
return WidgetViewsFactory.buildExpanded(ctx);
|
||||||
|
case COMPACT:
|
||||||
|
default:
|
||||||
|
return WidgetViewsFactory.buildCompact(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static android.widget.RemoteViews choosePopulate(Context ctx,
|
||||||
|
String title,
|
||||||
|
String artist,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
String elapsedText,
|
||||||
|
String totalText,
|
||||||
|
int progress,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode,
|
||||||
|
int appWidgetId) {
|
||||||
|
LayoutSize size = resolveLayoutSize(ctx, appWidgetId);
|
||||||
|
switch (size) {
|
||||||
|
case MEDIUM:
|
||||||
|
return WidgetViewsFactory.populateMedium(ctx, title, artist, album, art, playing,
|
||||||
|
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||||
|
case LARGE:
|
||||||
|
return WidgetViewsFactory.populateLarge(ctx, title, artist, album, art, playing,
|
||||||
|
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||||
|
case EXPANDED:
|
||||||
|
return WidgetViewsFactory.populateExpanded(ctx, title, artist, album, art, playing,
|
||||||
|
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||||
|
case COMPACT:
|
||||||
|
default:
|
||||||
|
return WidgetViewsFactory.populateCompact(ctx, title, artist, album, art, playing,
|
||||||
|
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LayoutSize resolveLayoutSize(Context ctx, int appWidgetId) {
|
||||||
|
AppWidgetManager mgr = AppWidgetManager.getInstance(ctx);
|
||||||
|
android.os.Bundle opts = mgr.getAppWidgetOptions(appWidgetId);
|
||||||
|
int minH = opts != null ? opts.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0;
|
||||||
|
int expandedThreshold = ctx.getResources().getInteger(R.integer.widget_expanded_min_height_dp);
|
||||||
|
int largeThreshold = ctx.getResources().getInteger(R.integer.widget_large_min_height_dp);
|
||||||
|
int mediumThreshold = ctx.getResources().getInteger(R.integer.widget_medium_min_height_dp);
|
||||||
|
if (minH >= expandedThreshold) return LayoutSize.EXPANDED;
|
||||||
|
if (minH >= largeThreshold) return LayoutSize.LARGE;
|
||||||
|
if (minH >= mediumThreshold) return LayoutSize.MEDIUM;
|
||||||
|
return LayoutSize.COMPACT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LayoutSize {
|
||||||
|
COMPACT,
|
||||||
|
MEDIUM,
|
||||||
|
LARGE,
|
||||||
|
EXPANDED
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TimingInfo {
|
||||||
|
final String elapsedText;
|
||||||
|
final String totalText;
|
||||||
|
final int progress;
|
||||||
|
|
||||||
|
TimingInfo(String elapsedText, String totalText, int progress) {
|
||||||
|
this.elapsedText = elapsedText;
|
||||||
|
this.totalText = totalText;
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
package com.cappielloantonio.tempo.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapShader;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
||||||
|
public final class WidgetViewsFactory {
|
||||||
|
|
||||||
|
static final int PROGRESS_MAX = 1000;
|
||||||
|
private static final float ALBUM_ART_CORNER_RADIUS_DP = 6f;
|
||||||
|
|
||||||
|
private WidgetViewsFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews buildCompact(Context ctx) {
|
||||||
|
return build(ctx, R.layout.widget_layout_compact, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews buildMedium(Context ctx) {
|
||||||
|
return build(ctx, R.layout.widget_layout_medium, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews buildLarge(Context ctx) {
|
||||||
|
return build(ctx, R.layout.widget_layout_large_short, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews buildExpanded(Context ctx) {
|
||||||
|
return build(ctx, R.layout.widget_layout_large, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RemoteViews build(Context ctx,
|
||||||
|
int layoutRes,
|
||||||
|
boolean showAlbum,
|
||||||
|
boolean showSecondaryControls) {
|
||||||
|
RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes);
|
||||||
|
rv.setTextViewText(R.id.title, ctx.getString(R.string.widget_not_playing));
|
||||||
|
rv.setTextViewText(R.id.subtitle, ctx.getString(R.string.widget_placeholder_subtitle));
|
||||||
|
rv.setTextViewText(R.id.album, "");
|
||||||
|
rv.setViewVisibility(R.id.album, showAlbum ? View.INVISIBLE : View.GONE);
|
||||||
|
rv.setTextViewText(R.id.time_elapsed, ctx.getString(R.string.widget_time_elapsed_placeholder));
|
||||||
|
rv.setTextViewText(R.id.time_total, ctx.getString(R.string.widget_time_duration_placeholder));
|
||||||
|
rv.setProgressBar(R.id.progress, PROGRESS_MAX, 0, false);
|
||||||
|
rv.setImageViewResource(R.id.btn_play_pause, R.drawable.ic_play);
|
||||||
|
rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo);
|
||||||
|
applySecondaryControlsDefaults(ctx, rv, showSecondaryControls);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applySecondaryControlsDefaults(Context ctx,
|
||||||
|
RemoteViews rv,
|
||||||
|
boolean show) {
|
||||||
|
int visibility = show ? View.VISIBLE : View.GONE;
|
||||||
|
rv.setViewVisibility(R.id.controls_secondary, visibility);
|
||||||
|
rv.setViewVisibility(R.id.btn_shuffle, visibility);
|
||||||
|
rv.setViewVisibility(R.id.btn_repeat, visibility);
|
||||||
|
if (show) {
|
||||||
|
int defaultColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint);
|
||||||
|
rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle);
|
||||||
|
rv.setImageViewResource(R.id.btn_repeat, R.drawable.ic_repeat);
|
||||||
|
rv.setInt(R.id.btn_shuffle, "setColorFilter", defaultColor);
|
||||||
|
rv.setInt(R.id.btn_repeat, "setColorFilter", defaultColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews populateCompact(Context ctx,
|
||||||
|
String title,
|
||||||
|
String subtitle,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
String elapsedText,
|
||||||
|
String totalText,
|
||||||
|
int progress,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode) {
|
||||||
|
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||||
|
progress, R.layout.widget_layout_compact, false, false, shuffleEnabled, repeatMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews populateMedium(Context ctx,
|
||||||
|
String title,
|
||||||
|
String subtitle,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
String elapsedText,
|
||||||
|
String totalText,
|
||||||
|
int progress,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode) {
|
||||||
|
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||||
|
progress, R.layout.widget_layout_medium, true, true, shuffleEnabled, repeatMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews populateLarge(Context ctx,
|
||||||
|
String title,
|
||||||
|
String subtitle,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
String elapsedText,
|
||||||
|
String totalText,
|
||||||
|
int progress,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode) {
|
||||||
|
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||||
|
progress, R.layout.widget_layout_large_short, true, true, shuffleEnabled, repeatMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RemoteViews populateExpanded(Context ctx,
|
||||||
|
String title,
|
||||||
|
String subtitle,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
String elapsedText,
|
||||||
|
String totalText,
|
||||||
|
int progress,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode) {
|
||||||
|
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||||
|
progress, R.layout.widget_layout_large, true, true, shuffleEnabled, repeatMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RemoteViews populateWithLayout(Context ctx,
|
||||||
|
String title,
|
||||||
|
String subtitle,
|
||||||
|
String album,
|
||||||
|
Bitmap art,
|
||||||
|
boolean playing,
|
||||||
|
String elapsedText,
|
||||||
|
String totalText,
|
||||||
|
int progress,
|
||||||
|
int layoutRes,
|
||||||
|
boolean showAlbum,
|
||||||
|
boolean showSecondaryControls,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode) {
|
||||||
|
RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes);
|
||||||
|
rv.setTextViewText(R.id.title, title);
|
||||||
|
rv.setTextViewText(R.id.subtitle, subtitle);
|
||||||
|
|
||||||
|
if (showAlbum && !TextUtils.isEmpty(album)) {
|
||||||
|
rv.setTextViewText(R.id.album, album);
|
||||||
|
rv.setViewVisibility(R.id.album, View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
rv.setTextViewText(R.id.album, "");
|
||||||
|
rv.setViewVisibility(R.id.album, View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (art != null) {
|
||||||
|
Bitmap rounded = maybeRoundBitmap(ctx, art);
|
||||||
|
rv.setImageViewBitmap(R.id.album_art, rounded != null ? rounded : art);
|
||||||
|
} else {
|
||||||
|
rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.setImageViewResource(R.id.btn_play_pause,
|
||||||
|
playing ? R.drawable.ic_pause : R.drawable.ic_play);
|
||||||
|
|
||||||
|
String elapsed = !TextUtils.isEmpty(elapsedText)
|
||||||
|
? elapsedText
|
||||||
|
: ctx.getString(R.string.widget_time_elapsed_placeholder);
|
||||||
|
String total = !TextUtils.isEmpty(totalText)
|
||||||
|
? totalText
|
||||||
|
: ctx.getString(R.string.widget_time_duration_placeholder);
|
||||||
|
|
||||||
|
int safeProgress = progress;
|
||||||
|
if (safeProgress < 0) safeProgress = 0;
|
||||||
|
if (safeProgress > PROGRESS_MAX) safeProgress = PROGRESS_MAX;
|
||||||
|
|
||||||
|
rv.setTextViewText(R.id.time_elapsed, elapsed);
|
||||||
|
rv.setTextViewText(R.id.time_total, total);
|
||||||
|
rv.setProgressBar(R.id.progress, PROGRESS_MAX, safeProgress, false);
|
||||||
|
|
||||||
|
applySecondaryControls(ctx, rv, showSecondaryControls, shuffleEnabled, repeatMode);
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bitmap maybeRoundBitmap(Context ctx, Bitmap source) {
|
||||||
|
if (source == null || source.isRecycled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int width = source.getWidth();
|
||||||
|
int height = source.getHeight();
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(output);
|
||||||
|
|
||||||
|
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
|
||||||
|
|
||||||
|
float radiusPx = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
ALBUM_ART_CORNER_RADIUS_DP,
|
||||||
|
ctx.getResources().getDisplayMetrics());
|
||||||
|
float maxRadius = Math.min(width, height) / 2f;
|
||||||
|
float safeRadius = Math.min(radiusPx, maxRadius);
|
||||||
|
|
||||||
|
canvas.drawRoundRect(new RectF(0f, 0f, width, height), safeRadius, safeRadius, paint);
|
||||||
|
return output;
|
||||||
|
} catch (RuntimeException | OutOfMemoryError e) {
|
||||||
|
android.util.Log.w("TempoWidget", "Failed to round album art", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applySecondaryControls(Context ctx,
|
||||||
|
RemoteViews rv,
|
||||||
|
boolean show,
|
||||||
|
boolean shuffleEnabled,
|
||||||
|
int repeatMode) {
|
||||||
|
if (!show) {
|
||||||
|
rv.setViewVisibility(R.id.controls_secondary, View.GONE);
|
||||||
|
rv.setViewVisibility(R.id.btn_shuffle, View.GONE);
|
||||||
|
rv.setViewVisibility(R.id.btn_repeat, View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int inactiveColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint);
|
||||||
|
int activeColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint_active);
|
||||||
|
|
||||||
|
rv.setViewVisibility(R.id.controls_secondary, View.VISIBLE);
|
||||||
|
rv.setViewVisibility(R.id.btn_shuffle, View.VISIBLE);
|
||||||
|
rv.setViewVisibility(R.id.btn_repeat, View.VISIBLE);
|
||||||
|
rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle);
|
||||||
|
rv.setImageViewResource(R.id.btn_repeat,
|
||||||
|
repeatMode == Player.REPEAT_MODE_ONE ? R.drawable.ic_repeat_one : R.drawable.ic_repeat);
|
||||||
|
rv.setInt(R.id.btn_shuffle, "setColorFilter", shuffleEnabled ? activeColor : inactiveColor);
|
||||||
|
rv.setInt(R.id.btn_repeat, "setColorFilter",
|
||||||
|
repeatMode == Player.REPEAT_MODE_OFF ? inactiveColor : activeColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/src/main/res/drawable/ic_folder.xml
Normal file
9
app/src/main/res/drawable/ic_folder.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M10,4L12,6H20c1.1,0 2,0.9 2,2v10c0,1.1 -0.9,2 -2,2H4c-1.1,0 -2,-0.9 -2,-2V6c0,-1.1 0.9,-2 2,-2h6z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_refresh.xml
Normal file
9
app/src/main/res/drawable/ic_refresh.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -8,3.58 -8,8h2c0,-3.31 2.69,-6 6,-6 1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35zM19,12c0,3.31 -2.69,6 -6,6 -1.66,0 -3.14,-0.69 -4.22,-1.78L11,13H4v7l2.35,-2.35C7.8,19.1 9.79,20 12,20c4.42,0 8,-3.58 8,-8h-2z" />
|
||||||
|
</vector>
|
||||||
12
app/src/main/res/drawable/ic_repeat_one.xml
Normal file
12
app/src/main/res/drawable/ic_repeat_one.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17H7v-3l-4,4 4,4v-3h12v-6h-2v4z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M12,9h-2v2h1v6h2V9h-1z" />
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/drawable/widget_bg.xml
Normal file
6
app/src/main/res/drawable/widget_bg.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<solid android:color="@color/widget_bg" />
|
||||||
|
</shape>
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/playlist_dialog_recycler_view"
|
android:id="@+id/playlist_dialog_recycler_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:clipToPadding="false" />
|
android:clipToPadding="false" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
14
app/src/main/res/layout/dialog_starred_artist_sync.xml
Normal file
14
app/src/main/res/layout/dialog_starred_artist_sync.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:text="@string/starred_artist_sync_dialog_summary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/download_title_section"
|
android:text="@string/download_title_section"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/downloaded_go_back_image_view"
|
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
@ -94,6 +94,19 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/downloaded_refresh_image_view"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/ic_refresh"
|
||||||
|
android:contentDescription="@string/download_refresh_button_content_description"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/downloaded_go_back_image_view"
|
android:id="@+id/downloaded_go_back_image_view"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
|
|
@ -103,6 +116,7 @@
|
||||||
android:background="@drawable/ic_arrow_back"
|
android:background="@drawable/ic_arrow_back"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||||
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
|
||||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,98 @@
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Download/Sync starred artists -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/home_sync_starred_artists_card"
|
||||||
|
style="?attr/materialCardViewOutlinedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
|
||||||
|
<!-- Title, secondary and supporting text -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_sync_starred_artists_title"
|
||||||
|
style="@style/TitleLarge"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/home_sync_starred_artists_title"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:textFontWeight="600"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/vertical_guideline_artists"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_sync_starred_artists_subtitle"
|
||||||
|
style="@style/TitleMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/home_sync_starred_artists_subtitle"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_artists_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_sync_starred_artists_to_sync"
|
||||||
|
style="@style/TitleSmall"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:text="@string/home_sync_starred_artists_subtitle"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_artists_subtitle" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_artists_to_sync">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/home_sync_starred_artists_cancel"
|
||||||
|
style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/home_sync_starred_cancel" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/home_sync_starred_artists_download"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/home_sync_starred_download" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/vertical_guideline_artists"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="0.90" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Discover music -->
|
<!-- Discover music -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/home_discover_sector"
|
android:id="@+id/home_discover_sector"
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,25 @@
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/download_lyrics_button"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:alpha="0.7"
|
||||||
|
android:contentDescription="@string/player_lyrics_download_content_description"
|
||||||
|
android:insetLeft="0dp"
|
||||||
|
android:insetTop="0dp"
|
||||||
|
android:insetRight="0dp"
|
||||||
|
android:insetBottom="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cornerRadius="64dp"
|
||||||
|
app:icon="@drawable/ic_download"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/sync_lyrics_tap_button"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/now_playing_song_lyrics_sroll_view" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/sync_lyrics_tap_button"
|
android:id="@+id/sync_lyrics_tap_button"
|
||||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
|
|
|
||||||
175
app/src/main/res/layout/widget_layout_compact.xml
Normal file
175
app/src/main/res/layout/widget_layout_compact.xml
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/texts"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_toRightOf="@id/album_art"
|
||||||
|
android:layout_toEndOf="@id/album_art"
|
||||||
|
android:layout_toLeftOf="@id/controls"
|
||||||
|
android:layout_toStartOf="@id/controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:includeFontPadding="false"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:includeFontPadding="false"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
189
app/src/main/res/layout/widget_layout_large.xml
Normal file
189
app/src/main/res/layout/widget_layout_large.xml
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="200dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="150dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="6dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
198
app/src/main/res/layout/widget_layout_large_short.xml
Normal file
198
app/src/main/res/layout/widget_layout_large_short.xml
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="172dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/album_art_container"
|
||||||
|
android:layout_width="90dp"
|
||||||
|
android:layout_height="90dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="6dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
216
app/src/main/res/layout/widget_layout_medium.xml
Normal file
216
app/src/main/res/layout/widget_layout_medium.xml
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="120dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/album_art_container"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:weightSum="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/subtitle_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="1dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:gravity="end"
|
||||||
|
android:textAlignment="viewEnd"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="3dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
82
app/src/main/res/layout/widget_preview_compact.xml
Normal file
82
app/src/main/res/layout/widget_preview_compact.xml
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingTop="0dp"
|
||||||
|
android:paddingRight="0dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/ic_splash_logo"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/texts"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_toEndOf="@id/album_art"
|
||||||
|
android:layout_toStartOf="@id/controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:text="@string/widget_not_playing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:text="@string/widget_placeholder_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="48dp" android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"/>
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="48dp" android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"/>
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/btn_next"
|
||||||
|
android:layout_width="48dp" android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
@ -16,4 +16,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_download_group_by_year"
|
android:id="@+id/menu_download_group_by_year"
|
||||||
android:title="@string/menu_group_by_year" />
|
android:title="@string/menu_group_by_year" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_download_set_directory"
|
||||||
|
android:title="@string/download_directory_set" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|
@ -440,4 +440,10 @@
|
||||||
<item quantity="one">%d album à synchroniser</item>
|
<item quantity="one">%d album à synchroniser</item>
|
||||||
<item quantity="other">%d albums à synchroniser</item>
|
<item quantity="other">%d albums à synchroniser</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="equalizer_fragment_title">Égaliseur</string>
|
||||||
|
<string name="equalizer_reset">Réinitialiser</string>
|
||||||
|
<string name="equalizer_enable">Activer</string>
|
||||||
|
<string name="equalizer_not_supported">Non supporté sur cet appareil</string>
|
||||||
|
<string name="settings_app_equalizer">Égaliseur</string>
|
||||||
|
<string name="settings_app_equalizer_summary">Ouvrir l\'égaliseur intégré</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
7
app/src/main/res/values-night/colors_widget.xml
Normal file
7
app/src/main/res/values-night/colors_widget.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="widget_bg">#CC000000</color>
|
||||||
|
<color name="widget_title">#FFFFFFFF</color>
|
||||||
|
<color name="widget_subtitle">#B3FFFFFF</color>
|
||||||
|
<color name="widget_icon_tint">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="activity_battery_optimizations_summary">Wyłącz optymalizacje baterii aby odtwarzać media przy wyłączonym ekranie.</string>
|
<string name="activity_battery_optimizations_summary">Wyłącz optymalizacje baterii aby odtwarzać media przy wyłączonym ekranie.</string>
|
||||||
<string name="activity_battery_optimizations_title">Optymalizcje Baterii</string>
|
<string name="activity_battery_optimizations_title">Optymalizcje Baterii</string>
|
||||||
<string name="activity_info_offline_mode">Tryb offline</string>
|
<string name="activity_info_offline_mode">Tryb offline</string>
|
||||||
|
<string name="album_bottom_sheet_add_to_playlist">Dodaj do playlisty</string>
|
||||||
<string name="album_bottom_sheet_add_to_queue">Dodaj do kolejki</string>
|
<string name="album_bottom_sheet_add_to_queue">Dodaj do kolejki</string>
|
||||||
<string name="album_bottom_sheet_download_all">Pobierz wszystkie</string>
|
<string name="album_bottom_sheet_download_all">Pobierz wszystkie</string>
|
||||||
<string name="album_bottom_sheet_go_to_artist">Przejdź do wykonawcy</string>
|
<string name="album_bottom_sheet_go_to_artist">Przejdź do wykonawcy</string>
|
||||||
|
|
@ -89,6 +90,7 @@
|
||||||
<string name="exo_download_notification_channel_name">Pobieranie</string>
|
<string name="exo_download_notification_channel_name">Pobieranie</string>
|
||||||
<string name="filter_info_selection">Wybierz dwa lub więcej filtrów</string>
|
<string name="filter_info_selection">Wybierz dwa lub więcej filtrów</string>
|
||||||
<string name="filter_title">Filtry</string>
|
<string name="filter_title">Filtry</string>
|
||||||
|
<string name="filter_artist">Filtruj wykonawców</string>
|
||||||
<string name="filter_title_expanded">Filtruj Gatunki</string>
|
<string name="filter_title_expanded">Filtruj Gatunki</string>
|
||||||
<string name="generic_list_page_count">(%1$d)</string>
|
<string name="generic_list_page_count">(%1$d)</string>
|
||||||
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
||||||
|
|
@ -107,10 +109,6 @@
|
||||||
<string name="home_section_music">Muzyka</string>
|
<string name="home_section_music">Muzyka</string>
|
||||||
<string name="home_section_podcast">Podcasty</string>
|
<string name="home_section_podcast">Podcasty</string>
|
||||||
<string name="home_section_radio">Radio</string>
|
<string name="home_section_radio">Radio</string>
|
||||||
<string name="player_queue_save_queue_success">Zapisano kolejkę odtwarzania</string>
|
|
||||||
<string name="track_info_bit_depth">Głębia bitowa</string>
|
|
||||||
<string name="track_info_sampling_rate">Częstotliwość próbkowania</string>
|
|
||||||
<string name="settings_system_language">Język systemu</string>
|
|
||||||
<string name="home_subtitle_best_of">Top piosenki od twoich ulubionych wykonawców</string>
|
<string name="home_subtitle_best_of">Top piosenki od twoich ulubionych wykonawców</string>
|
||||||
<string name="home_subtitle_made_for_you">Stwórz miks z piosenki którą lubisz</string>
|
<string name="home_subtitle_made_for_you">Stwórz miks z piosenki którą lubisz</string>
|
||||||
<string name="home_subtitle_new_internet_radio_station">Dodaj nowe radio</string>
|
<string name="home_subtitle_new_internet_radio_station">Dodaj nowe radio</string>
|
||||||
|
|
@ -119,6 +117,10 @@
|
||||||
<string name="home_sync_starred_download">Pobierz</string>
|
<string name="home_sync_starred_download">Pobierz</string>
|
||||||
<string name="home_sync_starred_subtitle">Pobieranie tych utworów może zużyć dużo danych</string>
|
<string name="home_sync_starred_subtitle">Pobieranie tych utworów może zużyć dużo danych</string>
|
||||||
<string name="home_sync_starred_title">Wygląda na to że, są utwory oznaczone gwiazdką</string>
|
<string name="home_sync_starred_title">Wygląda na to że, są utwory oznaczone gwiazdką</string>
|
||||||
|
<string name="home_sync_starred_albums_title">Synchronizacja albumów oznaczonych gwiazdką</string>
|
||||||
|
<string name="home_sync_starred_albums_subtitle">Albumy oznaczone gwiazdką będą dostępne offline</string>
|
||||||
|
<string name="home_sync_starred_artists_title">Synchronizacja wykonawców oznaczonych gwiazdką</string>
|
||||||
|
<string name="home_sync_starred_artists_subtitle">Masz wykonawców oznaczonych gwiazdką, bez pobranej muzyki</string>
|
||||||
<string name="home_title_best_of">Najlepsze</string>
|
<string name="home_title_best_of">Najlepsze</string>
|
||||||
<string name="home_title_discovery">Odkrywanie</string>
|
<string name="home_title_discovery">Odkrywanie</string>
|
||||||
<string name="home_title_discovery_shuffle_all_button">Odtwórz wszystkie losowo</string>
|
<string name="home_title_discovery_shuffle_all_button">Odtwórz wszystkie losowo</string>
|
||||||
|
|
@ -165,7 +167,9 @@
|
||||||
<string name="login_title_expanded">Serwery Subsonic</string>
|
<string name="login_title_expanded">Serwery Subsonic</string>
|
||||||
<string name="media_route_menu_title">Przesyłanie</string>
|
<string name="media_route_menu_title">Przesyłanie</string>
|
||||||
<string name="menu_add_button">Dodaj</string>
|
<string name="menu_add_button">Dodaj</string>
|
||||||
|
<string name="menu_add_to_playlist_button">Dodaj do playlisty</string>
|
||||||
<string name="menu_download_all_button">Pobierz wszystko</string>
|
<string name="menu_download_all_button">Pobierz wszystko</string>
|
||||||
|
<string name="menu_rate_album">Oceń album</string>
|
||||||
<string name="menu_download_label">Pobrane</string>
|
<string name="menu_download_label">Pobrane</string>
|
||||||
<string name="menu_filter_all">Wszystko</string>
|
<string name="menu_filter_all">Wszystko</string>
|
||||||
<string name="menu_filter_download">Pobrane</string>
|
<string name="menu_filter_download">Pobrane</string>
|
||||||
|
|
@ -194,13 +198,23 @@
|
||||||
<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_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>
|
||||||
|
<string name="player_lyrics_download_failure">Tekst nie jest dostępny do pobrania.</string>
|
||||||
<string name="player_server_priority">Priorytet Serwerów</string>
|
<string name="player_server_priority">Priorytet Serwerów</string>
|
||||||
|
<string name="player_unknown_format">Nieznany format</string>
|
||||||
|
<string name="player_transcoding">Transkodowanie</string>
|
||||||
|
<string name="player_transcoding_requested">zażądane</string>
|
||||||
<string name="playlist_catalogue_title">Katalog Playlist</string>
|
<string name="playlist_catalogue_title">Katalog Playlist</string>
|
||||||
<string name="playlist_catalogue_title_expanded">Przeglądaj Playlisty</string>
|
<string name="playlist_catalogue_title_expanded">Przeglądaj Playlisty</string>
|
||||||
<string name="playlist_chooser_dialog_empty">Nie utworzono playlist</string>
|
<string name="playlist_chooser_dialog_empty">Nie utworzono playlist</string>
|
||||||
<string name="playlist_chooser_dialog_negative_button">Anuluj</string>
|
<string name="playlist_chooser_dialog_negative_button">Anuluj</string>
|
||||||
<string name="playlist_chooser_dialog_neutral_button">Utwórz</string>
|
<string name="playlist_chooser_dialog_neutral_button">Utwórz</string>
|
||||||
<string name="playlist_chooser_dialog_title">Dodaj do playlisty</string>
|
<string name="playlist_chooser_dialog_title">Dodaj do playlisty</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_add_success">Dodano piosenkę do playlisty</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenki do playlisty</string>
|
||||||
<string name="playlist_counted_tracks">%1$d utworów • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d utworów • %2$s</string>
|
||||||
<string name="playlist_duration">Długość • %1$s</string>
|
<string name="playlist_duration">Długość • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
|
||||||
|
|
@ -295,6 +309,9 @@
|
||||||
<string name="settings_github_link">https://github.com/eddyizm/tempo</string>
|
<string name="settings_github_link">https://github.com/eddyizm/tempo</string>
|
||||||
<string name="settings_github_summary">Śledź tworzenie aplikacji</string>
|
<string name="settings_github_summary">Śledź tworzenie aplikacji</string>
|
||||||
<string name="settings_github_title">GitHub</string>
|
<string name="settings_github_title">GitHub</string>
|
||||||
|
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempo/discussions</string>
|
||||||
|
<string name="settings_support_summary">Dołącz do dyskusji i wsparcia społeczności</string>
|
||||||
|
<string name="settings_support_title">Wsparcie użytkowników</string>
|
||||||
<string name="settings_image_size">Rozdzielczość obrazów</string>
|
<string name="settings_image_size">Rozdzielczość obrazów</string>
|
||||||
<string name="settings_language">Język</string>
|
<string name="settings_language">Język</string>
|
||||||
<string name="settings_logout_title">Wyloguj</string>
|
<string name="settings_logout_title">Wyloguj</string>
|
||||||
|
|
@ -308,13 +325,17 @@
|
||||||
<string name="settings_podcast_summary">Jeżeli włączone, widoczna będzie sekcja z podcastami. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
|
<string name="settings_podcast_summary">Jeżeli włączone, widoczna będzie sekcja z podcastami. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
|
||||||
<string name="settings_audio_quality">Pokaż jakość audio</string>
|
<string name="settings_audio_quality">Pokaż jakość audio</string>
|
||||||
<string name="settings_audio_quality_summary">Bitrate i format audio będzie pokazywany dla każdego utworu.</string>
|
<string name="settings_audio_quality_summary">Bitrate i format audio będzie pokazywany dla każdego utworu.</string>
|
||||||
|
<string name="settings_song_rating">Pokaż ocenę piosenek w gwiazdkach</string>
|
||||||
|
<string name="settings_song_rating_summary">Jeżeli włączone, pokazuje ocenę w 5 gwiazdkach dla utworu na stronie piosenki\n\n*Wymaga ponownego uruchomienia aplikacji</string>
|
||||||
<string name="settings_item_rating">Pokaż oceny elementów</string>
|
<string name="settings_item_rating">Pokaż oceny elementów</string>
|
||||||
<string name="settings_item_rating_summary">Jeżeli włączone, ocena elementów oraz czy jest oznaczony jako ulubiony będą pokazywane.</string>
|
<string name="settings_item_rating_summary">Jeżeli włączone, ocena elementów oraz czy jest oznaczony jako ulubiony będą pokazywane.</string>
|
||||||
<string name="settings_queue_syncing_countdown">Timer synchronizacji</string>
|
<string name="settings_queue_syncing_countdown">Timer synchronizacji</string>
|
||||||
<string name="settings_queue_syncing_summary">Jeżeli włączone, użytkownik będzie miał możliwość zapisania kolejki i będzie miał możliwość załadowania jej stanu przy otwarciu aplikacji.</string>
|
<string name="settings_queue_syncing_summary">Jeżeli włączone, użytkownik będzie miał możliwość zapisania kolejki i będzie miał możliwość załadowania jej stanu przy otwarciu aplikacji.</string>
|
||||||
<string name="settings_queue_syncing_title">Synchronizuj kolejkę odtwarzania dla tego użytkownika</string>
|
<string name="settings_queue_syncing_title">Synchronizuj kolejkę odtwarzania dla tego użytkownika [Niedokończone]</string>
|
||||||
<string name="settings_radio">Pokaż radio</string>
|
<string name="settings_radio">Pokaż radio</string>
|
||||||
<string name="settings_radio_summary">Jeżeli włączone, widoczna będzie sekcja radia. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
|
<string name="settings_radio_summary">Jeżeli włączone, widoczna będzie sekcja radia. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
|
||||||
|
<string name="settings_auto_download_lyrics">Automatyczne pobieranie tesktów</string>
|
||||||
|
<string name="settings_auto_download_lyrics_summary">Automatycznie zapisuj teksty jeżeli, są dostępne aby, mogły być wyświetlane offline.</string>
|
||||||
<string name="settings_replay_gain">Tryb wzmocnienia głośności przy ponownym odtwarzaniu</string>
|
<string name="settings_replay_gain">Tryb wzmocnienia głośności przy ponownym odtwarzaniu</string>
|
||||||
<string name="settings_rounded_corner">Zaokrąglone rogi</string>
|
<string name="settings_rounded_corner">Zaokrąglone rogi</string>
|
||||||
<string name="settings_rounded_corner_size">Rozmiar rogów</string>
|
<string name="settings_rounded_corner_size">Rozmiar rogów</string>
|
||||||
|
|
@ -322,6 +343,7 @@
|
||||||
<string name="settings_rounded_corner_summary">Jeżeli włączone, ustawia kąt krzywizny dla wszystkich renderowanych okładek. Zmiany przyniosą efekt po restarcie.</string>
|
<string name="settings_rounded_corner_summary">Jeżeli włączone, ustawia kąt krzywizny dla wszystkich renderowanych okładek. Zmiany przyniosą efekt po restarcie.</string>
|
||||||
<string name="settings_scan_title">Skanuj bibliotekę</string>
|
<string name="settings_scan_title">Skanuj bibliotekę</string>
|
||||||
<string name="settings_scrobble_title">Włącz scrobbling muzyki</string>
|
<string name="settings_scrobble_title">Włącz scrobbling muzyki</string>
|
||||||
|
<string name="settings_system_language">Język systemowy</string>
|
||||||
<string name="settings_share_title">Włącz udostępnianie muzyki</string>
|
<string name="settings_share_title">Włącz udostępnianie muzyki</string>
|
||||||
<string name="settings_streaming_cache_size">Rozmiar cache dla strumieniowania</string>
|
<string name="settings_streaming_cache_size">Rozmiar cache dla strumieniowania</string>
|
||||||
<string name="settings_streaming_cache_storage_title">Pamięć cache dla strumieniowania</string>
|
<string name="settings_streaming_cache_storage_title">Pamięć cache dla strumieniowania</string>
|
||||||
|
|
@ -330,11 +352,15 @@
|
||||||
<string name="settings_summary_replay_gain">Wzmocnienie głośności jest funkcją która pozwala tobie na ustawienia poziomu głośności dla utworów aby słuchanie brzmiało cały czas tak samo. To ustawienia działa tylko wtedy kiedy utwór zawiera potrzebne metadane.</string>
|
<string name="settings_summary_replay_gain">Wzmocnienie głośności jest funkcją która pozwala tobie na ustawienia poziomu głośności dla utworów aby słuchanie brzmiało cały czas tak samo. To ustawienia działa tylko wtedy kiedy utwór zawiera potrzebne metadane.</string>
|
||||||
<string name="settings_summary_scrobble">Scrobbling jest funkcją która pozwala twojemu urządzeniu na wysyłanie informacji na temat piosenek których słuchasz do serwera muzyki. Te informacje pomagają tworzyć spersonalizowane rekomendacje na podstawie twojego gustu muzycznego.</string>
|
<string name="settings_summary_scrobble">Scrobbling jest funkcją która pozwala twojemu urządzeniu na wysyłanie informacji na temat piosenek których słuchasz do serwera muzyki. Te informacje pomagają tworzyć spersonalizowane rekomendacje na podstawie twojego gustu muzycznego.</string>
|
||||||
<string name="settings_summary_share">Pozwala udostępnić użytkownikowi muzykę przez link. Ta funkcjonalność musi być wspierana i włączona na serwerze i jest ograniczona do pojedyńczych utworów, albumów i playlist.</string>
|
<string name="settings_summary_share">Pozwala udostępnić użytkownikowi muzykę przez link. Ta funkcjonalność musi być wspierana i włączona na serwerze i jest ograniczona do pojedyńczych utworów, albumów i playlist.</string>
|
||||||
<string name="settings_summary_syncing">Przywraca stan kolejki odtwarzania dla tego użytkownika. Zawiera utwory w kolejce, aktualnie odtwarzany utwór i pozycję w nim. Serwer musi wspierać tę funkcję.</string>
|
<string name="settings_summary_syncing">Przywraca stan kolejki odtwarzania dla tego użytkownika. Zawiera utwory w kolejce, aktualnie odtwarzany utwór i pozycję w nim. Serwer musi wspierać tę funkcję.\n*To ustawienie nie działa na 100% na wszystkich serwerach/urządzeniach.</string>
|
||||||
<string name="settings_summary_streaming_cache_size">%1$s \nAktualnie w użyciu: %2$s MiB</string>
|
<string name="settings_summary_streaming_cache_size">%1$s \nAktualnie w użyciu: %2$s MiB</string>
|
||||||
<string name="settings_summary_transcoding">Priorytet dawany trybowi transkodowania. Jeżeli ustawiony na \"Odtwarzanie bezpośrednie\" bitrate pliku nie zostanie zmieniony.</string>
|
<string name="settings_summary_transcoding">Priorytet dawany trybowi transkodowania. Jeżeli ustawiony na \"Odtwarzanie bezpośrednie\" bitrate pliku nie zostanie zmieniony.</string>
|
||||||
<string name="settings_summary_transcoding_download">Pobieraj transkdowane media. Jeżeli włączone, endpoint pobierania nie będzie używnany, poza następującymi ustawieniami. \n\n Jeżeli \"Format transkodowania dla pobierania\" jest ustawiony na \"Pobieranie bezpośrednie\" bitrate pliku nie zostanie zmieniony.</string>
|
<string name="settings_summary_transcoding_download">Pobieraj transkdowane media. Jeżeli włączone, endpoint pobierania nie będzie używnany, poza następującymi ustawieniami. \n\n Jeżeli \"Format transkodowania dla pobierania\" jest ustawiony na \"Pobieranie bezpośrednie\" bitrate pliku nie zostanie zmieniony.</string>
|
||||||
<string name="settings_summary_transcoding_estimate_content_length">Kiedy plik jest transkodowany w locie, klient nie pokazuje zwykle długości utworu.Jest możliwe odpytanie serwera który wspiera tą funkcjonalność aby oszacował długość odtwarzanego utworu, ale czasy odpowiedzi mogą być dłuższe.</string>
|
<string name="settings_summary_transcoding_estimate_content_length">Kiedy plik jest transkodowany w locie, klient nie pokazuje zwykle długości utworu.Jest możliwe odpytanie serwera który wspiera tą funkcjonalność aby oszacował długość odtwarzanego utworu, ale czasy odpowiedzi mogą być dłuższe.</string>
|
||||||
|
<string name="settings_sync_starred_artists_for_offline_use_summary">Jeżeli włączone, utwory wykonawców oznaczonych gwiazdką będą pobierane do użycia offline.</string>
|
||||||
|
<string name="settings_sync_starred_artists_for_offline_use_title">Synchronizuj wykonawców oznacznych gwiazdką do użycia offline</string>
|
||||||
|
<string name="settings_sync_starred_albums_for_offline_use_summary">Jeżeli włączone, albumy oznaczone gwiazdką będą pobieranew do użycia offline.</string>
|
||||||
|
<string name="settings_sync_starred_albums_for_offline_use_title">Synchronizuj albumy oznaczone gwiazdką do użycia offline</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">Jeżeli włączone, utwory oznaczone gwiazdką będą pobrane do użycia offline.</string>
|
<string name="settings_sync_starred_tracks_for_offline_use_summary">Jeżeli włączone, utwory oznaczone gwiazdką będą pobrane do użycia offline.</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_title">Zsynchronizuj utwory oznaczone gwiazdką do użycia offline</string>
|
<string name="settings_sync_starred_tracks_for_offline_use_title">Zsynchronizuj utwory oznaczone gwiazdką do użycia offline</string>
|
||||||
<string name="settings_theme">Motyw</string>
|
<string name="settings_theme">Motyw</string>
|
||||||
|
|
@ -390,14 +416,19 @@
|
||||||
<string name="starred_sync_dialog_positive_button">Kontynuuj i pobierz</string>
|
<string name="starred_sync_dialog_positive_button">Kontynuuj i pobierz</string>
|
||||||
<string name="starred_sync_dialog_summary">Pobieranie utworów oznaczonych gwiazdką może wymagać dużej ilośći danych.</string>
|
<string name="starred_sync_dialog_summary">Pobieranie utworów oznaczonych gwiazdką może wymagać dużej ilośći danych.</string>
|
||||||
<string name="starred_sync_dialog_title">Synchronizuj utwory oznaczone gwiazdką</string>
|
<string name="starred_sync_dialog_title">Synchronizuj utwory oznaczone gwiazdką</string>
|
||||||
|
<string name="starred_artist_sync_dialog_summary">Pobieranie utworów artystów oznaczonych gwiazdką może wymagać dużej ilośći danych.</string>
|
||||||
|
<string name="starred_artist_sync_dialog_title">Synchronizacja wykonawców oznaczonych gwiazdką</string>
|
||||||
|
<string name="starred_album_sync_dialog_summary">Pobieranie albumów oznaczonych gwiazdką może wymagać dużej ilośći danych.</string>
|
||||||
|
<string name="starred_album_sync_dialog_title">Synchronizacja albumów oznaczonych gwiazdką</string>
|
||||||
<string name="streaming_cache_storage_dialog_sub_summary">Aby zmiany przyniosły efekt, zrestartuj aplikację.</string>
|
<string name="streaming_cache_storage_dialog_sub_summary">Aby zmiany przyniosły efekt, zrestartuj aplikację.</string>
|
||||||
<string name="streaming_cache_storage_dialog_summary">Zmiana lokalizacji plików cache z jednej na drugą spowoduje natychmiastowe usunięcie wcześniej pobranych plików cache w drugiej lokalizacji.</string>
|
<string name="streaming_cache_storage_dialog_summary">Zmiana lokalizacji plików cache z jednej na drugą spowoduje natychmiastowe usunięcie wcześniej pobranych plików cache w drugiej lokalizacji.</string>
|
||||||
<string name="streaming_cache_storage_dialog_title">Wybieranie pamięci</string>
|
<string name="streaming_cache_storage_dialog_title">Wybieranie pamięci</string>
|
||||||
<string name="streaming_cache_storage_external_dialog_positive_button">Zewnętrzna</string>
|
<string name="streaming_cache_storage_external_dialog_positive_button">Zewnętrzna</string>
|
||||||
<string name="streaming_cache_storage_internal_dialog_negative_button">Wewnętrzna</string>
|
<string name="streaming_cache_storage_internal_dialog_negative_button">Wewnętrzna</string>
|
||||||
<string name="support_url">https://buymeacoffee.com/a.cappiello</string>
|
<string name="support_url">https://ko-fi.com/eddyizm</string>
|
||||||
<string name="track_info_album">Album</string>
|
<string name="track_info_album">Album</string>
|
||||||
<string name="track_info_artist">Wykonawca</string>
|
<string name="track_info_artist">Wykonawca</string>
|
||||||
|
<string name="track_info_bit_depth">Głębia bitowa</string>
|
||||||
<string name="track_info_bitrate">Bitrate</string>
|
<string name="track_info_bitrate">Bitrate</string>
|
||||||
<string name="track_info_content_type">Typ Treści</string>
|
<string name="track_info_content_type">Typ Treści</string>
|
||||||
<string name="track_info_dialog_positive_button">OK</string>
|
<string name="track_info_dialog_positive_button">OK</string>
|
||||||
|
|
@ -406,6 +437,7 @@
|
||||||
<string name="track_info_duration">Długość</string>
|
<string name="track_info_duration">Długość</string>
|
||||||
<string name="track_info_genre">Gatunek</string>
|
<string name="track_info_genre">Gatunek</string>
|
||||||
<string name="track_info_path">Ścieżka</string>
|
<string name="track_info_path">Ścieżka</string>
|
||||||
|
<string name="track_info_sampling_rate">Częstotliwość próbkowania</string>
|
||||||
<string name="track_info_size">Rozmiar</string>
|
<string name="track_info_size">Rozmiar</string>
|
||||||
<string name="track_info_suffix">Sufiks</string>
|
<string name="track_info_suffix">Sufiks</string>
|
||||||
<string name="track_info_summary_downloaded_file">Plik został pobrany przy użyciu API Subsonic. Kodek i bitrate pliku pozostaje nie zmieniony względem pliku źródłowego.</string>
|
<string name="track_info_summary_downloaded_file">Plik został pobrany przy użyciu API Subsonic. Kodek i bitrate pliku pozostaje nie zmieniony względem pliku źródłowego.</string>
|
||||||
|
|
@ -426,6 +458,14 @@
|
||||||
<item quantity="one">%d album do zsynchronizowania </item>
|
<item quantity="one">%d album do zsynchronizowania </item>
|
||||||
<item quantity="other">%d albumów do zsynchrpnizowania</item>
|
<item quantity="other">%d albumów do zsynchrpnizowania</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="home_sync_starred_artists_count">
|
||||||
|
<item quantity="one">%d wykonawca do zsynchronizowania</item>
|
||||||
|
<item quantity="other">%d wykonawców do zsynchronizowania</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="songs_download_started">
|
||||||
|
<item quantity="one">Pobieranie %d piosenki</item>
|
||||||
|
<item quantity="other">Pobieranie %d piosenek</item>
|
||||||
|
</plurals>
|
||||||
<string name="equalizer_fragment_title">Korektor dźwięku</string>
|
<string name="equalizer_fragment_title">Korektor dźwięku</string>
|
||||||
<string name="equalizer_reset">Reset</string>
|
<string name="equalizer_reset">Reset</string>
|
||||||
<string name="equalizer_enable">Włączony</string>
|
<string name="equalizer_enable">Włączony</string>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@
|
||||||
<string name="exo_download_notification_channel_name">İndirilenler</string>
|
<string name="exo_download_notification_channel_name">İndirilenler</string>
|
||||||
<string name="filter_info_selection">İki veya daha fazla filtre seçin</string>
|
<string name="filter_info_selection">İki veya daha fazla filtre seçin</string>
|
||||||
<string name="filter_title">Filtre</string>
|
<string name="filter_title">Filtre</string>
|
||||||
|
<string name="filter_artist">Sanatçıları filtrele</string>
|
||||||
<string name="filter_title_expanded">Türleri filtrele</string>
|
<string name="filter_title_expanded">Türleri filtrele</string>
|
||||||
<string name="generic_list_page_count">(%1$d)</string>
|
<string name="generic_list_page_count">(%1$d)</string>
|
||||||
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
||||||
|
|
@ -116,6 +117,7 @@
|
||||||
<string name="home_sync_starred_download">İndir</string>
|
<string name="home_sync_starred_download">İndir</string>
|
||||||
<string name="home_sync_starred_subtitle">Bu parçaların indirilmesi önemli miktarda veri kullanabilir</string>
|
<string name="home_sync_starred_subtitle">Bu parçaların indirilmesi önemli miktarda veri kullanabilir</string>
|
||||||
<string name="home_sync_starred_title">Eşitlenecek bazı yıldızlı parçalar var gibi görünüyor</string>
|
<string name="home_sync_starred_title">Eşitlenecek bazı yıldızlı parçalar var gibi görünüyor</string>
|
||||||
|
<string name="home_sync_starred_albums_subtitle">Yıldız ile işaretlenen albümler çevrimdışı kullanılabilir olacak</string>
|
||||||
<string name="home_title_best_of">En iyiler</string>
|
<string name="home_title_best_of">En iyiler</string>
|
||||||
<string name="home_title_discovery">Keşfet</string>
|
<string name="home_title_discovery">Keşfet</string>
|
||||||
<string name="home_title_discovery_shuffle_all_button">Tümünü karıştır</string>
|
<string name="home_title_discovery_shuffle_all_button">Tümünü karıştır</string>
|
||||||
|
|
@ -164,6 +166,7 @@
|
||||||
<string name="menu_add_button">Ekle</string>
|
<string name="menu_add_button">Ekle</string>
|
||||||
<string name="menu_add_to_playlist_button">Çalma listesine ekle</string>
|
<string name="menu_add_to_playlist_button">Çalma listesine ekle</string>
|
||||||
<string name="menu_download_all_button">Tümünü indir</string>
|
<string name="menu_download_all_button">Tümünü indir</string>
|
||||||
|
<string name="menu_rate_album">Albümü oyla</string>
|
||||||
<string name="menu_download_label">İndir</string>
|
<string name="menu_download_label">İndir</string>
|
||||||
<string name="menu_filter_all">Tümü</string>
|
<string name="menu_filter_all">Tümü</string>
|
||||||
<string name="menu_filter_download">İndirilenler</string>
|
<string name="menu_filter_download">İndirilenler</string>
|
||||||
|
|
@ -192,6 +195,7 @@
|
||||||
<string name="menu_sort_year">Yıl</string>
|
<string name="menu_sort_year">Yıl</string>
|
||||||
<string name="player_playback_speed">%1$.2fx</string>
|
<string name="player_playback_speed">%1$.2fx</string>
|
||||||
<string name="player_queue_clean_all_button">Çalma sırasını temizle</string>
|
<string name="player_queue_clean_all_button">Çalma sırasını temizle</string>
|
||||||
|
<string name="player_queue_save_queue_success">Kayıtlı oynatma sırası</string>
|
||||||
<string name="player_server_priority">Sunucu önceliği</string>
|
<string name="player_server_priority">Sunucu önceliği</string>
|
||||||
<string name="player_unknown_format">Bilinmeyen format</string>
|
<string name="player_unknown_format">Bilinmeyen format</string>
|
||||||
<string name="player_transcoding">Dönüştürme</string>
|
<string name="player_transcoding">Dönüştürme</string>
|
||||||
|
|
@ -311,6 +315,7 @@
|
||||||
<string name="settings_podcast_summary">Etkinleştirildiğinde podcast bölümü görüntülenir. Tam etkili olması için uygulamayı yeniden başlatın.</string>
|
<string name="settings_podcast_summary">Etkinleştirildiğinde podcast bölümü görüntülenir. Tam etkili olması için uygulamayı yeniden başlatın.</string>
|
||||||
<string name="settings_audio_quality">Ses kalitesini göster</string>
|
<string name="settings_audio_quality">Ses kalitesini göster</string>
|
||||||
<string name="settings_audio_quality_summary">Her ses parçası için bit hızı ve ses formatı gösterilecektir.</string>
|
<string name="settings_audio_quality_summary">Her ses parçası için bit hızı ve ses formatı gösterilecektir.</string>
|
||||||
|
<string name="settings_song_rating_summary">" "</string>
|
||||||
<string name="settings_item_rating">Öğe değerlemesini göster</string>
|
<string name="settings_item_rating">Öğe değerlemesini göster</string>
|
||||||
<string name="settings_item_rating_summary">Etkinleştirildiğinde, öğenin puanı ve favori olarak işaretlenip işaretlenmediği görüntülenir.</string>
|
<string name="settings_item_rating_summary">Etkinleştirildiğinde, öğenin puanı ve favori olarak işaretlenip işaretlenmediği görüntülenir.</string>
|
||||||
<string name="settings_queue_syncing_countdown">Eşitleme zamanlayıcısı</string>
|
<string name="settings_queue_syncing_countdown">Eşitleme zamanlayıcısı</string>
|
||||||
|
|
@ -340,6 +345,7 @@
|
||||||
<string name="settings_summary_transcoding_download">Dönüştürülmüş medyayı indir. Etkinleştirilirse indirme uç noktası kullanılmaz, bunun yerine aşağıdaki ayarlar geçerli olur. \n\n “İndirmeler için dönüştürme formatı” “Doğrudan indir” olarak ayarlanırsa dosyanın bit hızı değiştirilmez.</string>
|
<string name="settings_summary_transcoding_download">Dönüştürülmüş medyayı indir. Etkinleştirilirse indirme uç noktası kullanılmaz, bunun yerine aşağıdaki ayarlar geçerli olur. \n\n “İndirmeler için dönüştürme formatı” “Doğrudan indir” olarak ayarlanırsa dosyanın bit hızı değiştirilmez.</string>
|
||||||
<string name="settings_summary_transcoding_estimate_content_length">Dosya anlık olarak dönüştürüldüğünde, istemci genellikle parçanın süresini göstermez. Bu işlevi destekleyen sunuculardan çalınan parçanın süresini tahmin etmeleri istenebilir,
|
<string name="settings_summary_transcoding_estimate_content_length">Dosya anlık olarak dönüştürüldüğünde, istemci genellikle parçanın süresini göstermez. Bu işlevi destekleyen sunuculardan çalınan parçanın süresini tahmin etmeleri istenebilir,
|
||||||
ancak yanıt süreleri daha uzun olabilir.</string>
|
ancak yanıt süreleri daha uzun olabilir.</string>
|
||||||
|
<string name="settings_sync_starred_albums_for_offline_use_title">Çevrimdışı kullanım için yıldızlı albümleri senkronize et</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">Etkinleştirildiğinde, yıldızlı parçalar çevrimdışı kullanım için indirilecektir.</string>
|
<string name="settings_sync_starred_tracks_for_offline_use_summary">Etkinleştirildiğinde, yıldızlı parçalar çevrimdışı kullanım için indirilecektir.</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_title">Çevrimdışı kullanım için yıldızlı parçaları eşitle</string>
|
<string name="settings_sync_starred_tracks_for_offline_use_title">Çevrimdışı kullanım için yıldızlı parçaları eşitle</string>
|
||||||
<string name="settings_theme">Tema</string>
|
<string name="settings_theme">Tema</string>
|
||||||
|
|
@ -395,6 +401,8 @@
|
||||||
<string name="starred_sync_dialog_positive_button">Devam et ve indir</string>
|
<string name="starred_sync_dialog_positive_button">Devam et ve indir</string>
|
||||||
<string name="starred_sync_dialog_summary">Yıldızlı parçaların indirilmesi yüksek miktarda veri gerektirebilir.</string>
|
<string name="starred_sync_dialog_summary">Yıldızlı parçaların indirilmesi yüksek miktarda veri gerektirebilir.</string>
|
||||||
<string name="starred_sync_dialog_title">Yıldızlı parçaları eşitle</string>
|
<string name="starred_sync_dialog_title">Yıldızlı parçaları eşitle</string>
|
||||||
|
<string name="starred_album_sync_dialog_summary">Yıldızlı albümleri indirmek yüksek miktarda veri kullanımı gerektirebilir.</string>
|
||||||
|
<string name="starred_album_sync_dialog_title">Yıldızlı albümleri senkronize et</string>
|
||||||
<string name="streaming_cache_storage_dialog_sub_summary">Değişikliklerin geçerli olması için uygulamayı yeniden başlatın.</string>
|
<string name="streaming_cache_storage_dialog_sub_summary">Değişikliklerin geçerli olması için uygulamayı yeniden başlatın.</string>
|
||||||
<string name="streaming_cache_storage_dialog_summary">Önbelleğe alınmış dosyaların hedefini bir depolamadan diğerine değiştirmek, önceki depolamadaki önbellek dosyalarının silinmesine yol açabilir.</string>
|
<string name="streaming_cache_storage_dialog_summary">Önbelleğe alınmış dosyaların hedefini bir depolamadan diğerine değiştirmek, önceki depolamadaki önbellek dosyalarının silinmesine yol açabilir.</string>
|
||||||
<string name="streaming_cache_storage_dialog_title">Depolama seçeneğini seç</string>
|
<string name="streaming_cache_storage_dialog_title">Depolama seçeneğini seç</string>
|
||||||
|
|
@ -433,4 +441,16 @@
|
||||||
<string name="undraw_page">unDraw</string>
|
<string name="undraw_page">unDraw</string>
|
||||||
<string name="undraw_thanks">İllüstrasyonlarıyla bu uygulamayı daha güzel hale getirmemize yardımcı olan unDraw’a özel teşekkürler.</string>
|
<string name="undraw_thanks">İllüstrasyonlarıyla bu uygulamayı daha güzel hale getirmemize yardımcı olan unDraw’a özel teşekkürler.</string>
|
||||||
<string name="undraw_url">https://undraw.co/</string>
|
<string name="undraw_url">https://undraw.co/</string>
|
||||||
|
<string name="home_sync_starred_albums_title">Yıldızlı Albümleri Senkronize Et</string>
|
||||||
|
<string name="widget_label">Tempo Widget</string>
|
||||||
|
<string name="widget_not_playing">Şu an oynatılmıyor</string>
|
||||||
|
<string name="widget_placeholder_subtitle">Tempo’yu aç</string>
|
||||||
|
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||||
|
<string name="widget_time_duration_placeholder">0:00</string>
|
||||||
|
<string name="widget_content_desc_album_art">Albüm kapağı</string>
|
||||||
|
<string name="widget_content_desc_play_pause">Çal/Duraklat</string>
|
||||||
|
<string name="widget_content_desc_next">Sonraki parça</string>
|
||||||
|
<string name="widget_content_desc_prev">Önceki parça</string>
|
||||||
|
<string name="settings_song_rating">Şarkının yıldız derecelendirmesini göster</string>
|
||||||
|
<string name="settings_sync_starred_albums_for_offline_use_summary">"Etkinleştirildiğinde yıldızlı albümler çevrimdışı kullanım için indirilecek. "</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
9
app/src/main/res/values/colors_widget.xml
Normal file
9
app/src/main/res/values/colors_widget.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Light theme: bright card with dark content -->
|
||||||
|
<color name="widget_bg">#CCFFFFFF</color>
|
||||||
|
<color name="widget_title">#DE000000</color>
|
||||||
|
<color name="widget_subtitle">#99000000</color>
|
||||||
|
<color name="widget_icon_tint">#DE000000</color>
|
||||||
|
<color name="widget_icon_tint_active">#FF6750A4</color>
|
||||||
|
</resources>
|
||||||
6
app/src/main/res/values/integers.xml
Normal file
6
app/src/main/res/values/integers.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<integer name="widget_medium_min_height_dp">100</integer>
|
||||||
|
<integer name="widget_large_min_height_dp">160</integer>
|
||||||
|
<integer name="widget_expanded_min_height_dp">220</integer>
|
||||||
|
</resources>
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
<string name="download_directory_dialog_positive_button">Download</string>
|
<string name="download_directory_dialog_positive_button">Download</string>
|
||||||
<string name="download_directory_dialog_summary">All tracks in this folder will be downloaded. Tracks present in subfolders will not be downloaded.</string>
|
<string name="download_directory_dialog_summary">All tracks in this folder will be downloaded. Tracks present in subfolders will not be downloaded.</string>
|
||||||
<string name="download_directory_dialog_title">Download the tracks</string>
|
<string name="download_directory_dialog_title">Download the tracks</string>
|
||||||
|
<string name="download_directory_set">Set where music is downloaded</string>
|
||||||
<string name="download_info_empty_subtitle">Once you download a song, you\'ll find it here</string>
|
<string name="download_info_empty_subtitle">Once you download a song, you\'ll find it here</string>
|
||||||
<string name="download_info_empty_title">No downloads yet!</string>
|
<string name="download_info_empty_title">No downloads yet!</string>
|
||||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</string>
|
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</string>
|
||||||
|
|
@ -78,7 +79,15 @@
|
||||||
<string name="download_storage_dialog_title">Select storage option</string>
|
<string name="download_storage_dialog_title">Select storage option</string>
|
||||||
<string name="download_storage_external_dialog_positive_button">External</string>
|
<string name="download_storage_external_dialog_positive_button">External</string>
|
||||||
<string name="download_storage_internal_dialog_negative_button">Internal</string>
|
<string name="download_storage_internal_dialog_negative_button">Internal</string>
|
||||||
|
<string name="download_storage_directory_dialog_neutral_button">Directory</string>
|
||||||
<string name="download_title_section">Downloads</string>
|
<string name="download_title_section">Downloads</string>
|
||||||
|
<string name="download_refresh_no_directory">Set a download folder to refresh your downloads.</string>
|
||||||
|
<string name="download_refresh_no_changes">No missing downloads found.</string>
|
||||||
|
<plurals name="download_refresh_removed">
|
||||||
|
<item quantity="one">Removed %d missing download.</item>
|
||||||
|
<item quantity="other">Removed %d missing downloads.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="download_refresh_button_content_description">Refresh downloaded items</string>
|
||||||
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
||||||
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
||||||
<string name="downloaded_bottom_sheet_remove">Remove</string>
|
<string name="downloaded_bottom_sheet_remove">Remove</string>
|
||||||
|
|
@ -88,6 +97,9 @@
|
||||||
<string name="error_required">Required</string>
|
<string name="error_required">Required</string>
|
||||||
<string name="error_server_prefix">http or https prefix required</string>
|
<string name="error_server_prefix">http or https prefix required</string>
|
||||||
<string name="exo_download_notification_channel_name">Downloads</string>
|
<string name="exo_download_notification_channel_name">Downloads</string>
|
||||||
|
<string name="exo_controls_heart_off_description">Toggle Heart off</string>
|
||||||
|
<string name="exo_controls_heart_on_description">Toggle Heart on</string>
|
||||||
|
<string name="cast_expanded_controller_loading">Loading…</string>
|
||||||
<string name="filter_info_selection">Select two or more filters</string>
|
<string name="filter_info_selection">Select two or more filters</string>
|
||||||
<string name="filter_title">Filter</string>
|
<string name="filter_title">Filter</string>
|
||||||
<string name="filter_artist">Filter artists</string>
|
<string name="filter_artist">Filter artists</string>
|
||||||
|
|
@ -119,6 +131,8 @@
|
||||||
<string name="home_sync_starred_title">Looks like there are some starred tracks to sync</string>
|
<string name="home_sync_starred_title">Looks like there are some starred tracks to sync</string>
|
||||||
<string name="home_sync_starred_albums_title">Sync Starred Albums</string>
|
<string name="home_sync_starred_albums_title">Sync Starred Albums</string>
|
||||||
<string name="home_sync_starred_albums_subtitle">Albums marked with a star will be available offline</string>
|
<string name="home_sync_starred_albums_subtitle">Albums marked with a star will be available offline</string>
|
||||||
|
<string name="home_sync_starred_artists_title">Starred Artists Sync</string>
|
||||||
|
<string name="home_sync_starred_artists_subtitle">You have starred artists with music not downloaded</string>
|
||||||
<string name="home_title_best_of">Best of</string>
|
<string name="home_title_best_of">Best of</string>
|
||||||
<string name="home_title_discovery">Discovery</string>
|
<string name="home_title_discovery">Discovery</string>
|
||||||
<string name="home_title_discovery_shuffle_all_button">Shuffle all</string>
|
<string name="home_title_discovery_shuffle_all_button">Shuffle all</string>
|
||||||
|
|
@ -197,6 +211,10 @@
|
||||||
<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_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>
|
||||||
|
<string name="player_lyrics_download_failure">Lyrics are not available to download.</string>
|
||||||
<string name="player_server_priority">Server Priority</string>
|
<string name="player_server_priority">Server Priority</string>
|
||||||
<string name="player_unknown_format">Unknown format</string>
|
<string name="player_unknown_format">Unknown format</string>
|
||||||
<string name="player_transcoding">Transcoding</string>
|
<string name="player_transcoding">Transcoding</string>
|
||||||
|
|
@ -207,8 +225,9 @@
|
||||||
<string name="playlist_chooser_dialog_negative_button">Cancel</string>
|
<string name="playlist_chooser_dialog_negative_button">Cancel</string>
|
||||||
<string name="playlist_chooser_dialog_neutral_button">Create</string>
|
<string name="playlist_chooser_dialog_neutral_button">Create</string>
|
||||||
<string name="playlist_chooser_dialog_title">Add to a playlist</string>
|
<string name="playlist_chooser_dialog_title">Add to a playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_success">Added song to playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song to playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
||||||
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
||||||
<string name="playlist_duration">Duration • %1$s</string>
|
<string name="playlist_duration">Duration • %1$s</string>
|
||||||
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
||||||
|
|
@ -275,6 +294,8 @@
|
||||||
<string name="settings_about_summary">Tempo is an open source and lightweight music client for Subsonic, designed and built natively for Android.</string>
|
<string name="settings_about_summary">Tempo is an open source and lightweight music client for Subsonic, designed and built natively for Android.</string>
|
||||||
<string name="settings_about_title">About</string>
|
<string name="settings_about_title">About</string>
|
||||||
<string name="settings_always_on_display">Always on display</string>
|
<string name="settings_always_on_display">Always on display</string>
|
||||||
|
<string name="settings_allow_playlist_duplicates">Allow adding duplicates to playlist</string>
|
||||||
|
<string name="settings_allow_playlist_duplicates_summary">If enabled, duplicates won\'t be checked while adding to a playlist.</string>
|
||||||
<string name="settings_audio_transcode_download_format">Transcode format</string>
|
<string name="settings_audio_transcode_download_format">Transcode format</string>
|
||||||
<string name="settings_audio_transcode_download_priority_summary">If enabled, Tempo will not force download the track with the transcode settings below.</string>
|
<string name="settings_audio_transcode_download_priority_summary">If enabled, Tempo will not force download the track with the transcode settings below.</string>
|
||||||
<string name="settings_audio_transcode_download_priority_title">Prioritize server settings used for streaming in downloads</string>
|
<string name="settings_audio_transcode_download_priority_title">Prioritize server settings used for streaming in downloads</string>
|
||||||
|
|
@ -303,6 +324,9 @@
|
||||||
<string name="settings_github_link">https://github.com/eddyizm/tempo</string>
|
<string name="settings_github_link">https://github.com/eddyizm/tempo</string>
|
||||||
<string name="settings_github_summary">Follow the development</string>
|
<string name="settings_github_summary">Follow the development</string>
|
||||||
<string name="settings_github_title">Github</string>
|
<string name="settings_github_title">Github</string>
|
||||||
|
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempo/discussions</string>
|
||||||
|
<string name="settings_support_summary">Join community discussions and support</string>
|
||||||
|
<string name="settings_support_title">User support</string>
|
||||||
<string name="settings_image_size">Set image resolution</string>
|
<string name="settings_image_size">Set image resolution</string>
|
||||||
<string name="settings_language">Language</string>
|
<string name="settings_language">Language</string>
|
||||||
<string name="settings_logout_title">Log out</string>
|
<string name="settings_logout_title">Log out</string>
|
||||||
|
|
@ -323,8 +347,12 @@
|
||||||
<string name="settings_queue_syncing_countdown">Sync timer</string>
|
<string name="settings_queue_syncing_countdown">Sync timer</string>
|
||||||
<string name="settings_queue_syncing_summary">If enabled, the user will have the ability to save their play queue and will have the ability to load state when opening the application.</string>
|
<string name="settings_queue_syncing_summary">If enabled, the user will have the ability to save their play queue and will have the ability to load state when opening the application.</string>
|
||||||
<string name="settings_queue_syncing_title">Sync play queue for this user [Not Fully Baked]</string>
|
<string name="settings_queue_syncing_title">Sync play queue for this user [Not Fully Baked]</string>
|
||||||
|
<string name="settings_show_mini_shuffle_button">Show Shuffle button</string>
|
||||||
|
<string name="settings_show_mini_shuffle_button_summary">If enabled, show the shuffle button, remove the heart in the mini player</string>
|
||||||
<string name="settings_radio">Show radio</string>
|
<string name="settings_radio">Show radio</string>
|
||||||
<string name="settings_radio_summary">If enabled, show the radio section. Restart the app for it to take full effect.</string>
|
<string name="settings_radio_summary">If enabled, show the radio section. Restart the app for it to take full effect.</string>
|
||||||
|
<string name="settings_auto_download_lyrics">Auto download lyrics</string>
|
||||||
|
<string name="settings_auto_download_lyrics_summary">Automatically save lyrics when they are available so they can be shown while offline.</string>
|
||||||
<string name="settings_replay_gain">Set replay gain mode</string>
|
<string name="settings_replay_gain">Set replay gain mode</string>
|
||||||
<string name="settings_rounded_corner">Rounded corners</string>
|
<string name="settings_rounded_corner">Rounded corners</string>
|
||||||
<string name="settings_rounded_corner_size">Corners size</string>
|
<string name="settings_rounded_corner_size">Corners size</string>
|
||||||
|
|
@ -346,6 +374,8 @@
|
||||||
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
|
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
|
||||||
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for donwloads\" is set to \"Direct download\" the bitrate of the file will not be changed.</string>
|
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for donwloads\" is set to \"Direct download\" the bitrate of the file will not be changed.</string>
|
||||||
<string name="settings_summary_transcoding_estimate_content_length">When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer.</string>
|
<string name="settings_summary_transcoding_estimate_content_length">When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer.</string>
|
||||||
|
<string name="settings_sync_starred_artists_for_offline_use_summary">If enabled, starred artists will be downloaded for offline use.</string>
|
||||||
|
<string name="settings_sync_starred_artists_for_offline_use_title">Sync starred artists for offline use</string>
|
||||||
<string name="settings_sync_starred_albums_for_offline_use_summary">If enabled, starred albums will be downloaded for offline use.</string>
|
<string name="settings_sync_starred_albums_for_offline_use_summary">If enabled, starred albums will be downloaded for offline use.</string>
|
||||||
<string name="settings_sync_starred_albums_for_offline_use_title">Sync starred albums for offline use</string>
|
<string name="settings_sync_starred_albums_for_offline_use_title">Sync starred albums for offline use</string>
|
||||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
|
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
|
||||||
|
|
@ -353,6 +383,7 @@
|
||||||
<string name="settings_theme">Theme</string>
|
<string name="settings_theme">Theme</string>
|
||||||
<string name="settings_title_data">Data</string>
|
<string name="settings_title_data">Data</string>
|
||||||
<string name="settings_title_general">General</string>
|
<string name="settings_title_general">General</string>
|
||||||
|
<string name="settings_title_playlist">Playlist</string>
|
||||||
<string name="settings_title_rating">Rating</string>
|
<string name="settings_title_rating">Rating</string>
|
||||||
<string name="settings_title_replay_gain">Replay Gain</string>
|
<string name="settings_title_replay_gain">Replay Gain</string>
|
||||||
<string name="settings_title_scrobble">Scrobble</string>
|
<string name="settings_title_scrobble">Scrobble</string>
|
||||||
|
|
@ -403,6 +434,8 @@
|
||||||
<string name="starred_sync_dialog_positive_button">Continue and download</string>
|
<string name="starred_sync_dialog_positive_button">Continue and download</string>
|
||||||
<string name="starred_sync_dialog_summary">Downloading starred tracks may require a large amount of data.</string>
|
<string name="starred_sync_dialog_summary">Downloading starred tracks may require a large amount of data.</string>
|
||||||
<string name="starred_sync_dialog_title">Sync starred tracks</string>
|
<string name="starred_sync_dialog_title">Sync starred tracks</string>
|
||||||
|
<string name="starred_artist_sync_dialog_summary">Downloading starred artists may require a large amount of data.</string>
|
||||||
|
<string name="starred_artist_sync_dialog_title">Sync starred artists</string>
|
||||||
<string name="starred_album_sync_dialog_summary">Downloading starred albums may require a large amount of data.</string>
|
<string name="starred_album_sync_dialog_summary">Downloading starred albums may require a large amount of data.</string>
|
||||||
<string name="starred_album_sync_dialog_title">Sync starred albums</string>
|
<string name="starred_album_sync_dialog_title">Sync starred albums</string>
|
||||||
<string name="streaming_cache_storage_dialog_sub_summary">For the changes to take effect, restart the app.</string>
|
<string name="streaming_cache_storage_dialog_sub_summary">For the changes to take effect, restart the app.</string>
|
||||||
|
|
@ -410,7 +443,7 @@
|
||||||
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
|
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
|
||||||
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
|
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
|
||||||
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
|
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
|
||||||
<string name="support_url">https://buymeacoffee.com/a.cappiello</string>
|
<string name="support_url">https://ko-fi.com/eddyizm</string>
|
||||||
<string name="track_info_album">Album</string>
|
<string name="track_info_album">Album</string>
|
||||||
<string name="track_info_artist">Artist</string>
|
<string name="track_info_artist">Artist</string>
|
||||||
<string name="track_info_bit_depth">Bit depth</string>
|
<string name="track_info_bit_depth">Bit depth</string>
|
||||||
|
|
@ -439,10 +472,29 @@
|
||||||
<string name="undraw_page">unDraw</string>
|
<string name="undraw_page">unDraw</string>
|
||||||
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
||||||
<string name="undraw_url">https://undraw.co/</string>
|
<string name="undraw_url">https://undraw.co/</string>
|
||||||
|
<string name="widget_label">Tempo Widget</string>
|
||||||
|
<string name="widget_not_playing">Not playing</string>
|
||||||
|
<string name="widget_placeholder_subtitle">Open Tempo</string>
|
||||||
|
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||||
|
<string name="widget_time_duration_placeholder">0:00</string>
|
||||||
|
<string name="widget_content_desc_album_art">Album artwork</string>
|
||||||
|
<string name="widget_content_desc_play_pause">Play or pause</string>
|
||||||
|
<string name="widget_content_desc_next">Next track</string>
|
||||||
|
<string name="widget_content_desc_prev">Previous track</string>
|
||||||
|
<string name="widget_content_desc_shuffle">Toggle shuffle</string>
|
||||||
|
<string name="widget_content_desc_repeat">Change repeat mode</string>
|
||||||
<plurals name="home_sync_starred_albums_count">
|
<plurals name="home_sync_starred_albums_count">
|
||||||
<item quantity="one">%d album to sync</item>
|
<item quantity="one">%d album to sync</item>
|
||||||
<item quantity="other">%d albums to sync</item>
|
<item quantity="other">%d albums to sync</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="home_sync_starred_artists_count">
|
||||||
|
<item quantity="one">%d artist to sync</item>
|
||||||
|
<item quantity="other">%d artists to sync</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="songs_download_started">
|
||||||
|
<item quantity="one">Downloading %d song</item>
|
||||||
|
<item quantity="other">Downloading %d songs</item>
|
||||||
|
</plurals>
|
||||||
<string name="equalizer_fragment_title">Equalizer</string>
|
<string name="equalizer_fragment_title">Equalizer</string>
|
||||||
<string name="equalizer_reset">Reset</string>
|
<string name="equalizer_reset">Reset</string>
|
||||||
<string name="equalizer_enable">Enable</string>
|
<string name="equalizer_enable">Enable</string>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,10 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<PreferenceCategory app:title="@string/settings_title_general">
|
<PreferenceCategory app:title="@string/settings_title_general">
|
||||||
<Preference
|
<Preference
|
||||||
android:key="system_equalizer"
|
android:layout_height="match_parent"
|
||||||
android:title="@string/settings_system_equalizer_title"
|
android:key="equalizer"
|
||||||
android:summary="@string/settings_system_equalizer_summary" />
|
android:summary="@string/settings_system_equalizer_summary"
|
||||||
|
android:title="@string/settings_system_equalizer_title" />
|
||||||
<Preference
|
|
||||||
android:key="app_equalizer"
|
|
||||||
android:title="@string/settings_app_equalizer"
|
|
||||||
android:summary="@string/settings_app_equalizer_summary" />
|
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:key="scan_library"
|
android:key="scan_library"
|
||||||
|
|
@ -22,12 +18,14 @@
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/settings_title_ui">
|
<PreferenceCategory app:title="@string/settings_title_ui">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
|
android:layout_height="match_parent"
|
||||||
app:defaultValue="default"
|
app:defaultValue="default"
|
||||||
app:dialogTitle="@string/settings_language"
|
app:dialogTitle="@string/settings_language"
|
||||||
app:key="language"
|
app:key="language"
|
||||||
app:title="@string/settings_language"/>
|
app:title="@string/settings_language" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
|
android:layout_height="wrap_content"
|
||||||
app:defaultValue="default"
|
app:defaultValue="default"
|
||||||
app:dialogTitle="@string/settings_theme"
|
app:dialogTitle="@string/settings_theme"
|
||||||
app:entries="@array/theme_list_titles"
|
app:entries="@array/theme_list_titles"
|
||||||
|
|
@ -42,10 +40,11 @@
|
||||||
android:key="always_on_display" />
|
android:key="always_on_display" />
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:title="@string/settings_rounded_corner"
|
android:layout_height="match_parent"
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
|
android:key="rounded_corner"
|
||||||
android:summary="@string/settings_rounded_corner_summary"
|
android:summary="@string/settings_rounded_corner_summary"
|
||||||
android:key="rounded_corner" />
|
android:title="@string/settings_rounded_corner" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:defaultValue="6"
|
app:defaultValue="6"
|
||||||
|
|
@ -57,10 +56,11 @@
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:title="@string/settings_audio_quality"
|
android:layout_height="wrap_content"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
android:key="audio_quality_per_item"
|
||||||
android:summary="@string/settings_audio_quality_summary"
|
android:summary="@string/settings_audio_quality_summary"
|
||||||
android:key="audio_quality_per_item" />
|
android:title="@string/settings_audio_quality" />
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:title="@string/settings_song_rating"
|
android:title="@string/settings_song_rating"
|
||||||
|
|
@ -86,13 +86,35 @@
|
||||||
android:summary="@string/settings_radio_summary"
|
android:summary="@string/settings_radio_summary"
|
||||||
android:key="radio_section_visibility" />
|
android:key="radio_section_visibility" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:title="@string/settings_auto_download_lyrics"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:summary="@string/settings_auto_download_lyrics_summary"
|
||||||
|
android:key="auto_download_lyrics" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:title="@string/settings_show_mini_shuffle_button"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:summary="@string/settings_show_mini_shuffle_button_summary"
|
||||||
|
android:key="mini_shuffle_button_visibility" />
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:title="@string/settings_music_directory"
|
android:title="@string/settings_music_directory"
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:summary="@string/settings_music_directory_summary"
|
android:summary="@string/settings_music_directory_summary"
|
||||||
android:key="music_directory_section_visibility" />
|
android:key="music_directory_section_visibility" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory app:title="@string/settings_title_playlist">
|
||||||
|
<SwitchPreference
|
||||||
|
android:title="@string/settings_allow_playlist_duplicates"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:summary="@string/settings_allow_playlist_duplicates_summary"
|
||||||
|
android:key="allow_playlist_duplicates" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/settings_title_data">
|
<PreferenceCategory app:title="@string/settings_title_data">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:defaultValue="256"
|
app:defaultValue="256"
|
||||||
|
|
@ -150,6 +172,12 @@
|
||||||
android:summary="@string/settings_sync_starred_albums_for_offline_use_summary"
|
android:summary="@string/settings_sync_starred_albums_for_offline_use_summary"
|
||||||
android:key="sync_starred_albums_for_offline_use" />
|
android:key="sync_starred_albums_for_offline_use" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:title="@string/settings_sync_starred_artists_for_offline_use_title"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:summary="@string/settings_sync_starred_artists_for_offline_use_summary"
|
||||||
|
android:key="sync_starred_artists_for_offline_use" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:defaultValue="1"
|
app:defaultValue="1"
|
||||||
app:dialogTitle="@string/settings_buffering_strategy"
|
app:dialogTitle="@string/settings_buffering_strategy"
|
||||||
|
|
@ -168,6 +196,14 @@
|
||||||
android:key="download_storage"
|
android:key="download_storage"
|
||||||
app:title="@string/settings_download_storage_title" />
|
app:title="@string/settings_download_storage_title" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="set_download_directory"
|
||||||
|
android:title="Set download folder"
|
||||||
|
android:summary="Choose a folder for downloaded music files"
|
||||||
|
android:icon="@drawable/ic_folder"
|
||||||
|
android:order="104"
|
||||||
|
app:isPreferenceVisible="false" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:key="delete_download_storage"
|
android:key="delete_download_storage"
|
||||||
app:title="@string/settings_delete_download_storage_title"
|
app:title="@string/settings_delete_download_storage_title"
|
||||||
|
|
@ -364,6 +400,14 @@
|
||||||
android:data="@string/settings_github_link" />
|
android:data="@string/settings_github_link" />
|
||||||
</Preference>
|
</Preference>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:summary="@string/settings_support_summary"
|
||||||
|
app:title="@string/settings_support_title">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:data="@string/settings_support_discussion_link" />
|
||||||
|
</Preference>
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:summary="@string/undraw_thanks"
|
app:summary="@string/undraw_thanks"
|
||||||
app:title="@string/undraw_page">
|
app:title="@string/undraw_page">
|
||||||
|
|
|
||||||
10
app/src/main/res/xml/widget_info.xml
Normal file
10
app/src/main/res/xml/widget_info.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="250dp"
|
||||||
|
android:minHeight="64dp"
|
||||||
|
android:updatePeriodMillis="0"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:initialLayout="@layout/widget_layout_compact"
|
||||||
|
android:previewImage="@drawable/ic_splash_logo"
|
||||||
|
android:previewLayout="@layout/widget_preview_compact"
|
||||||
|
android:widgetCategory="home_screen|keyguard" />
|
||||||
|
|
@ -8,6 +8,8 @@ import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.media3.common.*
|
import androidx.media3.common.*
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.DefaultLoadControl
|
import androidx.media3.exoplayer.DefaultLoadControl
|
||||||
|
|
@ -23,6 +25,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil
|
||||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||||
import com.cappielloantonio.tempo.util.Preferences
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||||
|
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
|
@ -39,6 +42,18 @@ class MediaService : MediaLibraryService() {
|
||||||
lateinit var equalizerManager: EqualizerManager
|
lateinit var equalizerManager: EqualizerManager
|
||||||
|
|
||||||
private var customLayout = ImmutableList.of<CommandButton>()
|
private var customLayout = ImmutableList.of<CommandButton>()
|
||||||
|
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var widgetUpdateScheduled = false
|
||||||
|
private val widgetUpdateRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (!player.isPlaying) {
|
||||||
|
widgetUpdateScheduled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateWidget()
|
||||||
|
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner class LocalBinder : Binder() {
|
inner class LocalBinder : Binder() {
|
||||||
fun getEqualizerManager(): EqualizerManager {
|
fun getEqualizerManager(): EqualizerManager {
|
||||||
|
|
@ -80,6 +95,7 @@ class MediaService : MediaLibraryService() {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
equalizerManager.release()
|
equalizerManager.release()
|
||||||
|
stopWidgetUpdates()
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
@ -260,6 +276,7 @@ class MediaService : MediaLibraryService() {
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||||
}
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
|
|
@ -282,6 +299,12 @@ class MediaService : MediaLibraryService() {
|
||||||
} else {
|
} else {
|
||||||
MediaManager.scrobble(player.currentMediaItem, false)
|
MediaManager.scrobble(player.currentMediaItem, false)
|
||||||
}
|
}
|
||||||
|
if (isPlaying) {
|
||||||
|
scheduleWidgetUpdates()
|
||||||
|
} else {
|
||||||
|
stopWidgetUpdates()
|
||||||
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
|
@ -293,6 +316,7 @@ class MediaService : MediaLibraryService() {
|
||||||
MediaManager.scrobble(player.currentMediaItem, true)
|
MediaManager.scrobble(player.currentMediaItem, true)
|
||||||
MediaManager.saveChronology(player.currentMediaItem)
|
MediaManager.saveChronology(player.currentMediaItem)
|
||||||
}
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
|
|
@ -326,6 +350,9 @@ class MediaService : MediaLibraryService() {
|
||||||
mediaLibrarySession.setCustomLayout(customLayout)
|
mediaLibrarySession.setCustomLayout(customLayout)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (player.isPlaying) {
|
||||||
|
scheduleWidgetUpdates()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPlayer(player: Player) {
|
private fun setPlayer(player: Player) {
|
||||||
|
|
@ -386,5 +413,46 @@ class MediaService : MediaLibraryService() {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateWidget() {
|
||||||
|
val mi = player.currentMediaItem
|
||||||
|
val title = mi?.mediaMetadata?.title?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||||
|
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||||
|
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||||
|
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||||
|
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
|
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
|
WidgetUpdateManager.updateFromState(
|
||||||
|
this,
|
||||||
|
title ?: "",
|
||||||
|
artist ?: "",
|
||||||
|
album ?: "",
|
||||||
|
coverId,
|
||||||
|
player.isPlaying,
|
||||||
|
player.shuffleModeEnabled,
|
||||||
|
player.repeatMode,
|
||||||
|
position,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleWidgetUpdates() {
|
||||||
|
if (widgetUpdateScheduled) return
|
||||||
|
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||||
|
widgetUpdateScheduled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopWidgetUpdates() {
|
||||||
|
if (!widgetUpdateScheduled) return
|
||||||
|
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||||
|
widgetUpdateScheduled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import android.app.TaskStackBuilder
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.media3.cast.CastPlayer
|
import androidx.media3.cast.CastPlayer
|
||||||
import androidx.media3.cast.SessionAvailabilityListener
|
import androidx.media3.cast.SessionAvailabilityListener
|
||||||
import androidx.media3.common.AudioAttributes
|
import androidx.media3.common.AudioAttributes
|
||||||
|
|
@ -25,6 +27,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil
|
||||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||||
import com.cappielloantonio.tempo.util.Preferences
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||||
|
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
|
|
@ -49,6 +52,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||||
}
|
}
|
||||||
|
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var widgetUpdateScheduled = false
|
||||||
|
private val widgetUpdateRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (!player.isPlaying) {
|
||||||
|
widgetUpdateScheduled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateWidget()
|
||||||
|
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
@ -80,6 +95,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
equalizerManager.release()
|
equalizerManager.release()
|
||||||
|
stopWidgetUpdates()
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
@ -161,6 +177,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||||
}
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
|
|
@ -183,6 +200,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
} else {
|
} else {
|
||||||
MediaManager.scrobble(player.currentMediaItem, false)
|
MediaManager.scrobble(player.currentMediaItem, false)
|
||||||
}
|
}
|
||||||
|
if (isPlaying) {
|
||||||
|
scheduleWidgetUpdates()
|
||||||
|
} else {
|
||||||
|
stopWidgetUpdates()
|
||||||
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
|
@ -195,6 +218,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
MediaManager.scrobble(player.currentMediaItem, true)
|
MediaManager.scrobble(player.currentMediaItem, true)
|
||||||
MediaManager.saveChronology(player.currentMediaItem)
|
MediaManager.saveChronology(player.currentMediaItem)
|
||||||
}
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
|
|
@ -230,6 +254,47 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (player.isPlaying) {
|
||||||
|
scheduleWidgetUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWidget() {
|
||||||
|
val mi = player.currentMediaItem
|
||||||
|
val title = mi?.mediaMetadata?.title?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||||
|
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||||
|
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||||
|
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||||
|
|
||||||
|
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
|
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
|
WidgetUpdateManager.updateFromState(
|
||||||
|
this,
|
||||||
|
title ?: "",
|
||||||
|
artist ?: "",
|
||||||
|
album ?: "",
|
||||||
|
coverId,
|
||||||
|
player.isPlaying,
|
||||||
|
player.shuffleModeEnabled,
|
||||||
|
player.repeatMode,
|
||||||
|
position,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleWidgetUpdates() {
|
||||||
|
if (widgetUpdateScheduled) return
|
||||||
|
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||||
|
widgetUpdateScheduled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopWidgetUpdates() {
|
||||||
|
if (!widgetUpdateScheduled) return
|
||||||
|
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||||
|
widgetUpdateScheduled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeLoadControl(): DefaultLoadControl {
|
private fun initializeLoadControl(): DefaultLoadControl {
|
||||||
|
|
@ -294,3 +359,5 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
player.prepare()
|
player.prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,42 @@ package com.cappielloantonio.tempo.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.concurrent.futures.CallbackToFutureAdapter
|
||||||
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Rating
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.session.CommandButton
|
import androidx.media3.session.CommandButton
|
||||||
import androidx.media3.session.LibraryResult
|
import androidx.media3.session.LibraryResult
|
||||||
|
import androidx.media3.session.MediaConstants
|
||||||
import androidx.media3.session.MediaLibraryService
|
import androidx.media3.session.MediaLibraryService
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.session.SessionCommand
|
import androidx.media3.session.SessionCommand
|
||||||
|
import androidx.media3.session.SessionError
|
||||||
import androidx.media3.session.SessionResult
|
import androidx.media3.session.SessionResult
|
||||||
|
import com.cappielloantonio.tempo.App
|
||||||
import com.cappielloantonio.tempo.R
|
import com.cappielloantonio.tempo.R
|
||||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||||
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_HEART_LOADING
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
open class MediaLibrarySessionCallback(
|
open class MediaLibrarySessionCallback(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -28,82 +49,244 @@ open class MediaLibrarySessionCallback(
|
||||||
MediaBrowserTree.initialize(automotiveRepository)
|
MediaBrowserTree.initialize(automotiveRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val shuffleCommandButtons: List<CommandButton> = listOf(
|
private val customCommandToggleShuffleModeOn = CommandButton.Builder()
|
||||||
CommandButton.Builder()
|
|
||||||
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
|
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
|
||||||
.setSessionCommand(
|
.setSessionCommand(
|
||||||
SessionCommand(
|
SessionCommand(
|
||||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY
|
||||||
)
|
)
|
||||||
).setIconResId(R.drawable.exo_icon_shuffle_off).build(),
|
).setIconResId(R.drawable.exo_icon_shuffle_off).build()
|
||||||
|
|
||||||
CommandButton.Builder()
|
private val customCommandToggleShuffleModeOff = CommandButton.Builder()
|
||||||
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
|
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
|
||||||
.setSessionCommand(
|
.setSessionCommand(
|
||||||
SessionCommand(
|
SessionCommand(
|
||||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY
|
||||||
)
|
)
|
||||||
).setIconResId(R.drawable.exo_icon_shuffle_on).build()
|
).setIconResId(R.drawable.exo_icon_shuffle_on).build()
|
||||||
)
|
|
||||||
|
|
||||||
private val repeatCommandButtons: List<CommandButton> = listOf(
|
private val customCommandToggleRepeatModeOff = CommandButton.Builder()
|
||||||
CommandButton.Builder()
|
|
||||||
.setDisplayName(context.getString(R.string.exo_controls_repeat_off_description))
|
.setDisplayName(context.getString(R.string.exo_controls_repeat_off_description))
|
||||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY))
|
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY))
|
||||||
.setIconResId(R.drawable.exo_icon_repeat_off)
|
.setIconResId(R.drawable.exo_icon_repeat_off)
|
||||||
.build(),
|
.build()
|
||||||
CommandButton.Builder()
|
|
||||||
|
private val customCommandToggleRepeatModeOne = CommandButton.Builder()
|
||||||
.setDisplayName(context.getString(R.string.exo_controls_repeat_one_description))
|
.setDisplayName(context.getString(R.string.exo_controls_repeat_one_description))
|
||||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY))
|
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY))
|
||||||
.setIconResId(R.drawable.exo_icon_repeat_one)
|
.setIconResId(R.drawable.exo_icon_repeat_one)
|
||||||
.build(),
|
.build()
|
||||||
CommandButton.Builder()
|
|
||||||
|
private val customCommandToggleRepeatModeAll = CommandButton.Builder()
|
||||||
.setDisplayName(context.getString(R.string.exo_controls_repeat_all_description))
|
.setDisplayName(context.getString(R.string.exo_controls_repeat_all_description))
|
||||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY))
|
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY))
|
||||||
.setIconResId(R.drawable.exo_icon_repeat_all)
|
.setIconResId(R.drawable.exo_icon_repeat_all)
|
||||||
.build()
|
.build()
|
||||||
)
|
|
||||||
|
|
||||||
private val customLayoutCommandButtons: List<CommandButton> =
|
private val customCommandToggleHeartOn = CommandButton.Builder()
|
||||||
shuffleCommandButtons + repeatCommandButtons
|
.setDisplayName(context.getString(R.string.exo_controls_heart_on_description))
|
||||||
|
.setSessionCommand(
|
||||||
|
SessionCommand(
|
||||||
|
CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setIconResId(R.drawable.ic_favorite)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val customCommandToggleHeartOff = CommandButton.Builder()
|
||||||
|
.setDisplayName(context.getString(R.string.exo_controls_heart_off_description))
|
||||||
|
.setSessionCommand(
|
||||||
|
SessionCommand(CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY)
|
||||||
|
)
|
||||||
|
.setIconResId(R.drawable.ic_favorites_outlined)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Fake Command while waiting for like update command
|
||||||
|
private val customCommandToggleHeartLoading = CommandButton.Builder()
|
||||||
|
.setDisplayName(context.getString(R.string.cast_expanded_controller_loading))
|
||||||
|
.setSessionCommand(
|
||||||
|
SessionCommand(CUSTOM_COMMAND_TOGGLE_HEART_LOADING, Bundle.EMPTY)
|
||||||
|
)
|
||||||
|
.setIconResId(R.drawable.ic_bookmark_sync)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val customLayoutCommandButtons = listOf(
|
||||||
|
customCommandToggleShuffleModeOn,
|
||||||
|
customCommandToggleShuffleModeOff,
|
||||||
|
customCommandToggleRepeatModeOff,
|
||||||
|
customCommandToggleRepeatModeOne,
|
||||||
|
customCommandToggleRepeatModeAll,
|
||||||
|
customCommandToggleHeartOn,
|
||||||
|
customCommandToggleHeartOff,
|
||||||
|
customCommandToggleHeartLoading,
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
val mediaNotificationSessionCommands =
|
val mediaNotificationSessionCommands =
|
||||||
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
||||||
.also { builder ->
|
.also { builder ->
|
||||||
(shuffleCommandButtons + repeatCommandButtons).forEach { commandButton ->
|
customLayoutCommandButtons.forEach { commandButton ->
|
||||||
commandButton.sessionCommand?.let { builder.add(it) }
|
commandButton.sessionCommand?.let { builder.add(it) }
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
|
|
||||||
val shuffle = shuffleCommandButtons[if (player.shuffleModeEnabled) 1 else 0]
|
|
||||||
val repeat = when (player.repeatMode) {
|
|
||||||
Player.REPEAT_MODE_ONE -> repeatCommandButtons[1]
|
|
||||||
Player.REPEAT_MODE_ALL -> repeatCommandButtons[2]
|
|
||||||
else -> repeatCommandButtons[0]
|
|
||||||
}
|
|
||||||
return ImmutableList.of(shuffle, repeat)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onConnect(
|
override fun onConnect(
|
||||||
session: MediaSession, controller: MediaSession.ControllerInfo
|
session: MediaSession, controller: MediaSession.ControllerInfo
|
||||||
): MediaSession.ConnectionResult {
|
): MediaSession.ConnectionResult {
|
||||||
|
session.player.addListener(object : Player.Listener {
|
||||||
|
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// FIXME: I'm not sure this if is required anymore
|
||||||
if (session.isMediaNotificationController(controller) || session.isAutomotiveController(
|
if (session.isMediaNotificationController(controller) || session.isAutomotiveController(
|
||||||
controller
|
controller
|
||||||
) || session.isAutoCompanionController(controller)
|
) || session.isAutoCompanionController(controller)
|
||||||
) {
|
) {
|
||||||
val customLayout = buildCustomLayout(session.player)
|
|
||||||
|
|
||||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||||
.setAvailableSessionCommands(mediaNotificationSessionCommands)
|
.setAvailableSessionCommands(mediaNotificationSessionCommands)
|
||||||
.setCustomLayout(customLayout).build()
|
.setCustomLayout(buildCustomLayout(session.player))
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the mediaNotification after some changes
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun updateMediaNotificationCustomLayout(
|
||||||
|
session: MediaSession,
|
||||||
|
isRatingPending: Boolean = false
|
||||||
|
) {
|
||||||
|
session.setCustomLayout(
|
||||||
|
session.mediaNotificationControllerInfo!!,
|
||||||
|
buildCustomLayout(session.player, isRatingPending)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCustomLayout(player: Player, isRatingPending: Boolean = false): ImmutableList<CommandButton> {
|
||||||
|
val customLayout = mutableListOf<CommandButton>()
|
||||||
|
|
||||||
|
val showShuffle = Preferences.showShuffleInsteadOfHeart()
|
||||||
|
|
||||||
|
if (!showShuffle) {
|
||||||
|
if (player.currentMediaItem != null && !isRatingPending) {
|
||||||
|
if ((player.mediaMetadata.userRating as HeartRating?)?.isHeart == true) {
|
||||||
|
customLayout.add(customCommandToggleHeartOn)
|
||||||
|
} else {
|
||||||
|
customLayout.add(customCommandToggleHeartOff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customLayout.add(
|
||||||
|
if (player.shuffleModeEnabled) customCommandToggleShuffleModeOff else customCommandToggleShuffleModeOn
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add repeat button
|
||||||
|
val repeatButton = when (player.repeatMode) {
|
||||||
|
Player.REPEAT_MODE_ONE -> customCommandToggleRepeatModeOne
|
||||||
|
Player.REPEAT_MODE_ALL -> customCommandToggleRepeatModeAll
|
||||||
|
else -> customCommandToggleRepeatModeOff
|
||||||
|
}
|
||||||
|
|
||||||
|
customLayout.add(repeatButton)
|
||||||
|
return ImmutableList.copyOf(customLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting rating without a mediaId will set the currently listened mediaId
|
||||||
|
override fun onSetRating(
|
||||||
|
session: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo,
|
||||||
|
rating: Rating
|
||||||
|
): ListenableFuture<SessionResult> {
|
||||||
|
return onSetRating(session, controller, session.player.currentMediaItem!!.mediaId, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetRating(
|
||||||
|
session: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo,
|
||||||
|
mediaId: String,
|
||||||
|
rating: Rating
|
||||||
|
): ListenableFuture<SessionResult> {
|
||||||
|
val isStaring = (rating as HeartRating).isHeart
|
||||||
|
|
||||||
|
val networkCall = if (isStaring)
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.mediaAnnotationClient
|
||||||
|
.star(mediaId, null, null)
|
||||||
|
else
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.mediaAnnotationClient
|
||||||
|
.unstar(mediaId, null, null)
|
||||||
|
|
||||||
|
return CallbackToFutureAdapter.getFuture { completer ->
|
||||||
|
networkCall.enqueue(object : Callback<ApiResponse?> {
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ApiResponse?>,
|
||||||
|
response: Response<ApiResponse?>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
|
||||||
|
// Search if the media item in the player should be updated
|
||||||
|
for (i in 0 until session.player.mediaItemCount) {
|
||||||
|
val mediaItem = session.player.getMediaItemAt(i)
|
||||||
|
if (mediaItem.mediaId == mediaId) {
|
||||||
|
val newMetadata = mediaItem.mediaMetadata.buildUpon()
|
||||||
|
.setUserRating(HeartRating(isStaring)).build()
|
||||||
|
session.player.replaceMediaItem(
|
||||||
|
i,
|
||||||
|
mediaItem.buildUpon().setMediaMetadata(newMetadata).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
completer.set(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||||
|
} else {
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
completer.set(
|
||||||
|
SessionResult(
|
||||||
|
SessionError(
|
||||||
|
response.code(),
|
||||||
|
response.message()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onFailure(call: Call<ApiResponse?>, t: Throwable) {
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
completer.set(
|
||||||
|
SessionResult(
|
||||||
|
SessionError(
|
||||||
|
SessionError.ERROR_UNKNOWN,
|
||||||
|
"An error as occurred"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onCustomCommand(
|
override fun onCustomCommand(
|
||||||
session: MediaSession,
|
session: MediaSession,
|
||||||
|
|
@ -111,9 +294,23 @@ open class MediaLibrarySessionCallback(
|
||||||
customCommand: SessionCommand,
|
customCommand: SessionCommand,
|
||||||
args: Bundle
|
args: Bundle
|
||||||
): ListenableFuture<SessionResult> {
|
): ListenableFuture<SessionResult> {
|
||||||
|
|
||||||
|
val mediaItemId = args.getString(
|
||||||
|
MediaConstants.EXTRA_KEY_MEDIA_ID,
|
||||||
|
session.player.currentMediaItem?.mediaId
|
||||||
|
)
|
||||||
|
|
||||||
when (customCommand.customAction) {
|
when (customCommand.customAction) {
|
||||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> {
|
||||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
|
session.player.shuffleModeEnabled = true
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||||
|
}
|
||||||
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> {
|
||||||
|
session.player.shuffleModeEnabled = false
|
||||||
|
updateMediaNotificationCustomLayout(session)
|
||||||
|
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||||
|
}
|
||||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
||||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
||||||
|
|
@ -123,17 +320,32 @@ open class MediaLibrarySessionCallback(
|
||||||
else -> Player.REPEAT_MODE_OFF
|
else -> Player.REPEAT_MODE_OFF
|
||||||
}
|
}
|
||||||
session.player.repeatMode = nextMode
|
session.player.repeatMode = nextMode
|
||||||
}
|
updateMediaNotificationCustomLayout(session)
|
||||||
else -> return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
|
|
||||||
}
|
|
||||||
|
|
||||||
session.setCustomLayout(
|
|
||||||
session.mediaNotificationControllerInfo!!,
|
|
||||||
buildCustomLayout(session.player)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||||
}
|
}
|
||||||
|
CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||||
|
CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
|
||||||
|
val currentRating = session.player.mediaMetadata.userRating as? HeartRating
|
||||||
|
val isCurrentlyLiked = currentRating?.isHeart ?: false
|
||||||
|
|
||||||
|
val newLikedState = !isCurrentlyLiked
|
||||||
|
|
||||||
|
updateMediaNotificationCustomLayout(
|
||||||
|
session,
|
||||||
|
isRatingPending = true // Show loading state
|
||||||
|
)
|
||||||
|
return onSetRating(session, controller, HeartRating(newLikedState))
|
||||||
|
}
|
||||||
|
else -> return Futures.immediateFuture(
|
||||||
|
SessionResult(
|
||||||
|
SessionError(
|
||||||
|
SessionError.ERROR_NOT_SUPPORTED,
|
||||||
|
customCommand.customAction
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGetLibraryRoot(
|
override fun onGetLibraryRoot(
|
||||||
session: MediaLibraryService.MediaLibrarySession,
|
session: MediaLibraryService.MediaLibrarySession,
|
||||||
|
|
@ -186,17 +398,4 @@ open class MediaLibrarySessionCallback(
|
||||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
return MediaBrowserTree.search(query)
|
return MediaBrowserTree.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
|
||||||
"android.media3.session.demo.SHUFFLE_ON"
|
|
||||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
|
|
||||||
"android.media3.session.demo.SHUFFLE_OFF"
|
|
||||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
|
|
||||||
"android.media3.session.demo.REPEAT_OFF"
|
|
||||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
|
|
||||||
"android.media3.session.demo.REPEAT_ONE"
|
|
||||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
|
||||||
"android.media3.session.demo.REPEAT_ALL"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,8 @@ import android.app.TaskStackBuilder
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.media3.cast.CastPlayer
|
import androidx.media3.cast.CastPlayer
|
||||||
import androidx.media3.cast.SessionAvailabilityListener
|
import androidx.media3.cast.SessionAvailabilityListener
|
||||||
import androidx.media3.common.AudioAttributes
|
import androidx.media3.common.AudioAttributes
|
||||||
|
|
@ -25,6 +27,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil
|
||||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||||
import com.cappielloantonio.tempo.util.Preferences
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||||
|
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
|
|
@ -49,6 +52,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||||
}
|
}
|
||||||
|
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var widgetUpdateScheduled = false
|
||||||
|
private val widgetUpdateRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (!player.isPlaying) {
|
||||||
|
widgetUpdateScheduled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateWidget()
|
||||||
|
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
@ -80,6 +95,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
equalizerManager.release()
|
equalizerManager.release()
|
||||||
|
stopWidgetUpdates()
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
@ -161,6 +177,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||||
}
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
|
|
@ -184,6 +201,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
} else {
|
} else {
|
||||||
MediaManager.scrobble(player.currentMediaItem, false)
|
MediaManager.scrobble(player.currentMediaItem, false)
|
||||||
}
|
}
|
||||||
|
if (isPlaying) {
|
||||||
|
scheduleWidgetUpdates()
|
||||||
|
} else {
|
||||||
|
stopWidgetUpdates()
|
||||||
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
|
@ -196,6 +219,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
MediaManager.scrobble(player.currentMediaItem, true)
|
MediaManager.scrobble(player.currentMediaItem, true)
|
||||||
MediaManager.saveChronology(player.currentMediaItem)
|
MediaManager.saveChronology(player.currentMediaItem)
|
||||||
}
|
}
|
||||||
|
updateWidget()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
|
|
@ -219,18 +243,52 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||||
mediaLibrarySession.setCustomLayout(
|
|
||||||
librarySessionCallback.buildCustomLayout(player)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||||
Preferences.setRepeatMode(repeatMode)
|
Preferences.setRepeatMode(repeatMode)
|
||||||
mediaLibrarySession.setCustomLayout(
|
|
||||||
librarySessionCallback.buildCustomLayout(player)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (player.isPlaying) {
|
||||||
|
scheduleWidgetUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWidget() {
|
||||||
|
val mi = player.currentMediaItem
|
||||||
|
val title = mi?.mediaMetadata?.title?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||||
|
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||||
|
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||||
|
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||||
|
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||||
|
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
|
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
|
WidgetUpdateManager.updateFromState(
|
||||||
|
this,
|
||||||
|
title ?: "",
|
||||||
|
artist ?: "",
|
||||||
|
album ?: "",
|
||||||
|
coverId,
|
||||||
|
player.isPlaying,
|
||||||
|
player.shuffleModeEnabled,
|
||||||
|
player.repeatMode,
|
||||||
|
position,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleWidgetUpdates() {
|
||||||
|
if (widgetUpdateScheduled) return
|
||||||
|
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||||
|
widgetUpdateScheduled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopWidgetUpdates() {
|
||||||
|
if (!widgetUpdateScheduled) return
|
||||||
|
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||||
|
widgetUpdateScheduled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeLoadControl(): DefaultLoadControl {
|
private fun initializeLoadControl(): DefaultLoadControl {
|
||||||
|
|
@ -294,3 +352,5 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
player.prepare()
|
player.prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||||
|
|
|
||||||
0
notes
Normal file
0
notes
Normal file
Loading…
Add table
Add a link
Reference in a new issue