Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a06ab77b42 | ||
|
|
04a6176bfd | ||
|
|
1f4464e089 | ||
|
|
9d01d2057a | ||
|
|
ad440c490a | ||
|
|
acdcfff9ac | ||
|
|
8c7a25cbd0 | ||
|
|
bdca5e16ed | ||
|
|
f091b3d248 | ||
|
|
18cd84f820 | ||
|
|
281ebf8263 | ||
|
|
2854ac6354 | ||
|
|
16d25a1f1d | ||
|
|
5d3ca8acfa | ||
|
|
0689272046 | ||
|
|
17372fc4d0 | ||
|
|
44679855cd | ||
|
|
78e7032903 | ||
|
|
8d8087f2d6 | ||
|
|
5b6a4fab62 | ||
|
|
fdc41b299c |
2
.github/workflows/github_release.yml
vendored
2
.github/workflows/github_release.yml
vendored
@@ -3,7 +3,7 @@ name: Github Release Workflow
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@
|
|||||||
# release / debug files
|
# release / debug files
|
||||||
tempus-release-key.jks
|
tempus-release-key.jks
|
||||||
app/tempo/
|
app/tempo/
|
||||||
|
app/notquitemy/
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
|
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
|
||||||
|
|
||||||
|
## [3.17.0](https://github.com/eddyizm/tempo/releases/tag/v3.17.0) (2025-10-10)
|
||||||
|
## What's Changed
|
||||||
|
* chore: adding screenshot and docs for 4 icons/buttons in player control by @eddyizm in https://github.com/eddyizm/tempo/pull/162
|
||||||
|
* Update Polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/160
|
||||||
|
* feat: Make all objects in Tempo references for quick access by @le-firehawk in https://github.com/eddyizm/tempo/pull/158
|
||||||
|
* fix: Glide module incorrectly encoding IPv6 addresses by @le-firehawk in https://github.com/eddyizm/tempo/pull/159
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.6...v3.17.0
|
||||||
|
|
||||||
## [3.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08)
|
## [3.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08)
|
||||||
## What's Changed
|
## What's Changed
|
||||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
|
|||||||
## Fork
|
## Fork
|
||||||
|
|
||||||
sha256 signing key fingerprint
|
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`
|
`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.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Please note the two variants in the release assets include release/debug and 32/
|
|||||||
|
|
||||||
As mentioned above, I am working towards a rebrand to get into app stores with a new name an icon.
|
As mentioned above, I am working towards a rebrand to get into app stores with a new name an icon.
|
||||||
|
|
||||||
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
Moved details to [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
|||||||
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
|
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
|
||||||
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
|
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
|
||||||
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
|
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
|
||||||
|
- **Multiple Libraries**: Tempo handles multi-library setups gracefully. They are displayed as Library folders.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (3.9.0)
|
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (3.9.0)
|
||||||
@@ -101,9 +102,9 @@ Thanks to the original repo/creator [CappielloAntonio](https://github.com/Cappie
|
|||||||
|
|
||||||
Please fork and open PR's against the development branch. Make sure your PR builds successfully.
|
Please fork and open PR's against the development branch. Make sure your PR builds successfully.
|
||||||
|
|
||||||
If there is a UI change, please provide a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
|
If there is an UI change, please include a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
|
||||||
|
|
||||||
Currently there are not tests but I would love to start on some unit tests.
|
Currently there are no tests but I would love to start on some unit tests.
|
||||||
|
|
||||||
Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
|
Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
|
||||||
|
|
||||||
|
|||||||
9
USAGE.md
9
USAGE.md
@@ -57,7 +57,14 @@ This app works with any service that implements the Subsonic API, including:
|
|||||||
## Main Features
|
## Main Features
|
||||||
|
|
||||||
### Library View
|
### Library View
|
||||||
**TODO**
|
|
||||||
|
**Multi-library**
|
||||||
|
|
||||||
|
Tempo handles multi-library setups gracefully. They are displayed as Library folders.
|
||||||
|
|
||||||
|
However, if you want to limit or change libraries you could use a workaround, if your server supports it.
|
||||||
|
|
||||||
|
You can create multiple users , one for each library, and save each of them in Tempo app.
|
||||||
|
|
||||||
### Now Playing Screen
|
### Now Playing Screen
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 35
|
versionCode 36
|
||||||
versionName '3.17.0'
|
versionName '3.17.14'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import androidx.room.PrimaryKey
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child
|
import com.cappielloantonio.tempo.subsonic.models.Child
|
||||||
import com.cappielloantonio.tempo.util.Preferences
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Entity(tableName = "chronology")
|
@Entity(tableName = "chronology")
|
||||||
class Chronology(@PrimaryKey override val id: String) : Child(id) {
|
class Chronology(
|
||||||
|
@PrimaryKey override val id: String,
|
||||||
@ColumnInfo(name = "timestamp")
|
@ColumnInfo(name = "timestamp")
|
||||||
var timestamp: Long = System.currentTimeMillis()
|
var timestamp: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
@ColumnInfo(name = "server")
|
@ColumnInfo(name = "server")
|
||||||
var server: String? = null
|
var server: String? = null,
|
||||||
|
) : Child(id) {
|
||||||
constructor(mediaItem: MediaItem) : this(mediaItem.mediaMetadata.extras!!.getString("id")!!) {
|
constructor(mediaItem: MediaItem) : this(mediaItem.mediaMetadata.extras!!.getString("id")!!) {
|
||||||
parentId = mediaItem.mediaMetadata.extras!!.getString("parentId")
|
parentId = mediaItem.mediaMetadata.extras!!.getString("parentId")
|
||||||
isDir = mediaItem.mediaMetadata.extras!!.getBoolean("isDir")
|
isDir = mediaItem.mediaMetadata.extras!!.getBoolean("isDir")
|
||||||
|
|||||||
@@ -10,19 +10,17 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Entity(tableName = "download")
|
@Entity(tableName = "download")
|
||||||
class Download(@PrimaryKey override val id: String) : Child(id) {
|
class Download(
|
||||||
|
@PrimaryKey override val id: String,
|
||||||
@ColumnInfo(name = "playlist_id")
|
@ColumnInfo(name = "playlist_id")
|
||||||
var playlistId: String? = null
|
var playlistId: String? = null,
|
||||||
|
|
||||||
@ColumnInfo(name = "playlist_name")
|
@ColumnInfo(name = "playlist_name")
|
||||||
var playlistName: String? = null
|
var playlistName: String? = null,
|
||||||
|
|
||||||
@ColumnInfo(name = "download_state", defaultValue = "1")
|
@ColumnInfo(name = "download_state", defaultValue = "1")
|
||||||
var downloadState: Int = 0
|
var downloadState: Int = 0,
|
||||||
|
|
||||||
@ColumnInfo(name = "download_uri", defaultValue = "")
|
@ColumnInfo(name = "download_uri", defaultValue = "")
|
||||||
var downloadUri: String? = null
|
var downloadUri: String? = null,
|
||||||
|
) : Child(id) {
|
||||||
constructor(child: Child) : this(child.id) {
|
constructor(child: Child) : this(child.id) {
|
||||||
parentId = child.parentId
|
parentId = child.parentId
|
||||||
isDir = child.isDir
|
isDir = child.isDir
|
||||||
@@ -62,5 +60,5 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
|
|||||||
@Keep
|
@Keep
|
||||||
data class DownloadStack(
|
data class DownloadStack(
|
||||||
var id: String,
|
var id: String,
|
||||||
var view: String?
|
var view: String?,
|
||||||
)
|
)
|
||||||
@@ -10,20 +10,18 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Entity(tableName = "queue")
|
@Entity(tableName = "queue")
|
||||||
class Queue(override val id: String) : Child(id) {
|
class Queue(
|
||||||
|
override val id: String,
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@ColumnInfo(name = "track_order")
|
@ColumnInfo(name = "track_order")
|
||||||
var trackOrder: Int = 0
|
var trackOrder: Int = 0,
|
||||||
|
|
||||||
@ColumnInfo(name = "last_play")
|
@ColumnInfo(name = "last_play")
|
||||||
var lastPlay: Long = 0
|
var lastPlay: Long = 0,
|
||||||
|
|
||||||
@ColumnInfo(name = "playing_changed")
|
@ColumnInfo(name = "playing_changed")
|
||||||
var playingChanged: Long = 0
|
var playingChanged: Long = 0,
|
||||||
|
|
||||||
@ColumnInfo(name = "stream_id")
|
@ColumnInfo(name = "stream_id")
|
||||||
var streamId: String? = null
|
var streamId: String? = null,
|
||||||
|
) : Child(id) {
|
||||||
constructor(child: Child) : this(child.id) {
|
constructor(child: Child) : this(child.id) {
|
||||||
parentId = child.parentId
|
parentId = child.parentId
|
||||||
isDir = child.isDir
|
isDir = child.isDir
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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.interfaces.DecadesCallback;
|
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
|
||||||
@@ -31,14 +32,22 @@ public class AlbumRepository {
|
|||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
if (response.isSuccessful()
|
||||||
|
&& response.body() != null
|
||||||
|
&& response.body().getSubsonicResponse().getAlbumList2() != null
|
||||||
|
&& response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
||||||
|
|
||||||
listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums());
|
listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums());
|
||||||
|
} else {
|
||||||
|
Log.e("AlbumRepository", "API Error on getAlbums. HTTP Code: " + response.code());
|
||||||
|
listLiveAlbums.setValue(new ArrayList<>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
Log.e("AlbumRepository", "Network Failure on getAlbums: " + t.getMessage());
|
||||||
|
listLiveAlbums.setValue(new ArrayList<>());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,28 @@ package com.cappielloantonio.tempo.subsonic
|
|||||||
|
|
||||||
import com.cappielloantonio.tempo.App
|
import com.cappielloantonio.tempo.App
|
||||||
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
||||||
|
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class RetrofitClient(subsonic: Subsonic) {
|
class RetrofitClient(subsonic: Subsonic) {
|
||||||
var retrofit: Retrofit
|
var retrofit: Retrofit
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(Date::class.java, EmptyDateTypeAdapter())
|
||||||
|
.setLenient()
|
||||||
|
.create()
|
||||||
|
|
||||||
retrofit = Retrofit.Builder()
|
retrofit = Retrofit.Builder()
|
||||||
.baseUrl(subsonic.url)
|
.baseUrl(subsonic.url)
|
||||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()))
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
|
|
||||||
.client(getOkHttpClient())
|
.client(getOkHttpClient())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,38 +4,36 @@ import android.os.Parcelable
|
|||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.time.Instant
|
import java.util.Date
|
||||||
import java.time.LocalDate
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class AlbumID3 : Parcelable {
|
open class AlbumID3(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
var artist: String? = null
|
var artist: String? = null,
|
||||||
var artistId: String? = null
|
var artistId: String? = null,
|
||||||
@SerializedName("coverArt")
|
@SerializedName("coverArt")
|
||||||
var coverArtId: String? = null
|
var coverArtId: String? = null,
|
||||||
var songCount: Int? = 0
|
var songCount: Int? = 0,
|
||||||
var duration: Int? = 0
|
var duration: Int? = 0,
|
||||||
var playCount: Long? = 0
|
var playCount: Long? = 0,
|
||||||
var created: Date? = null
|
var created: Date? = null,
|
||||||
var starred: Date? = null
|
var starred: Date? = null,
|
||||||
var year: Int = 0
|
var year: Int = 0,
|
||||||
var genre: String? = null
|
var genre: String? = null,
|
||||||
var played: Date? = Date(0)
|
var played: Date? = Date(0),
|
||||||
var userRating: Int? = 0
|
var userRating: Int? = 0,
|
||||||
var recordLabels: List<RecordLabel>? = null
|
var recordLabels: List<RecordLabel>? = null,
|
||||||
var musicBrainzId: String? = null
|
var musicBrainzId: String? = null,
|
||||||
var genres: List<ItemGenre>? = null
|
var genres: List<ItemGenre>? = null,
|
||||||
var artists: List<ArtistID3>? = null
|
var artists: List<ArtistID3>? = null,
|
||||||
var displayArtist: String? = null
|
var displayArtist: String? = null,
|
||||||
var releaseTypes: List<String>? = null
|
var releaseTypes: List<String>? = null,
|
||||||
var moods: List<String>? = null
|
var moods: List<String>? = null,
|
||||||
var sortName: String? = null
|
var sortName: String? = null,
|
||||||
var originalReleaseDate: ItemDate? = null
|
var originalReleaseDate: ItemDate? = null,
|
||||||
var releaseDate: ItemDate? = null
|
var releaseDate: ItemDate? = null,
|
||||||
var isCompilation: Boolean? = null
|
var isCompilation: Boolean? = null,
|
||||||
var discTitles: List<DiscTitle>? = null
|
var discTitles: List<DiscTitle>? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class AlbumWithSongsID3 : AlbumID3(), Parcelable {
|
class AlbumWithSongsID3(
|
||||||
@SerializedName("song")
|
@SerializedName("song")
|
||||||
var songs: List<Child>? = null
|
var songs: List<Child>? = null,
|
||||||
}
|
) : AlbumID3(), Parcelable
|
||||||
@@ -7,10 +7,10 @@ import java.util.Date
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Artist : Parcelable {
|
class Artist(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
var starred: Date? = null
|
var starred: Date? = null,
|
||||||
var userRating: Int? = null
|
var userRating: Int? = null,
|
||||||
var averageRating: Double? = null
|
var averageRating: Double? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -4,15 +4,15 @@ import android.os.Parcelable
|
|||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class ArtistID3 : Parcelable {
|
open class ArtistID3(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
@SerializedName("coverArt")
|
@SerializedName("coverArt")
|
||||||
var coverArtId: String? = null
|
var coverArtId: String? = null,
|
||||||
var albumCount = 0
|
var albumCount: Int = 0,
|
||||||
var starred: Date? = null
|
var starred: Date? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class ArtistWithAlbumsID3 : ArtistID3(), Parcelable {
|
class ArtistWithAlbumsID3(
|
||||||
@SerializedName("album")
|
@SerializedName("album")
|
||||||
var albums: List<AlbumID3>? = null
|
var albums: List<AlbumID3>? = null,
|
||||||
}
|
) : ArtistID3(), Parcelable
|
||||||
@@ -8,15 +8,15 @@ import java.util.Date
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Directory : Parcelable {
|
class Directory(
|
||||||
@SerializedName("child")
|
@SerializedName("child")
|
||||||
var children: List<Child>? = null
|
var children: List<Child>? = null,
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
@SerializedName("parent")
|
@SerializedName("parent")
|
||||||
var parentId: String? = null
|
var parentId: String? = null,
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
var starred: Date? = null
|
var starred: Date? = null,
|
||||||
var userRating: Int? = null
|
var userRating: Int? = null,
|
||||||
var averageRating: Double? = null
|
var averageRating: Double? = null,
|
||||||
var playCount: Long? = null
|
var playCount: Long? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class DiscTitle : Parcelable {
|
open class DiscTitle(
|
||||||
var disc: Int? = null
|
var disc: Int? = null,
|
||||||
var title: String? = null
|
var title: String? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -7,9 +7,9 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Genre : Parcelable {
|
class Genre(
|
||||||
@SerializedName("value")
|
@SerializedName("value")
|
||||||
var genre: String? = null
|
var genre: String? = null,
|
||||||
var songCount = 0
|
var songCount: Int = 0,
|
||||||
var albumCount = 0
|
var albumCount: Int = 0,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -6,9 +6,9 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class InternetRadioStation : Parcelable {
|
class InternetRadioStation(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
var streamUrl: String? = null
|
var streamUrl: String? = null,
|
||||||
var homePageUrl: String? = null
|
var homePageUrl: String? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -9,11 +9,11 @@ import java.util.Locale
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class ItemDate : Parcelable {
|
open class ItemDate(
|
||||||
var year: Int? = null
|
var year: Int? = null,
|
||||||
var month: Int? = null
|
var month: Int? = null,
|
||||||
var day: Int? = null
|
var day: Int? = null,
|
||||||
|
) : Parcelable {
|
||||||
fun getFormattedDate(): String? {
|
fun getFormattedDate(): String? {
|
||||||
if (year == null && month == null && day == null) return null
|
if (year == null && month == null && day == null) return null
|
||||||
|
|
||||||
@@ -22,8 +22,7 @@ open class ItemDate : Parcelable {
|
|||||||
SimpleDateFormat("yyyy", Locale.getDefault())
|
SimpleDateFormat("yyyy", Locale.getDefault())
|
||||||
} else if (day == null) {
|
} else if (day == null) {
|
||||||
SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
|
SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class ItemGenre : Parcelable {
|
open class ItemGenre(
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class MusicFolder : Parcelable {
|
class MusicFolder(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -8,10 +8,9 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
class NowPlayingEntry(
|
class NowPlayingEntry(
|
||||||
@SerializedName("_id")
|
@SerializedName("_id")
|
||||||
override val id: String
|
override val id: String,
|
||||||
) : Child(id) {
|
var username: String? = null,
|
||||||
var username: String? = null
|
var minutesAgo: Int = 0,
|
||||||
var minutesAgo = 0
|
var playerId: Int = 0,
|
||||||
var playerId = 0
|
var playerName: String? = null,
|
||||||
var playerName: String? = null
|
) : Child(id)
|
||||||
}
|
|
||||||
@@ -7,8 +7,9 @@ import androidx.room.Entity
|
|||||||
import androidx.room.Ignore
|
import androidx.room.Ignore
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -16,27 +17,56 @@ import java.util.*
|
|||||||
open class Playlist(
|
open class Playlist(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@ColumnInfo(name = "id")
|
@ColumnInfo(name = "id")
|
||||||
open var id: String
|
open var id: String,
|
||||||
) : Parcelable {
|
|
||||||
@ColumnInfo(name = "name")
|
@ColumnInfo(name = "name")
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
|
@ColumnInfo(name = "duration")
|
||||||
|
var duration: Long = 0,
|
||||||
|
@ColumnInfo(name = "coverArt")
|
||||||
|
var coverArtId: String? = null,
|
||||||
|
) : Parcelable {
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
var comment: String? = null
|
var comment: String? = null
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
var owner: String? = null
|
var owner: String? = null
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
@SerializedName("public")
|
@SerializedName("public")
|
||||||
var isUniversal: Boolean? = null
|
var isUniversal: Boolean? = null
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
var songCount: Int = 0
|
var songCount: Int = 0
|
||||||
@ColumnInfo(name = "duration")
|
|
||||||
var duration: Long = 0
|
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
var created: Date? = null
|
var created: Date? = null
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
var changed: Date? = null
|
var changed: Date? = null
|
||||||
@ColumnInfo(name = "coverArt")
|
|
||||||
var coverArtId: String? = null
|
|
||||||
@Ignore
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
var allowedUsers: List<String>? = null
|
var allowedUsers: List<String>? = null
|
||||||
|
@Ignore
|
||||||
|
constructor(
|
||||||
|
id: String,
|
||||||
|
name: String?,
|
||||||
|
comment: String?,
|
||||||
|
owner: String?,
|
||||||
|
isUniversal: Boolean?,
|
||||||
|
songCount: Int,
|
||||||
|
duration: Long,
|
||||||
|
created: Date?,
|
||||||
|
changed: Date?,
|
||||||
|
coverArtId: String?,
|
||||||
|
allowedUsers: List<String>?,
|
||||||
|
) : this(id, name, duration, coverArtId) {
|
||||||
|
this.comment = comment
|
||||||
|
this.owner = owner
|
||||||
|
this.isUniversal = isUniversal
|
||||||
|
this.songCount = songCount
|
||||||
|
this.created = created
|
||||||
|
this.changed = changed
|
||||||
|
this.allowedUsers = allowedUsers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
class PlaylistWithSongs(
|
class PlaylistWithSongs(
|
||||||
@SerializedName("_id")
|
@SerializedName("_id")
|
||||||
override var id: String
|
override var id: String,
|
||||||
) : Playlist(id), Parcelable {
|
|
||||||
@SerializedName("entry")
|
@SerializedName("entry")
|
||||||
var entries: List<Child>? = null
|
var entries: List<Child>? = null,
|
||||||
}
|
) : Playlist(id), Parcelable
|
||||||
@@ -6,5 +6,5 @@ import com.google.gson.annotations.SerializedName
|
|||||||
@Keep
|
@Keep
|
||||||
class Playlists(
|
class Playlists(
|
||||||
@SerializedName("playlist")
|
@SerializedName("playlist")
|
||||||
var playlists: List<Playlist>? = null
|
var playlists: List<Playlist>? = null,
|
||||||
)
|
)
|
||||||
@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class PodcastChannel : Parcelable {
|
class PodcastChannel(
|
||||||
@SerializedName("episode")
|
@SerializedName("episode")
|
||||||
var episodes: List<PodcastEpisode>? = null
|
var episodes: List<PodcastEpisode>? = null,
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var url: String? = null
|
var url: String? = null,
|
||||||
var title: String? = null
|
var title: String? = null,
|
||||||
var description: String? = null
|
var description: String? = null,
|
||||||
@SerializedName("coverArt")
|
@SerializedName("coverArt")
|
||||||
var coverArtId: String? = null
|
var coverArtId: String? = null,
|
||||||
var originalImageUrl: String? = null
|
var originalImageUrl: String? = null,
|
||||||
var status: String? = null
|
var status: String? = null,
|
||||||
var errorMessage: String? = null
|
var errorMessage: String? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -3,37 +3,38 @@ package com.cappielloantonio.tempo.subsonic.models
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class PodcastEpisode : Parcelable {
|
class PodcastEpisode(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
@SerializedName("parent")
|
@SerializedName("parent")
|
||||||
var parentId: String? = null
|
var parentId: String? = null,
|
||||||
var isDir = false
|
var isDir: Boolean = false,
|
||||||
var title: String? = null
|
var title: String? = null,
|
||||||
var album: String? = null
|
var album: String? = null,
|
||||||
var artist: String? = null
|
var artist: String? = null,
|
||||||
var year: Int? = null
|
var year: Int? = null,
|
||||||
var genre: String? = null
|
var genre: String? = null,
|
||||||
@SerializedName("coverArt")
|
@SerializedName("coverArt")
|
||||||
var coverArtId: String? = null
|
var coverArtId: String? = null,
|
||||||
var size: Long? = null
|
var size: Long? = null,
|
||||||
var contentType: String? = null
|
var contentType: String? = null,
|
||||||
var suffix: String? = null
|
var suffix: String? = null,
|
||||||
var duration: Int? = null
|
var duration: Int? = null,
|
||||||
@SerializedName("bitRate")
|
@SerializedName("bitRate")
|
||||||
var bitrate: Int? = null
|
var bitrate: Int? = null,
|
||||||
var path: String? = null
|
var path: String? = null,
|
||||||
var isVideo: Boolean = false
|
var isVideo: Boolean = false,
|
||||||
var created: Date? = null
|
var created: Date? = null,
|
||||||
var artistId: String? = null
|
var artistId: String? = null,
|
||||||
var type: String? = null
|
var type: String? = null,
|
||||||
var streamId: String? = null
|
var streamId: String? = null,
|
||||||
var channelId: String? = null
|
var channelId: String? = null,
|
||||||
var description: String? = null
|
var description: String? = null,
|
||||||
var status: String? = null
|
var status: String? = null,
|
||||||
var publishDate: Date? = null
|
var publishDate: Date? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -2,10 +2,11 @@ package com.cappielloantonio.tempo.subsonic.models
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class RecordLabel : Parcelable {
|
open class RecordLabel(
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
}
|
) : Parcelable
|
||||||
@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Share : Parcelable {
|
data class Share(
|
||||||
@SerializedName("entry")
|
@SerializedName("entry")
|
||||||
var entries: List<Child>? = null
|
var entries: List<Child>? = null,
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var url: String? = null
|
var url: String? = null,
|
||||||
var description: String? = null
|
var description: String? = null,
|
||||||
var username: String? = null
|
var username: String? = null,
|
||||||
var created: Date? = null
|
var created: Date? = null,
|
||||||
var expires: Date? = null
|
var expires: Date? = null,
|
||||||
var lastVisited: Date? = null
|
var lastVisited: Date? = null,
|
||||||
var visitCount = 0
|
var visitCount: Int = 0
|
||||||
}
|
) : Parcelable
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.cappielloantonio.tempo.subsonic.utils
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext
|
||||||
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
// This adapter handles Date objects, returning null if the JSON string is empty or unparsable.
|
||||||
|
class EmptyDateTypeAdapter : JsonDeserializer<Date> {
|
||||||
|
|
||||||
|
// Define the date formats expected from the Subsonic server.
|
||||||
|
private val dateFormats: List<SimpleDateFormat> = listOf(
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(JsonParseException::class)
|
||||||
|
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date? {
|
||||||
|
val jsonString = json.asString.trim()
|
||||||
|
|
||||||
|
if (jsonString.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (format in dateFormats) {
|
||||||
|
try {
|
||||||
|
return format.parse(jsonString)
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
// Ignore and try the next format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import com.cappielloantonio.tempo.util.MusicUtil;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAdapter.ViewHolder> implements Filterable {
|
public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAdapter.ViewHolder> implements Filterable {
|
||||||
@@ -152,12 +153,20 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void sort(String order) {
|
public void sort(String order) {
|
||||||
|
if (albums == null) return;
|
||||||
|
|
||||||
switch (order) {
|
switch (order) {
|
||||||
case Constants.ALBUM_ORDER_BY_NAME:
|
case Constants.ALBUM_ORDER_BY_NAME:
|
||||||
albums.sort(Comparator.comparing(AlbumID3::getName));
|
albums.sort(Comparator.comparing(
|
||||||
|
album -> album.getName() != null ? album.getName() : "",
|
||||||
|
String.CASE_INSENSITIVE_ORDER
|
||||||
|
));
|
||||||
break;
|
break;
|
||||||
case Constants.ALBUM_ORDER_BY_ARTIST:
|
case Constants.ALBUM_ORDER_BY_ARTIST:
|
||||||
albums.sort(Comparator.comparing(AlbumID3::getArtist, Comparator.nullsLast(Comparator.naturalOrder())));
|
albums.sort(Comparator.comparing(
|
||||||
|
album -> album.getArtist() != null ? album.getArtist() : "",
|
||||||
|
String.CASE_INSENSITIVE_ORDER
|
||||||
|
));
|
||||||
break;
|
break;
|
||||||
case Constants.ALBUM_ORDER_BY_YEAR:
|
case Constants.ALBUM_ORDER_BY_YEAR:
|
||||||
albums.sort(Comparator.comparing(AlbumID3::getYear));
|
albums.sort(Comparator.comparing(AlbumID3::getYear));
|
||||||
@@ -166,15 +175,23 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
|
|||||||
Collections.shuffle(albums);
|
Collections.shuffle(albums);
|
||||||
break;
|
break;
|
||||||
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
|
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
|
||||||
albums.sort(Comparator.comparing(AlbumID3::getCreated));
|
albums.sort(Comparator.comparing(
|
||||||
|
album -> album.getCreated() != null ? album.getCreated() : new Date(0),
|
||||||
|
Comparator.nullsLast(Date::compareTo)
|
||||||
|
));
|
||||||
Collections.reverse(albums);
|
Collections.reverse(albums);
|
||||||
break;
|
break;
|
||||||
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
|
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
|
||||||
albums.sort(Comparator.comparing(AlbumID3::getPlayed));
|
albums.sort(Comparator.comparing(
|
||||||
|
album -> album.getPlayed() != null ? album.getPlayed() : new Date(0),
|
||||||
|
Comparator.nullsLast(Date::compareTo)
|
||||||
|
));
|
||||||
Collections.reverse(albums);
|
Collections.reverse(albums);
|
||||||
break;
|
break;
|
||||||
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
|
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
|
||||||
albums.sort(Comparator.comparing(AlbumID3::getPlayCount));
|
albums.sort(Comparator.comparing(
|
||||||
|
album -> album.getPlayCount() != null ? album.getPlayCount() : 0L
|
||||||
|
));
|
||||||
Collections.reverse(albums);
|
Collections.reverse(albums);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,19 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||||
private static final String TAG = "ArtistCatalogueFragment";
|
private static final String TAG = "AlbumCatalogueFragment";
|
||||||
|
|
||||||
private FragmentAlbumCatalogueBinding bind;
|
private FragmentAlbumCatalogueBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
||||||
|
|
||||||
private AlbumCatalogueAdapter albumAdapter;
|
private AlbumCatalogueAdapter albumAdapter;
|
||||||
|
private String currentSortOrder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@@ -115,7 +117,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
||||||
albumAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
|
albumAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
|
||||||
bind.albumCatalogueRecyclerView.setAdapter(albumAdapter);
|
bind.albumCatalogueRecyclerView.setAdapter(albumAdapter);
|
||||||
albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> albumAdapter.setItems(albums));
|
albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> {
|
||||||
|
albumAdapter.setItems(albums);
|
||||||
|
applySavedSortOrder();
|
||||||
|
});
|
||||||
|
|
||||||
bind.albumCatalogueRecyclerView.setOnTouchListener((v, event) -> {
|
bind.albumCatalogueRecyclerView.setOnTouchListener((v, event) -> {
|
||||||
hideKeyboard(v);
|
hideKeyboard(v);
|
||||||
@@ -137,6 +142,44 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applySavedSortOrder() {
|
||||||
|
String savedSortOrder = Preferences.getAlbumSortOrder();
|
||||||
|
currentSortOrder = savedSortOrder;
|
||||||
|
albumAdapter.sort(savedSortOrder);
|
||||||
|
updateSortIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSortIndicator() {
|
||||||
|
if (bind == null) return;
|
||||||
|
|
||||||
|
String sortText = getSortDisplayText(currentSortOrder);
|
||||||
|
bind.albumListSortTextView.setText(sortText);
|
||||||
|
bind.albumListSortTextView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSortDisplayText(String sortOrder) {
|
||||||
|
if (sortOrder == null) return "";
|
||||||
|
|
||||||
|
switch (sortOrder) {
|
||||||
|
case Constants.ALBUM_ORDER_BY_NAME:
|
||||||
|
return getString(R.string.menu_sort_name);
|
||||||
|
case Constants.ALBUM_ORDER_BY_ARTIST:
|
||||||
|
return getString(R.string.menu_group_by_artist);
|
||||||
|
case Constants.ALBUM_ORDER_BY_YEAR:
|
||||||
|
return getString(R.string.menu_sort_year);
|
||||||
|
case Constants.ALBUM_ORDER_BY_RANDOM:
|
||||||
|
return getString(R.string.menu_sort_random);
|
||||||
|
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
|
||||||
|
return getString(R.string.menu_sort_recently_added);
|
||||||
|
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
|
||||||
|
return getString(R.string.menu_sort_recently_played);
|
||||||
|
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
|
||||||
|
return getString(R.string.menu_sort_most_played);
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.toolbar_menu, menu);
|
inflater.inflate(R.menu.toolbar_menu, menu);
|
||||||
@@ -172,26 +215,29 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
|
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener(menuItem -> {
|
popup.setOnMenuItemClickListener(menuItem -> {
|
||||||
|
String newSortOrder = null;
|
||||||
|
|
||||||
if (menuItem.getItemId() == R.id.menu_album_sort_name) {
|
if (menuItem.getItemId() == R.id.menu_album_sort_name) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_NAME);
|
newSortOrder = Constants.ALBUM_ORDER_BY_NAME;
|
||||||
return true;
|
|
||||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_artist) {
|
} else if (menuItem.getItemId() == R.id.menu_album_sort_artist) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_ARTIST);
|
newSortOrder = Constants.ALBUM_ORDER_BY_ARTIST;
|
||||||
return true;
|
|
||||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_year) {
|
} else if (menuItem.getItemId() == R.id.menu_album_sort_year) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_YEAR);
|
newSortOrder = Constants.ALBUM_ORDER_BY_YEAR;
|
||||||
return true;
|
|
||||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_random) {
|
} else if (menuItem.getItemId() == R.id.menu_album_sort_random) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RANDOM);
|
newSortOrder = Constants.ALBUM_ORDER_BY_RANDOM;
|
||||||
return true;
|
|
||||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) {
|
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_ADDED);
|
newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_ADDED;
|
||||||
return true;
|
|
||||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) {
|
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED);
|
newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED;
|
||||||
return true;
|
|
||||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) {
|
} else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) {
|
||||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_PLAYED);
|
newSortOrder = Constants.ALBUM_ORDER_BY_MOST_PLAYED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSortOrder != null) {
|
||||||
|
currentSortOrder = newSortOrder;
|
||||||
|
albumAdapter.sort(newSortOrder);
|
||||||
|
Preferences.setAlbumSortOrder(newSortOrder);
|
||||||
|
updateSortIndicator();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ object Preferences {
|
|||||||
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"
|
private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility"
|
||||||
|
private const val ALBUM_SORT_ORDER = "album_sort_order"
|
||||||
|
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServer(): String? {
|
fun getServer(): String? {
|
||||||
@@ -638,4 +640,14 @@ object Preferences {
|
|||||||
if (parts.size < bandCount) return ShortArray(bandCount.toInt())
|
if (parts.size < bandCount) return ShortArray(bandCount.toInt())
|
||||||
return ShortArray(bandCount.toInt()) { i -> parts[i].toShortOrNull() ?: 0 }
|
return ShortArray(bandCount.toInt()) { i -> parts[i].toShortOrNull() ?: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getAlbumSortOrder(): String {
|
||||||
|
return App.getInstance().preferences.getString(ALBUM_SORT_ORDER, DEFAULT_ALBUM_SORT_ORDER) ?: DEFAULT_ALBUM_SORT_ORDER
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setAlbumSortOrder(sortOrder: String) {
|
||||||
|
App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,8 @@ import java.util.concurrent.ExecutionException;
|
|||||||
|
|
||||||
public final class WidgetUpdateManager {
|
public final class WidgetUpdateManager {
|
||||||
|
|
||||||
|
private static final int WIDGET_SAFE_ART_SIZE = 512;
|
||||||
|
|
||||||
public static void updateFromState(Context ctx,
|
public static void updateFromState(Context ctx,
|
||||||
String title,
|
String title,
|
||||||
String artist,
|
String artist,
|
||||||
@@ -95,7 +97,7 @@ public final class WidgetUpdateManager {
|
|||||||
CustomGlideRequest.loadAlbumArtBitmap(
|
CustomGlideRequest.loadAlbumArtBitmap(
|
||||||
appCtx,
|
appCtx,
|
||||||
coverArtId,
|
coverArtId,
|
||||||
com.cappielloantonio.tempo.util.Preferences.getImageSize(),
|
WIDGET_SAFE_ART_SIZE,
|
||||||
new CustomTarget<Bitmap>() {
|
new CustomTarget<Bitmap>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
|
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
|
||||||
|
|||||||
@@ -41,23 +41,40 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<Button
|
<LinearLayout
|
||||||
android:id="@+id/album_list_sort_image_view"
|
android:id="@+id/sort_container"
|
||||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="52dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="52dp"
|
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:insetLeft="0dp"
|
android:orientation="horizontal"
|
||||||
android:insetTop="0dp"
|
android:gravity="center_vertical"
|
||||||
android:insetRight="0dp"
|
|
||||||
android:insetBottom="0dp"
|
|
||||||
app:cornerRadius="30dp"
|
|
||||||
app:icon="@drawable/ic_sort_list"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/albumListSortTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/album_list_sort_image_view"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:insetLeft="0dp"
|
||||||
|
android:insetTop="0dp"
|
||||||
|
android:insetRight="0dp"
|
||||||
|
android:insetBottom="0dp"
|
||||||
|
app:cornerRadius="30dp"
|
||||||
|
app:icon="@drawable/ic_sort_list" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/album_list_progress_loader"
|
android:id="@+id/album_list_progress_loader"
|
||||||
@@ -71,7 +88,6 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
@@ -87,4 +103,3 @@
|
|||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,19 @@ 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
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
import androidx.media3.exoplayer.source.TrackGroupArray
|
import androidx.media3.exoplayer.source.TrackGroupArray
|
||||||
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
|
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
|
||||||
import androidx.media3.session.*
|
import androidx.media3.session.*
|
||||||
import androidx.media3.session.MediaSession.ControllerInfo
|
import androidx.media3.session.MediaSession.ControllerInfo
|
||||||
import com.cappielloantonio.tempo.R
|
import com.cappielloantonio.tempo.R
|
||||||
|
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||||
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
||||||
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.DynamicMediaSourceFactory
|
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||||
|
import com.cappielloantonio.tempo.util.MappingUtil
|
||||||
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.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||||
@@ -84,6 +87,7 @@ class MediaService : MediaLibraryService() {
|
|||||||
initializeCustomCommands()
|
initializeCustomCommands()
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
initializeMediaLibrarySession()
|
initializeMediaLibrarySession()
|
||||||
|
restorePlayerFromQueue()
|
||||||
initializePlayerListener()
|
initializePlayerListener()
|
||||||
initializeEqualizerManager()
|
initializeEqualizerManager()
|
||||||
|
|
||||||
@@ -119,15 +123,17 @@ class MediaService : MediaLibraryService() {
|
|||||||
val connectionResult = super.onConnect(session, controller)
|
val connectionResult = super.onConnect(session, controller)
|
||||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||||
|
|
||||||
shuffleCommands.forEach { commandButton ->
|
(shuffleCommands + repeatCommands).forEach { commandButton ->
|
||||||
// TODO: Aggiungere i comandi personalizzati
|
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||||
// commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaSession.ConnectionResult.accept(
|
customLayout = buildCustomLayout(session.player)
|
||||||
availableSessionCommands.build(),
|
|
||||||
connectionResult.availablePlayerCommands
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||||
)
|
.setAvailableSessionCommands(availableSessionCommands.build())
|
||||||
|
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
|
||||||
|
.setCustomLayout(customLayout)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
|
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
|
||||||
@@ -226,7 +232,7 @@ class MediaService : MediaLibraryService() {
|
|||||||
private fun initializePlayer() {
|
private fun initializePlayer() {
|
||||||
player = ExoPlayer.Builder(this)
|
player = ExoPlayer.Builder(this)
|
||||||
.setRenderersFactory(getRenderersFactory())
|
.setRenderersFactory(getRenderersFactory())
|
||||||
.setMediaSourceFactory(DynamicMediaSourceFactory(this))
|
.setMediaSourceFactory(getMediaSourceFactory())
|
||||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||||
.setHandleAudioBecomingNoisy(true)
|
.setHandleAudioBecomingNoisy(true)
|
||||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||||
@@ -269,6 +275,33 @@ class MediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restorePlayerFromQueue() {
|
||||||
|
if (player.mediaItemCount > 0) return
|
||||||
|
|
||||||
|
val queueRepository = QueueRepository()
|
||||||
|
val storedQueue = queueRepository.media
|
||||||
|
if (storedQueue.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||||
|
if (mediaItems.isEmpty()) return
|
||||||
|
|
||||||
|
val lastIndex = try {
|
||||||
|
queueRepository.lastPlayedMediaIndex
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0
|
||||||
|
}.coerceIn(0, mediaItems.size - 1)
|
||||||
|
|
||||||
|
val lastPosition = try {
|
||||||
|
queueRepository.lastPlayedMediaTimestamp
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}.let { if (it < 0L) 0L else it }
|
||||||
|
|
||||||
|
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||||
|
player.prepare()
|
||||||
|
updateWidget()
|
||||||
|
}
|
||||||
|
|
||||||
private fun initializePlayerListener() {
|
private fun initializePlayerListener() {
|
||||||
player.addListener(object : Player.Listener {
|
player.addListener(object : Player.Listener {
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
@@ -399,7 +432,7 @@ class MediaService : MediaLibraryService() {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
|
private fun ignoreFuture(@Suppress("UNUSED_PARAMETER") customLayout: ListenableFuture<SessionResult>) {
|
||||||
/* Do nothing. */
|
/* Do nothing. */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +497,7 @@ class MediaService : MediaLibraryService() {
|
|||||||
|
|
||||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||||
|
|
||||||
|
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||||
|
|||||||
@@ -295,11 +295,6 @@ open class MediaLibrarySessionCallback(
|
|||||||
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 -> {
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> {
|
||||||
session.player.shuffleModeEnabled = true
|
session.player.shuffleModeEnabled = true
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Binder
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
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
|
||||||
@@ -21,11 +22,13 @@ import androidx.media3.exoplayer.ExoPlayer
|
|||||||
import androidx.media3.session.MediaLibraryService
|
import androidx.media3.session.MediaLibraryService
|
||||||
import androidx.media3.session.MediaSession.ControllerInfo
|
import androidx.media3.session.MediaSession.ControllerInfo
|
||||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||||
|
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||||
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
||||||
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.DynamicMediaSourceFactory
|
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||||
|
import com.cappielloantonio.tempo.util.MappingUtil
|
||||||
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.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||||
@@ -71,9 +74,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||||||
|
|
||||||
initializeRepository()
|
initializeRepository()
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
initializeCastPlayer()
|
|
||||||
initializeMediaLibrarySession()
|
initializeMediaLibrarySession()
|
||||||
|
restorePlayerFromQueue()
|
||||||
initializePlayerListener()
|
initializePlayerListener()
|
||||||
|
initializeCastPlayer()
|
||||||
initializeEqualizerManager()
|
initializeEqualizerManager()
|
||||||
|
|
||||||
setPlayer(
|
setPlayer(
|
||||||
@@ -143,12 +147,20 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||||||
player.repeatMode = Preferences.getRepeatMode()
|
player.repeatMode = Preferences.getRepeatMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private fun initializeCastPlayer() {
|
private fun initializeCastPlayer() {
|
||||||
if (GoogleApiAvailability.getInstance()
|
if (GoogleApiAvailability.getInstance()
|
||||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||||
) {
|
) {
|
||||||
castPlayer = CastPlayer(CastContext.getSharedInstance(this))
|
CastContext.getSharedInstance(this, ContextCompat.getMainExecutor(this))
|
||||||
castPlayer.setSessionAvailabilityListener(this)
|
.addOnSuccessListener { castContext ->
|
||||||
|
castPlayer = CastPlayer(castContext)
|
||||||
|
castPlayer.setSessionAvailabilityListener(this@MediaService)
|
||||||
|
|
||||||
|
if (castPlayer.isCastSessionAvailable && this::mediaLibrarySession.isInitialized) {
|
||||||
|
setPlayer(player, castPlayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +178,33 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restorePlayerFromQueue() {
|
||||||
|
if (player.mediaItemCount > 0) return
|
||||||
|
|
||||||
|
val queueRepository = QueueRepository()
|
||||||
|
val storedQueue = queueRepository.media
|
||||||
|
if (storedQueue.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||||
|
if (mediaItems.isEmpty()) return
|
||||||
|
|
||||||
|
val lastIndex = try {
|
||||||
|
queueRepository.lastPlayedMediaIndex
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0
|
||||||
|
}.coerceIn(0, mediaItems.size - 1)
|
||||||
|
|
||||||
|
val lastPosition = try {
|
||||||
|
queueRepository.lastPlayedMediaTimestamp
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}.let { if (it < 0L) 0L else it }
|
||||||
|
|
||||||
|
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||||
|
player.prepare()
|
||||||
|
updateWidget()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLibrarySessionCallback(): MediaLibrarySessionCallback {
|
private fun createLibrarySessionCallback(): MediaLibrarySessionCallback {
|
||||||
return MediaLibrarySessionCallback(this, automotiveRepository)
|
return MediaLibrarySessionCallback(this, automotiveRepository)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ package com.cappielloantonio.tempo.util;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
public class Flavors {
|
public class Flavors {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public static void initializeCastContext(Context context) {
|
public static void initializeCastContext(Context context) {
|
||||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS)
|
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS)
|
||||||
CastContext.getSharedInstance(context);
|
CastContext.getSharedInstance(context, ContextCompat.getMainExecutor(context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user