mirror of
https://github.com/antebudimir/tempus.git
synced 2026-01-01 09:53:33 +00:00
Merge branch 'development' into main
This commit is contained in:
commit
442fe1ea01
66 changed files with 1856 additions and 327 deletions
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/
|
||||||
|
|
|
||||||
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -2,6 +2,25 @@
|
||||||
|
|
||||||
***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.14](https://github.com/eddyizm/tempo/releases/tag/v3.17.14) (2025-10-16)
|
||||||
|
## What's Changed
|
||||||
|
* fix: General build warning and playback issues by @le-firehawk in https://github.com/eddyizm/tempo/pull/167
|
||||||
|
* fix: persist album sort preference by @eddyizm in https://github.com/eddyizm/tempo/pull/168
|
||||||
|
* Fix album parse empty date field by @eddyizm in https://github.com/eddyizm/tempo/pull/171
|
||||||
|
* fix: Include shuffle/repeat controls in f-droid build's media notific… by @le-firehawk in https://github.com/eddyizm/tempo/pull/174
|
||||||
|
* fix: limits image size to prevent widget crash #172 by @eddyizm in https://github.com/eddyizm/tempo/pull/175
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.17.0...v3.17.14
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
|
||||||
30
README.md
30
README.md
|
|
@ -25,11 +25,21 @@ 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.
|
||||||
|
|
||||||
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
### Releases
|
||||||
|
|
||||||
|
Please note the two variants in the release assets include release/debug and 32/64 bit flavors.
|
||||||
|
|
||||||
|
`app-tempo` <- The github release with all the android auto/chromecast features
|
||||||
|
|
||||||
|
`app-notquitemy*` <- The f-droid release that goes without any of the google stuff. It was last released at 3.8.1 from the original repo. Since I don't have access to that original repo, I am releasing the apk's here on github.
|
||||||
|
|
||||||
|
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](CHANGELOG.md)
|
||||||
|
|
||||||
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
||||||
|
|
||||||
|
|
@ -49,13 +59,11 @@ 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.
|
||||||
|
|
||||||
## Sponsors
|
## 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)
|
||||||
|
|
||||||
Tempo is an open-source project developed and maintained solely by me. I would like to express my heartfelt thanks to all the users who have shown their love and support for Tempo. Your contributions and encouragement mean a lot to me, and they help drive the development and improvement of the app.
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
@ -90,6 +98,16 @@ Tempo is an open-source project developed and maintained solely by me. I would l
|
||||||
<img src="mockup/dark/8_screenshot.png" width=200>
|
<img src="mockup/dark/8_screenshot.png" width=200>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please fork and open PR's against the development branch. Make sure your PR builds successfully.
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Tempo is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.
|
Tempo is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.
|
||||||
|
|
|
||||||
24
USAGE.md
24
USAGE.md
|
|
@ -57,10 +57,30 @@ 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
|
||||||
**TODO**
|
|
||||||
|
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/player_icons.png" width=159>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
*marked the icons with numbers for clarity*
|
||||||
|
|
||||||
|
1. Downloads the track (there is a notification if the android screen but not a pop toast currently )
|
||||||
|
2. Adds track to playlist - pops up playlist dialog.
|
||||||
|
3. Adds tracks to the queue via instant mix function
|
||||||
|
4. Saves play queue (if the feature is enabled in the settings)
|
||||||
|
* if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right)
|
||||||
|
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ android {
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 34
|
versionCode 36
|
||||||
versionName '3.16.6'
|
versionName '3.17.14'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,16 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="asset"
|
||||||
|
android:scheme="tempo" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,18 @@ import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.GlideBuilder;
|
import com.bumptech.glide.GlideBuilder;
|
||||||
import com.bumptech.glide.annotation.GlideModule;
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
import com.bumptech.glide.load.DecodeFormat;
|
import com.bumptech.glide.load.DecodeFormat;
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
||||||
|
import com.bumptech.glide.Registry;
|
||||||
import com.bumptech.glide.module.AppGlideModule;
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
public class CustomGlideModule extends AppGlideModule {
|
public class CustomGlideModule extends AppGlideModule {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -20,4 +24,9 @@ public class CustomGlideModule extends AppGlideModule {
|
||||||
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize));
|
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize));
|
||||||
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
|
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||||
|
registry.replace(String.class, InputStream.class, new IPv6StringLoader.Factory());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ public class CustomGlideRequest {
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private final RequestManager requestManager;
|
private final RequestManager requestManager;
|
||||||
private Object item;
|
private String item;
|
||||||
|
|
||||||
private Builder(Context context, String item, ResourceType type) {
|
private Builder(Context context, String item, ResourceType type) {
|
||||||
this.requestManager = Glide.with(context);
|
this.requestManager = Glide.with(context);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.cappielloantonio.tempo.glide;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority;
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.Options;
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher;
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader;
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||||
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public class IPv6StringLoader implements ModelLoader<String, InputStream> {
|
||||||
|
private static final int DEFAULT_TIMEOUT_MS = 2500;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handles(@NonNull String model) {
|
||||||
|
return model.startsWith("http://") || model.startsWith("https://");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
|
||||||
|
if (!handles(model)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new LoadData<>(new ObjectKey(model), new IPv6StreamFetcher(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class IPv6StreamFetcher implements DataFetcher<InputStream> {
|
||||||
|
private final String model;
|
||||||
|
private InputStream stream;
|
||||||
|
private HttpURLConnection connection;
|
||||||
|
|
||||||
|
IPv6StreamFetcher(String model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(model);
|
||||||
|
connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setConnectTimeout(DEFAULT_TIMEOUT_MS);
|
||||||
|
connection.setReadTimeout(DEFAULT_TIMEOUT_MS);
|
||||||
|
connection.setUseCaches(true);
|
||||||
|
connection.setDoInput(true);
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
if (connection.getResponseCode() / 100 != 2) {
|
||||||
|
callback.onLoadFailed(new IOException("Request failed with status code: " + connection.getResponseCode()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = connection.getInputStream();
|
||||||
|
callback.onDataReady(stream);
|
||||||
|
} catch (IOException e) {
|
||||||
|
callback.onLoadFailed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
if (stream != null) {
|
||||||
|
try {
|
||||||
|
stream.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel() {
|
||||||
|
// HttpURLConnection does not provide a direct cancel mechanism.
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<InputStream> getDataClass() {
|
||||||
|
return InputStream.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DataSource getDataSource() {
|
||||||
|
return DataSource.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Factory implements ModelLoaderFactory<String, InputStream> {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||||
|
return new IPv6StringLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void teardown() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<>());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,33 @@ public class PlaylistRepository {
|
||||||
return listLivePlaylistSongs;
|
return listLivePlaylistSongs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<Playlist> getPlaylist(String id) {
|
||||||
|
MutableLiveData<Playlist> playlistLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylist(id)
|
||||||
|
.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().getPlaylist() != null) {
|
||||||
|
playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
|
||||||
|
} else {
|
||||||
|
playlistLiveData.setValue(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
playlistLiveData.setValue(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return playlistLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
||||||
if (songsId.isEmpty()) {
|
if (songsId.isEmpty()) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -38,21 +38,36 @@ public class CacheUtil {
|
||||||
return chain.proceed(request);
|
return chain.proceed(request);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
private boolean isConnected() {
|
private boolean isConnected() {
|
||||||
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
|
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (connectivityManager == null) {
|
||||||
if (connectivityManager != null) {
|
return false;
|
||||||
Network network = connectivityManager.getActiveNetwork();
|
|
||||||
|
|
||||||
if (network != null) {
|
|
||||||
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
|
|
||||||
|
|
||||||
if (capabilities != null) {
|
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
Network network = connectivityManager.getActiveNetwork();
|
||||||
|
if (network == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
|
||||||
|
if (capabilities == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||||
|
if (!hasInternet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
|
||||||
|
if (!hasAppropriateTransport) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,8 @@ import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
||||||
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
|
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkNavigator;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||||
|
|
@ -60,6 +62,8 @@ public class MainActivity extends BaseActivity {
|
||||||
private BottomNavigationView bottomNavigationView;
|
private BottomNavigationView bottomNavigationView;
|
||||||
public NavController navController;
|
public NavController navController;
|
||||||
private BottomSheetBehavior bottomSheetBehavior;
|
private BottomSheetBehavior bottomSheetBehavior;
|
||||||
|
private AssetLinkNavigator assetLinkNavigator;
|
||||||
|
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||||
|
|
||||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||||
private Intent pendingDownloadPlaybackIntent;
|
private Intent pendingDownloadPlaybackIntent;
|
||||||
|
|
@ -76,6 +80,7 @@ public class MainActivity extends BaseActivity {
|
||||||
setContentView(view);
|
setContentView(view);
|
||||||
|
|
||||||
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
|
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
|
||||||
|
assetLinkNavigator = new AssetLinkNavigator(this);
|
||||||
|
|
||||||
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
||||||
connectivityStatusReceiverManager(true);
|
connectivityStatusReceiverManager(true);
|
||||||
|
|
@ -311,6 +316,24 @@ public class MainActivity extends BaseActivity {
|
||||||
public void goFromLogin() {
|
public void goFromLogin() {
|
||||||
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
||||||
goToHome();
|
goToHome();
|
||||||
|
consumePendingAssetLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink) {
|
||||||
|
openAssetLink(assetLink, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink, boolean collapsePlayer) {
|
||||||
|
if (!isUserAuthenticated()) {
|
||||||
|
pendingAssetLink = assetLink;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (collapsePlayer) {
|
||||||
|
setBottomSheetInPeek(true);
|
||||||
|
}
|
||||||
|
if (assetLinkNavigator != null) {
|
||||||
|
assetLinkNavigator.open(assetLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void quit() {
|
public void quit() {
|
||||||
|
|
@ -443,6 +466,7 @@ public class MainActivity extends BaseActivity {
|
||||||
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
|
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
|
||||||
pendingDownloadPlaybackIntent = new Intent(intent);
|
pendingDownloadPlaybackIntent = new Intent(intent);
|
||||||
}
|
}
|
||||||
|
handleAssetLinkIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void consumePendingPlaybackIntent() {
|
private void consumePendingPlaybackIntent() {
|
||||||
|
|
@ -452,6 +476,35 @@ public class MainActivity extends BaseActivity {
|
||||||
playDownloadedMedia(intent);
|
playDownloadedMedia(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleAssetLinkIntent(Intent intent) {
|
||||||
|
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.parse(intent);
|
||||||
|
if (assetLink == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isUserAuthenticated()) {
|
||||||
|
pendingAssetLink = assetLink;
|
||||||
|
intent.setData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (assetLinkNavigator != null) {
|
||||||
|
assetLinkNavigator.open(assetLink);
|
||||||
|
}
|
||||||
|
intent.setData(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUserAuthenticated() {
|
||||||
|
return Preferences.getPassword() != null
|
||||||
|
|| (Preferences.getToken() != null && Preferences.getSalt() != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumePendingAssetLink() {
|
||||||
|
if (pendingAssetLink == null || assetLinkNavigator == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assetLinkNavigator.open(pendingAssetLink);
|
||||||
|
pendingAssetLink = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void playDownloadedMedia(Intent intent) {
|
private void playDownloadedMedia(Intent intent) {
|
||||||
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
|
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
|
||||||
if (TextUtils.isEmpty(uriString)) {
|
if (TextUtils.isEmpty(uriString)) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ 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.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
@ -10,6 +11,7 @@ import androidx.media3.common.MediaMetadata;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
|
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
@ -21,6 +23,11 @@ public class TrackInfoDialog extends DialogFragment {
|
||||||
private DialogTrackInfoBinding bind;
|
private DialogTrackInfoBinding bind;
|
||||||
|
|
||||||
private final MediaMetadata mediaMetadata;
|
private final MediaMetadata mediaMetadata;
|
||||||
|
private AssetLinkUtil.AssetLink songLink;
|
||||||
|
private AssetLinkUtil.AssetLink albumLink;
|
||||||
|
private AssetLinkUtil.AssetLink artistLink;
|
||||||
|
private AssetLinkUtil.AssetLink genreLink;
|
||||||
|
private AssetLinkUtil.AssetLink yearLink;
|
||||||
|
|
||||||
public TrackInfoDialog(MediaMetadata mediaMetadata) {
|
public TrackInfoDialog(MediaMetadata mediaMetadata) {
|
||||||
this.mediaMetadata = mediaMetadata;
|
this.mediaMetadata = mediaMetadata;
|
||||||
|
|
@ -52,6 +59,8 @@ public class TrackInfoDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTrackInfo() {
|
private void setTrackInfo() {
|
||||||
|
genreLink = null;
|
||||||
|
yearLink = null;
|
||||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||||
bind.trakArtistInfoTextView.setText(
|
bind.trakArtistInfoTextView.setText(
|
||||||
mediaMetadata.artist != null
|
mediaMetadata.artist != null
|
||||||
|
|
@ -61,17 +70,41 @@ public class TrackInfoDialog extends DialogFragment {
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
if (mediaMetadata.extras != null) {
|
if (mediaMetadata.extras != null) {
|
||||||
|
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||||
|
albumLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ALBUM, mediaMetadata.extras.getString("albumId"));
|
||||||
|
artistLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, mediaMetadata.extras.getString("artistId"));
|
||||||
|
genreLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkGenre"));
|
||||||
|
yearLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkYear"));
|
||||||
|
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.into(bind.trackCoverInfoImageView);
|
.into(bind.trackCoverInfoImageView);
|
||||||
|
|
||||||
bind.titleValueSector.setText(mediaMetadata.extras.getString("title", getString(R.string.label_placeholder)));
|
bindAssetLink(bind.trackCoverInfoImageView, albumLink != null ? albumLink : songLink);
|
||||||
bind.albumValueSector.setText(mediaMetadata.extras.getString("album", getString(R.string.label_placeholder)));
|
bindAssetLink(bind.trakTitleInfoTextView, songLink);
|
||||||
bind.artistValueSector.setText(mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder)));
|
bindAssetLink(bind.trakArtistInfoTextView, artistLink != null ? artistLink : songLink);
|
||||||
|
|
||||||
|
String titleValue = mediaMetadata.extras.getString("title", getString(R.string.label_placeholder));
|
||||||
|
String albumValue = mediaMetadata.extras.getString("album", getString(R.string.label_placeholder));
|
||||||
|
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
|
||||||
|
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
|
||||||
|
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
||||||
|
|
||||||
|
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
||||||
|
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yearLink == null && yearValue != 0) {
|
||||||
|
yearLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(yearValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.titleValueSector.setText(titleValue);
|
||||||
|
bind.albumValueSector.setText(albumValue);
|
||||||
|
bind.artistValueSector.setText(artistValue);
|
||||||
bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder));
|
bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder));
|
||||||
bind.yearValueSector.setText(mediaMetadata.extras.getInt("year", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("year", 0)) : getString(R.string.label_placeholder));
|
bind.yearValueSector.setText(yearValue != 0 ? String.valueOf(yearValue) : getString(R.string.label_placeholder));
|
||||||
bind.genreValueSector.setText(mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder)));
|
bind.genreValueSector.setText(genreValue);
|
||||||
bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder));
|
bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder));
|
||||||
bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder)));
|
bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder)));
|
||||||
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
|
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
|
||||||
|
|
@ -83,6 +116,12 @@ public class TrackInfoDialog extends DialogFragment {
|
||||||
bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : getString(R.string.label_placeholder));
|
bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : getString(R.string.label_placeholder));
|
||||||
bind.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder)));
|
bind.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder)));
|
||||||
bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder));
|
bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder));
|
||||||
|
|
||||||
|
bindAssetLink(bind.titleValueSector, songLink);
|
||||||
|
bindAssetLink(bind.albumValueSector, albumLink);
|
||||||
|
bindAssetLink(bind.artistValueSector, artistLink);
|
||||||
|
bindAssetLink(bind.genreValueSector, genreLink);
|
||||||
|
bindAssetLink(bind.yearValueSector, yearLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,4 +174,31 @@ public class TrackInfoDialog extends DialogFragment {
|
||||||
bind.trakTranscodingInfoTextView.setText(info);
|
bind.trakTranscodingInfoTextView.setText(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bindAssetLink(android.view.View view, AssetLinkUtil.AssetLink assetLink) {
|
||||||
|
if (view == null) return;
|
||||||
|
if (assetLink == null) {
|
||||||
|
AssetLinkUtil.clearLinkAppearance(view);
|
||||||
|
view.setOnClickListener(null);
|
||||||
|
view.setOnLongClickListener(null);
|
||||||
|
view.setClickable(false);
|
||||||
|
view.setLongClickable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setClickable(true);
|
||||||
|
view.setLongClickable(true);
|
||||||
|
AssetLinkUtil.applyLinkAppearance(view);
|
||||||
|
view.setOnClickListener(v -> {
|
||||||
|
dismissAllowingStateLoss();
|
||||||
|
boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
|
||||||
|
((com.cappielloantonio.tempo.ui.activity.MainActivity) requireActivity()).openAssetLink(assetLink, collapse);
|
||||||
|
});
|
||||||
|
view.setOnLongClickListener(v -> {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
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.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.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
|
@ -177,8 +178,35 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
bind.albumNameLabel.setText(album.getName());
|
bind.albumNameLabel.setText(album.getName());
|
||||||
bind.albumArtistLabel.setText(album.getArtist());
|
bind.albumArtistLabel.setText(album.getArtist());
|
||||||
|
AssetLinkUtil.applyLinkAppearance(bind.albumArtistLabel);
|
||||||
|
AssetLinkUtil.AssetLink artistLink = buildArtistLink(album);
|
||||||
|
bind.albumArtistLabel.setOnLongClickListener(v -> {
|
||||||
|
if (artistLink != null) {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), artistLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, artistLink.id), Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
|
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
|
||||||
bind.albumReleaseYearLabel.setVisibility(album.getYear() != 0 ? View.VISIBLE : View.GONE);
|
if (album.getYear() != 0) {
|
||||||
|
bind.albumReleaseYearLabel.setVisibility(View.VISIBLE);
|
||||||
|
AssetLinkUtil.applyLinkAppearance(bind.albumReleaseYearLabel);
|
||||||
|
bind.albumReleaseYearLabel.setOnClickListener(v -> openYearLink(album.getYear()));
|
||||||
|
bind.albumReleaseYearLabel.setOnLongClickListener(v -> {
|
||||||
|
AssetLinkUtil.AssetLink yearLink = buildYearLink(album.getYear());
|
||||||
|
if (yearLink != null) {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), yearLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, yearLink.id), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
bind.albumReleaseYearLabel.setVisibility(View.GONE);
|
||||||
|
bind.albumReleaseYearLabel.setOnClickListener(null);
|
||||||
|
bind.albumReleaseYearLabel.setOnLongClickListener(null);
|
||||||
|
AssetLinkUtil.clearLinkAppearance(bind.albumReleaseYearLabel);
|
||||||
|
}
|
||||||
bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0));
|
bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0));
|
||||||
if (album.getGenre() != null && !album.getGenre().isEmpty()) {
|
if (album.getGenre() != null && !album.getGenre().isEmpty()) {
|
||||||
bind.albumGenresTextview.setText(album.getGenre());
|
bind.albumGenresTextview.setText(album.getGenre());
|
||||||
|
|
@ -351,4 +379,23 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||||
private void setMediaBrowserListenableFuture() {
|
private void setMediaBrowserListenableFuture() {
|
||||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openYearLink(int year) {
|
||||||
|
AssetLinkUtil.AssetLink link = buildYearLink(year);
|
||||||
|
if (link != null) {
|
||||||
|
activity.openAssetLink(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetLinkUtil.AssetLink buildYearLink(int year) {
|
||||||
|
if (year <= 0) return null;
|
||||||
|
return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetLinkUtil.AssetLink buildArtistLink(AlbumID3 album) {
|
||||||
|
if (album == null || album.getArtistId() == null || album.getArtistId().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, album.getArtistId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +195,7 @@ public class PlayerBottomSheetFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
||||||
if (mediaBrowser.getMediaMetadata().extras != null) {
|
if (mediaBrowser.getMediaMetadata().extras != null) {
|
||||||
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {
|
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,10 @@ import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.RatingBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
import android.widget.RatingBar;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
|
@ -41,12 +42,14 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
||||||
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
|
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
|
||||||
import com.google.android.material.chip.Chip;
|
import com.google.android.material.chip.Chip;
|
||||||
|
import com.google.android.material.chip.ChipGroup;
|
||||||
import com.google.android.material.elevation.SurfaceColors;
|
import com.google.android.material.elevation.SurfaceColors;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
@ -76,6 +79,10 @@ public class PlayerControllerFragment extends Fragment {
|
||||||
private ImageButton playerTrackInfo;
|
private ImageButton playerTrackInfo;
|
||||||
private LinearLayout ratingContainer;
|
private LinearLayout ratingContainer;
|
||||||
private ImageButton equalizerButton;
|
private ImageButton equalizerButton;
|
||||||
|
private ChipGroup assetLinkChipGroup;
|
||||||
|
private Chip playerSongLinkChip;
|
||||||
|
private Chip playerAlbumLinkChip;
|
||||||
|
private Chip playerArtistLinkChip;
|
||||||
|
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||||
|
|
@ -139,6 +146,10 @@ public class PlayerControllerFragment extends Fragment {
|
||||||
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
|
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
|
||||||
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
|
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
|
||||||
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
|
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
|
||||||
|
assetLinkChipGroup = bind.getRoot().findViewById(R.id.asset_link_chip_group);
|
||||||
|
playerSongLinkChip = bind.getRoot().findViewById(R.id.asset_link_song_chip);
|
||||||
|
playerAlbumLinkChip = bind.getRoot().findViewById(R.id.asset_link_album_chip);
|
||||||
|
playerArtistLinkChip = bind.getRoot().findViewById(R.id.asset_link_artist_chip);
|
||||||
checkAndSetRatingContainerVisibility();
|
checkAndSetRatingContainerVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,6 +230,8 @@ public class PlayerControllerFragment extends Fragment {
|
||||||
|| mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null
|
|| mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null
|
||||||
? View.VISIBLE
|
? View.VISIBLE
|
||||||
: View.GONE);
|
: View.GONE);
|
||||||
|
|
||||||
|
updateAssetLinkChips(mediaMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
||||||
|
|
@ -259,6 +272,110 @@ public class PlayerControllerFragment extends Fragment {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
||||||
|
if (assetLinkChipGroup == null) return;
|
||||||
|
String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC;
|
||||||
|
if (!Constants.MEDIA_TYPE_MUSIC.equals(mediaType)) {
|
||||||
|
clearAssetLinkChip(playerSongLinkChip);
|
||||||
|
clearAssetLinkChip(playerAlbumLinkChip);
|
||||||
|
clearAssetLinkChip(playerArtistLinkChip);
|
||||||
|
syncAssetLinkGroupVisibility();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String songId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("id") : null;
|
||||||
|
String albumId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("albumId") : null;
|
||||||
|
String artistId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("artistId") : null;
|
||||||
|
|
||||||
|
AssetLinkUtil.AssetLink songLink = bindAssetLinkChip(playerSongLinkChip, AssetLinkUtil.TYPE_SONG, songId);
|
||||||
|
AssetLinkUtil.AssetLink albumLink = bindAssetLinkChip(playerAlbumLinkChip, AssetLinkUtil.TYPE_ALBUM, albumId);
|
||||||
|
AssetLinkUtil.AssetLink artistLink = bindAssetLinkChip(playerArtistLinkChip, AssetLinkUtil.TYPE_ARTIST, artistId);
|
||||||
|
bindAssetLinkView(playerMediaTitleLabel, songLink);
|
||||||
|
bindAssetLinkView(playerArtistNameLabel, artistLink != null ? artistLink : songLink);
|
||||||
|
bindAssetLinkView(playerMediaCoverViewPager, songLink);
|
||||||
|
syncAssetLinkGroupVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetLinkUtil.AssetLink bindAssetLinkChip(Chip chip, String type, String id) {
|
||||||
|
if (chip == null) return null;
|
||||||
|
if (TextUtils.isEmpty(id)) {
|
||||||
|
clearAssetLinkChip(chip);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label = getString(AssetLinkUtil.getLabelRes(type));
|
||||||
|
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
|
||||||
|
if (assetLink == null) {
|
||||||
|
clearAssetLinkChip(chip);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
|
||||||
|
chip.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
chip.setOnClickListener(v -> {
|
||||||
|
if (assetLink != null) {
|
||||||
|
activity.openAssetLink(assetLink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chip.setOnLongClickListener(v -> {
|
||||||
|
if (assetLink != null) {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return assetLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAssetLinkChip(Chip chip) {
|
||||||
|
if (chip == null) return;
|
||||||
|
chip.setVisibility(View.GONE);
|
||||||
|
chip.setText("");
|
||||||
|
chip.setOnClickListener(null);
|
||||||
|
chip.setOnLongClickListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindAssetLinkView(View view, AssetLinkUtil.AssetLink assetLink) {
|
||||||
|
if (view == null) return;
|
||||||
|
if (assetLink == null) {
|
||||||
|
AssetLinkUtil.clearLinkAppearance(view);
|
||||||
|
view.setOnClickListener(null);
|
||||||
|
view.setOnLongClickListener(null);
|
||||||
|
view.setClickable(false);
|
||||||
|
view.setLongClickable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setClickable(true);
|
||||||
|
view.setLongClickable(true);
|
||||||
|
AssetLinkUtil.applyLinkAppearance(view);
|
||||||
|
view.setOnClickListener(v -> {
|
||||||
|
boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
|
||||||
|
activity.openAssetLink(assetLink, collapse);
|
||||||
|
});
|
||||||
|
view.setOnLongClickListener(v -> {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAssetLinkGroupVisibility() {
|
||||||
|
if (assetLinkChipGroup == null) return;
|
||||||
|
boolean hasVisible = false;
|
||||||
|
for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
|
||||||
|
View child = assetLinkChipGroup.getChildAt(i);
|
||||||
|
if (child.getVisibility() == View.VISIBLE) {
|
||||||
|
hasVisible = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
||||||
initPlaybackSpeedButton(mediaBrowser);
|
initPlaybackSpeedButton(mediaBrowser);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ 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.dialog.PlaylistChooserDialog;
|
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.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.ExternalAudioReader;
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
|
@ -39,6 +40,8 @@ import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
import com.google.android.material.chip.Chip;
|
||||||
|
import com.google.android.material.chip.ChipGroup;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
@ -56,6 +59,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
|
|
||||||
private TextView downloadButton;
|
private TextView downloadButton;
|
||||||
private TextView removeButton;
|
private TextView removeButton;
|
||||||
|
private ChipGroup assetLinkChipGroup;
|
||||||
|
private Chip songLinkChip;
|
||||||
|
private Chip albumLinkChip;
|
||||||
|
private Chip artistLinkChip;
|
||||||
|
private AssetLinkUtil.AssetLink currentSongLink;
|
||||||
|
private AssetLinkUtil.AssetLink currentAlbumLink;
|
||||||
|
private AssetLinkUtil.AssetLink currentArtistLink;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
|
|
@ -109,6 +119,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
|
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
|
||||||
artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
|
artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
|
||||||
|
|
||||||
|
initAssetLinkChips(view);
|
||||||
|
bindAssetLinkView(coverSong, currentSongLink);
|
||||||
|
bindAssetLinkView(titleSong, currentSongLink);
|
||||||
|
bindAssetLinkView(artistSong, currentArtistLink != null ? currentArtistLink : currentSongLink);
|
||||||
|
|
||||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
|
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> {
|
favoriteToggle.setOnClickListener(v -> {
|
||||||
|
|
@ -282,6 +297,95 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initAssetLinkChips(View root) {
|
||||||
|
assetLinkChipGroup = root.findViewById(R.id.asset_link_chip_group);
|
||||||
|
songLinkChip = root.findViewById(R.id.asset_link_song_chip);
|
||||||
|
albumLinkChip = root.findViewById(R.id.asset_link_album_chip);
|
||||||
|
artistLinkChip = root.findViewById(R.id.asset_link_artist_chip);
|
||||||
|
|
||||||
|
currentSongLink = bindAssetLinkChip(songLinkChip, AssetLinkUtil.TYPE_SONG, song.getId());
|
||||||
|
currentAlbumLink = bindAssetLinkChip(albumLinkChip, AssetLinkUtil.TYPE_ALBUM, song.getAlbumId());
|
||||||
|
currentArtistLink = bindAssetLinkChip(artistLinkChip, AssetLinkUtil.TYPE_ARTIST, song.getArtistId());
|
||||||
|
syncAssetLinkGroupVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetLinkUtil.AssetLink bindAssetLinkChip(@Nullable Chip chip, String type, @Nullable String id) {
|
||||||
|
if (chip == null) return null;
|
||||||
|
if (id == null || id.isEmpty()) {
|
||||||
|
clearAssetLinkChip(chip);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label = getString(AssetLinkUtil.getLabelRes(type));
|
||||||
|
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
|
||||||
|
if (assetLink == null) {
|
||||||
|
clearAssetLinkChip(chip);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
|
||||||
|
chip.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
chip.setOnClickListener(v -> {
|
||||||
|
if (assetLink != null) {
|
||||||
|
((MainActivity) requireActivity()).openAssetLink(assetLink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chip.setOnLongClickListener(v -> {
|
||||||
|
if (assetLink != null) {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return assetLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAssetLinkChip(@Nullable Chip chip) {
|
||||||
|
if (chip == null) return;
|
||||||
|
chip.setVisibility(View.GONE);
|
||||||
|
chip.setText("");
|
||||||
|
chip.setOnClickListener(null);
|
||||||
|
chip.setOnLongClickListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAssetLinkGroupVisibility() {
|
||||||
|
if (assetLinkChipGroup == null) return;
|
||||||
|
boolean hasVisible = false;
|
||||||
|
for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
|
||||||
|
View child = assetLinkChipGroup.getChildAt(i);
|
||||||
|
if (child.getVisibility() == View.VISIBLE) {
|
||||||
|
hasVisible = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindAssetLinkView(@Nullable View view, @Nullable AssetLinkUtil.AssetLink assetLink) {
|
||||||
|
if (view == null) return;
|
||||||
|
if (assetLink == null) {
|
||||||
|
AssetLinkUtil.clearLinkAppearance(view);
|
||||||
|
view.setOnClickListener(null);
|
||||||
|
view.setOnLongClickListener(null);
|
||||||
|
view.setClickable(false);
|
||||||
|
view.setLongClickable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setClickable(true);
|
||||||
|
view.setLongClickable(true);
|
||||||
|
AssetLinkUtil.applyLinkAppearance(view);
|
||||||
|
view.setOnClickListener(v -> ((MainActivity) requireActivity()).openAssetLink(assetLink, !AssetLinkUtil.TYPE_SONG.equals(assetLink.type)));
|
||||||
|
view.setOnLongClickListener(v -> {
|
||||||
|
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||||
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeMediaBrowser() {
|
private void initializeMediaBrowser() {
|
||||||
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavOptions;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.BuildConfig;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Genre;
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||||
|
|
||||||
|
public final class AssetLinkNavigator {
|
||||||
|
private final MainActivity activity;
|
||||||
|
private final SongRepository songRepository = new SongRepository();
|
||||||
|
private final AlbumRepository albumRepository = new AlbumRepository();
|
||||||
|
private final ArtistRepository artistRepository = new ArtistRepository();
|
||||||
|
private final PlaylistRepository playlistRepository = new PlaylistRepository();
|
||||||
|
|
||||||
|
public AssetLinkNavigator(@NonNull MainActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(@Nullable AssetLinkUtil.AssetLink assetLink) {
|
||||||
|
if (assetLink == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (assetLink.type) {
|
||||||
|
case AssetLinkUtil.TYPE_SONG:
|
||||||
|
openSong(assetLink.id);
|
||||||
|
break;
|
||||||
|
case AssetLinkUtil.TYPE_ALBUM:
|
||||||
|
openAlbum(assetLink.id);
|
||||||
|
break;
|
||||||
|
case AssetLinkUtil.TYPE_ARTIST:
|
||||||
|
openArtist(assetLink.id);
|
||||||
|
break;
|
||||||
|
case AssetLinkUtil.TYPE_PLAYLIST:
|
||||||
|
openPlaylist(assetLink.id);
|
||||||
|
break;
|
||||||
|
case AssetLinkUtil.TYPE_GENRE:
|
||||||
|
openGenre(assetLink.id);
|
||||||
|
break;
|
||||||
|
case AssetLinkUtil.TYPE_YEAR:
|
||||||
|
openYear(assetLink.id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openSong(@NonNull String id) {
|
||||||
|
MutableLiveData<Child> liveData = songRepository.getSong(id);
|
||||||
|
Observer<Child> observer = new Observer<Child>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(Child child) {
|
||||||
|
liveData.removeObserver(this);
|
||||||
|
if (child == null) {
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_song, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SongBottomSheetViewModel viewModel = new ViewModelProvider(activity).get(SongBottomSheetViewModel.class);
|
||||||
|
viewModel.setSong(child);
|
||||||
|
SongBottomSheetDialog dialog = new SongBottomSheetDialog();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable(Constants.TRACK_OBJECT, child);
|
||||||
|
dialog.setArguments(args);
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
liveData.observe(activity, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAlbum(@NonNull String id) {
|
||||||
|
MutableLiveData<AlbumID3> liveData = albumRepository.getAlbum(id);
|
||||||
|
Observer<AlbumID3> observer = new Observer<AlbumID3>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(AlbumID3 album) {
|
||||||
|
liveData.removeObserver(this);
|
||||||
|
if (album == null) {
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_album, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable(Constants.ALBUM_OBJECT, album);
|
||||||
|
navigateSafely(R.id.albumPageFragment, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
liveData.observe(activity, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openArtist(@NonNull String id) {
|
||||||
|
MutableLiveData<ArtistID3> liveData = artistRepository.getArtist(id);
|
||||||
|
Observer<ArtistID3> observer = new Observer<ArtistID3>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(ArtistID3 artist) {
|
||||||
|
liveData.removeObserver(this);
|
||||||
|
if (artist == null) {
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_artist, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable(Constants.ARTIST_OBJECT, artist);
|
||||||
|
navigateSafely(R.id.artistPageFragment, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
liveData.observe(activity, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openPlaylist(@NonNull String id) {
|
||||||
|
MutableLiveData<Playlist> liveData = playlistRepository.getPlaylist(id);
|
||||||
|
Observer<Playlist> observer = new Observer<Playlist>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(Playlist playlist) {
|
||||||
|
liveData.removeObserver(this);
|
||||||
|
if (playlist == null) {
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_playlist, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable(Constants.PLAYLIST_OBJECT, playlist);
|
||||||
|
navigateSafely(R.id.playlistPageFragment, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
liveData.observe(activity, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openGenre(@NonNull String genreName) {
|
||||||
|
String trimmed = genreName.trim();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Genre genre = new Genre();
|
||||||
|
genre.setGenre(trimmed);
|
||||||
|
genre.setSongCount(0);
|
||||||
|
genre.setAlbumCount(0);
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putParcelable(Constants.GENRE_OBJECT, genre);
|
||||||
|
args.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE);
|
||||||
|
navigateSafely(R.id.songListPageFragment, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openYear(@NonNull String yearValue) {
|
||||||
|
try {
|
||||||
|
int year = Integer.parseInt(yearValue.trim());
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putInt("year_object", year);
|
||||||
|
args.putString(Constants.MEDIA_BY_YEAR, Constants.MEDIA_BY_YEAR);
|
||||||
|
navigateSafely(R.id.songListPageFragment, args);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateSafely(int destinationId, @Nullable Bundle args) {
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
NavController navController = activity.navController;
|
||||||
|
if (navController == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (navController.getCurrentDestination() != null
|
||||||
|
&& navController.getCurrentDestination().getId() == destinationId) {
|
||||||
|
navController.navigate(destinationId, args, new NavOptions.Builder().setLaunchSingleTop(true).build());
|
||||||
|
} else {
|
||||||
|
navController.navigate(destinationId, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import com.google.android.material.color.MaterialColors;
|
||||||
|
|
||||||
|
public final class AssetLinkUtil {
|
||||||
|
public static final String SCHEME = "tempo";
|
||||||
|
public static final String HOST_ASSET = "asset";
|
||||||
|
|
||||||
|
public static final String TYPE_SONG = "song";
|
||||||
|
public static final String TYPE_ALBUM = "album";
|
||||||
|
public static final String TYPE_ARTIST = "artist";
|
||||||
|
public static final String TYPE_PLAYLIST = "playlist";
|
||||||
|
public static final String TYPE_GENRE = "genre";
|
||||||
|
public static final String TYPE_YEAR = "year";
|
||||||
|
|
||||||
|
private AssetLinkUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static AssetLink parse(@Nullable Intent intent) {
|
||||||
|
if (intent == null) return null;
|
||||||
|
return parse(intent.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static AssetLink parse(@Nullable Uri uri) {
|
||||||
|
if (uri == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SCHEME.equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String host = uri.getHost();
|
||||||
|
if (!HOST_ASSET.equalsIgnoreCase(host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.getPathSegments().size() < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = uri.getPathSegments().get(0);
|
||||||
|
String id = uri.getPathSegments().get(1);
|
||||||
|
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupportedType(type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AssetLink(type, id, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSupportedType(@Nullable String type) {
|
||||||
|
if (type == null) return false;
|
||||||
|
switch (type) {
|
||||||
|
case TYPE_SONG:
|
||||||
|
case TYPE_ALBUM:
|
||||||
|
case TYPE_ARTIST:
|
||||||
|
case TYPE_PLAYLIST:
|
||||||
|
case TYPE_GENRE:
|
||||||
|
case TYPE_YEAR:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Uri buildUri(@NonNull String type, @NonNull String id) {
|
||||||
|
return new Uri.Builder()
|
||||||
|
.scheme(SCHEME)
|
||||||
|
.authority(HOST_ASSET)
|
||||||
|
.appendPath(type)
|
||||||
|
.appendPath(id)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String buildLink(@Nullable String type, @Nullable String id) {
|
||||||
|
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id) || !isSupportedType(type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buildUri(Objects.requireNonNull(type), Objects.requireNonNull(id)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static AssetLink buildAssetLink(@Nullable String type, @Nullable String id) {
|
||||||
|
String link = buildLink(type, id);
|
||||||
|
return parseLinkString(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static AssetLink parseLinkString(@Nullable String link) {
|
||||||
|
if (TextUtils.isEmpty(link)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parse(Uri.parse(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyToClipboard(@NonNull Context context, @NonNull AssetLink assetLink) {
|
||||||
|
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
if (clipboardManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ClipData clipData = ClipData.newPlainText(context.getString(R.string.asset_link_clipboard_label), assetLink.uri.toString());
|
||||||
|
clipboardManager.setPrimaryClip(clipData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
public static int getLabelRes(@NonNull String type) {
|
||||||
|
switch (type) {
|
||||||
|
case TYPE_SONG:
|
||||||
|
return R.string.asset_link_label_song;
|
||||||
|
case TYPE_ALBUM:
|
||||||
|
return R.string.asset_link_label_album;
|
||||||
|
case TYPE_ARTIST:
|
||||||
|
return R.string.asset_link_label_artist;
|
||||||
|
case TYPE_PLAYLIST:
|
||||||
|
return R.string.asset_link_label_playlist;
|
||||||
|
case TYPE_GENRE:
|
||||||
|
return R.string.asset_link_label_genre;
|
||||||
|
case TYPE_YEAR:
|
||||||
|
return R.string.asset_link_label_year;
|
||||||
|
default:
|
||||||
|
return R.string.asset_link_label_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyLinkAppearance(@NonNull View view) {
|
||||||
|
if (view instanceof TextView) {
|
||||||
|
TextView textView = (TextView) view;
|
||||||
|
if (textView.getTag(R.id.tag_link_original_color) == null) {
|
||||||
|
textView.setTag(R.id.tag_link_original_color, textView.getCurrentTextColor());
|
||||||
|
}
|
||||||
|
int accent = MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimary,
|
||||||
|
ContextCompat.getColor(view.getContext(), android.R.color.holo_blue_light));
|
||||||
|
textView.setTextColor(accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearLinkAppearance(@NonNull View view) {
|
||||||
|
if (view instanceof TextView) {
|
||||||
|
TextView textView = (TextView) view;
|
||||||
|
Object original = textView.getTag(R.id.tag_link_original_color);
|
||||||
|
if (original instanceof Integer) {
|
||||||
|
textView.setTextColor((Integer) original);
|
||||||
|
} else {
|
||||||
|
int defaultColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnSurface,
|
||||||
|
ContextCompat.getColor(view.getContext(), android.R.color.primary_text_light));
|
||||||
|
textView.setTextColor(defaultColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class AssetLink {
|
||||||
|
public final String type;
|
||||||
|
public final String id;
|
||||||
|
public final Uri uri;
|
||||||
|
|
||||||
|
AssetLink(@NonNull String type, @NonNull String id, @NonNull Uri uri) {
|
||||||
|
this.type = type;
|
||||||
|
this.id = id;
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,9 @@ import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
|
@ -102,35 +105,76 @@ public class ExternalAudioWriter {
|
||||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String scheme = mediaUri.getScheme();
|
|
||||||
if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
|
String scheme = mediaUri.getScheme() != null ? mediaUri.getScheme().toLowerCase(Locale.ROOT) : "";
|
||||||
notifyFailure(context, "Unsupported media URI.");
|
|
||||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpURLConnection connection = null;
|
HttpURLConnection connection = null;
|
||||||
|
DocumentFile sourceDocument = null;
|
||||||
|
File sourceFile = null;
|
||||||
|
long remoteLength = -1;
|
||||||
|
String mimeType = null;
|
||||||
DocumentFile targetFile = 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();
|
try {
|
||||||
if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) {
|
if (scheme.equals("http") || scheme.equals("https")) {
|
||||||
notifyFailure(context, "Server returned " + responseCode);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType = connection.getContentType();
|
||||||
|
remoteLength = connection.getContentLengthLong();
|
||||||
|
} else if (scheme.equals("content")) {
|
||||||
|
sourceDocument = DocumentFile.fromSingleUri(context, mediaUri);
|
||||||
|
mimeType = context.getContentResolver().getType(mediaUri);
|
||||||
|
if (sourceDocument != null) {
|
||||||
|
remoteLength = sourceDocument.length();
|
||||||
|
}
|
||||||
|
} else if (scheme.equals("file")) {
|
||||||
|
String path = mediaUri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
sourceFile = new File(path);
|
||||||
|
if (sourceFile.exists()) {
|
||||||
|
remoteLength = sourceFile.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String ext = MimeTypeMap.getFileExtensionFromUrl(mediaUri.toString());
|
||||||
|
if (ext != null && !ext.isEmpty()) {
|
||||||
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifyFailure(context, "Unsupported media URI.");
|
||||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String mimeType = connection.getContentType();
|
|
||||||
if (mimeType == null || mimeType.isEmpty()) {
|
if (mimeType == null || mimeType.isEmpty()) {
|
||||||
mimeType = "application/octet-stream";
|
mimeType = "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||||
|
if ((extension == null || extension.isEmpty()) && sourceDocument != null && sourceDocument.getName() != null) {
|
||||||
|
String name = sourceDocument.getName();
|
||||||
|
int dot = name.lastIndexOf('.');
|
||||||
|
if (dot >= 0 && dot < name.length() - 1) {
|
||||||
|
extension = name.substring(dot + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((extension == null || extension.isEmpty()) && sourceFile != null) {
|
||||||
|
String name = sourceFile.getName();
|
||||||
|
int dot = name.lastIndexOf('.');
|
||||||
|
if (dot >= 0 && dot < name.length() - 1) {
|
||||||
|
extension = name.substring(dot + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (extension == null || extension.isEmpty()) {
|
if (extension == null || extension.isEmpty()) {
|
||||||
String suffix = child.getSuffix();
|
String suffix = child.getSuffix();
|
||||||
if (suffix != null && !suffix.isEmpty()) {
|
if (suffix != null && !suffix.isEmpty()) {
|
||||||
|
|
@ -146,7 +190,6 @@ public class ExternalAudioWriter {
|
||||||
String fileName = sanitized + "." + extension;
|
String fileName = sanitized + "." + extension;
|
||||||
|
|
||||||
DocumentFile existingFile = findFile(directory, fileName);
|
DocumentFile existingFile = findFile(directory, fileName);
|
||||||
long remoteLength = connection.getContentLengthLong();
|
|
||||||
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
|
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
|
||||||
if (existingFile != null && existingFile.exists()) {
|
if (existingFile != null && existingFile.exists()) {
|
||||||
long localLength = existingFile.length();
|
long localLength = existingFile.length();
|
||||||
|
|
@ -175,7 +218,7 @@ public class ExternalAudioWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri targetUri = targetFile.getUri();
|
Uri targetUri = targetFile.getUri();
|
||||||
try (InputStream in = connection.getInputStream();
|
try (InputStream in = openInputStream(context, mediaUri, scheme, connection, sourceFile);
|
||||||
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
|
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
notifyFailure(context, "Cannot open output stream.");
|
notifyFailure(context, "Cannot open output stream.");
|
||||||
|
|
@ -319,4 +362,32 @@ public class ExternalAudioWriter {
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static InputStream openInputStream(Context context,
|
||||||
|
Uri mediaUri,
|
||||||
|
String scheme,
|
||||||
|
HttpURLConnection connection,
|
||||||
|
File sourceFile) throws IOException {
|
||||||
|
switch (scheme) {
|
||||||
|
case "http":
|
||||||
|
case "https":
|
||||||
|
if (connection == null) {
|
||||||
|
throw new IOException("Connection not initialized");
|
||||||
|
}
|
||||||
|
return connection.getInputStream();
|
||||||
|
case "content":
|
||||||
|
InputStream contentStream = context.getContentResolver().openInputStream(mediaUri);
|
||||||
|
if (contentStream == null) {
|
||||||
|
throw new IOException("Cannot open content stream");
|
||||||
|
}
|
||||||
|
return contentStream;
|
||||||
|
case "file":
|
||||||
|
if (sourceFile == null || !sourceFile.exists()) {
|
||||||
|
throw new IOException("Missing source file");
|
||||||
|
}
|
||||||
|
return new FileInputStream(sourceFile);
|
||||||
|
default:
|
||||||
|
throw new IOException("Unsupported scheme " + scheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,12 @@ public class MappingUtil {
|
||||||
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
||||||
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
||||||
bundle.putString("uri", uri.toString());
|
bundle.putString("uri", uri.toString());
|
||||||
|
bundle.putString("assetLinkSong", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId()));
|
||||||
|
bundle.putString("assetLinkAlbum", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId()));
|
||||||
|
bundle.putString("assetLinkArtist", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId()));
|
||||||
|
bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre()));
|
||||||
|
Integer year = media.getYear();
|
||||||
|
bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null);
|
||||||
|
|
||||||
return new MediaItem.Builder()
|
return new MediaItem.Builder()
|
||||||
.setMediaId(media.getId())
|
.setMediaId(media.getId())
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ object Preferences {
|
||||||
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_DETAIL = "album_detail"
|
private const val ALBUM_DETAIL = "album_detail"
|
||||||
|
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? {
|
||||||
|
|
@ -644,4 +646,14 @@ object Preferences {
|
||||||
fun showAlbumDetail(): Boolean {
|
fun showAlbumDetail(): Boolean {
|
||||||
return App.getInstance().preferences.getBoolean(ALBUM_DETAIL, false)
|
return App.getInstance().preferences.getBoolean(ALBUM_DETAIL, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,17 +5,20 @@ import android.appwidget.AppWidgetManager;
|
||||||
import android.appwidget.AppWidgetProvider;
|
import android.appwidget.AppWidgetProvider;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.widget.RemoteViews;
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
||||||
import android.app.TaskStackBuilder;
|
import android.app.TaskStackBuilder;
|
||||||
import android.app.PendingIntent;
|
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class WidgetProvider extends AppWidgetProvider {
|
public class WidgetProvider extends AppWidgetProvider {
|
||||||
private static final String TAG = "TempoWidget";
|
private static final String TAG = "TempoWidget";
|
||||||
public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE";
|
public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE";
|
||||||
|
|
@ -28,7 +31,7 @@ public class WidgetProvider extends AppWidgetProvider {
|
||||||
public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
|
public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
|
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
|
||||||
attachIntents(ctx, rv, id);
|
attachIntents(ctx, rv, id, null, null, null);
|
||||||
mgr.updateAppWidget(id, rv);
|
mgr.updateAppWidget(id, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,16 +53,23 @@ public class WidgetProvider extends AppWidgetProvider {
|
||||||
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) {
|
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) {
|
||||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||||
RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId);
|
RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId);
|
||||||
attachIntents(context, rv, appWidgetId);
|
attachIntents(context, rv, appWidgetId, null, null, null);
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, rv);
|
appWidgetManager.updateAppWidget(appWidgetId, rv);
|
||||||
WidgetUpdateManager.refreshFromController(context);
|
WidgetUpdateManager.refreshFromController(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void attachIntents(Context ctx, RemoteViews rv) {
|
public static void attachIntents(Context ctx, RemoteViews rv) {
|
||||||
attachIntents(ctx, rv, 0);
|
attachIntents(ctx, rv, 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) {
|
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) {
|
||||||
|
attachIntents(ctx, rv, requestCodeBase, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase,
|
||||||
|
String songLink,
|
||||||
|
String albumLink,
|
||||||
|
String artistLink) {
|
||||||
PendingIntent playPause = PendingIntent.getBroadcast(
|
PendingIntent playPause = PendingIntent.getBroadcast(
|
||||||
ctx,
|
ctx,
|
||||||
requestCodeBase + 0,
|
requestCodeBase + 0,
|
||||||
|
|
@ -97,9 +107,31 @@ public class WidgetProvider extends AppWidgetProvider {
|
||||||
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
|
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
|
||||||
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
|
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
|
||||||
|
|
||||||
PendingIntent launch = TaskStackBuilder.create(ctx)
|
PendingIntent launch = buildMainActivityPendingIntent(ctx, requestCodeBase + 10, null);
|
||||||
.addNextIntentWithParentStack(new Intent(ctx, MainActivity.class))
|
|
||||||
.getPendingIntent(requestCodeBase + 10, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
rv.setOnClickPendingIntent(R.id.root, launch);
|
rv.setOnClickPendingIntent(R.id.root, launch);
|
||||||
|
|
||||||
|
PendingIntent songPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 20, songLink);
|
||||||
|
PendingIntent artistPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 21, artistLink);
|
||||||
|
PendingIntent albumPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 22, albumLink);
|
||||||
|
|
||||||
|
PendingIntent fallback = launch;
|
||||||
|
rv.setOnClickPendingIntent(R.id.album_art, songPending != null ? songPending : fallback);
|
||||||
|
rv.setOnClickPendingIntent(R.id.title, songPending != null ? songPending : fallback);
|
||||||
|
rv.setOnClickPendingIntent(R.id.subtitle,
|
||||||
|
artistPending != null ? artistPending : (songPending != null ? songPending : fallback));
|
||||||
|
rv.setOnClickPendingIntent(R.id.album, albumPending != null ? albumPending : fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PendingIntent buildMainActivityPendingIntent(Context ctx, int requestCode, @Nullable String link) {
|
||||||
|
Intent intent;
|
||||||
|
if (!TextUtils.isEmpty(link)) {
|
||||||
|
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link), ctx, MainActivity.class);
|
||||||
|
} else {
|
||||||
|
intent = new Intent(ctx, MainActivity.class);
|
||||||
|
}
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx);
|
||||||
|
stackBuilder.addNextIntentWithParentStack(intent);
|
||||||
|
return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import android.appwidget.AppWidgetManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget;
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
import com.bumptech.glide.request.transition.Transition;
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
|
|
@ -17,6 +18,7 @@ import androidx.media3.session.MediaController;
|
||||||
import androidx.media3.session.SessionToken;
|
import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
@ -25,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,
|
||||||
|
|
@ -34,7 +38,10 @@ public final class WidgetUpdateManager {
|
||||||
boolean shuffleEnabled,
|
boolean shuffleEnabled,
|
||||||
int repeatMode,
|
int repeatMode,
|
||||||
long positionMs,
|
long positionMs,
|
||||||
long durationMs) {
|
long durationMs,
|
||||||
|
String songLink,
|
||||||
|
String albumLink,
|
||||||
|
String artistLink) {
|
||||||
if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing);
|
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(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle);
|
||||||
if (TextUtils.isEmpty(album)) album = "";
|
if (TextUtils.isEmpty(album)) album = "";
|
||||||
|
|
@ -46,7 +53,7 @@ public final class WidgetUpdateManager {
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing,
|
android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing,
|
||||||
timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id);
|
timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id);
|
||||||
WidgetProvider.attachIntents(ctx, rv, id);
|
WidgetProvider.attachIntents(ctx, rv, id, songLink, albumLink, artistLink);
|
||||||
mgr.updateAppWidget(id, rv);
|
mgr.updateAppWidget(id, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +63,7 @@ public final class WidgetUpdateManager {
|
||||||
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
|
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
android.widget.RemoteViews rv = chooseBuild(ctx, id);
|
android.widget.RemoteViews rv = chooseBuild(ctx, id);
|
||||||
WidgetProvider.attachIntents(ctx, rv, id);
|
WidgetProvider.attachIntents(ctx, rv, id, null, null, null);
|
||||||
mgr.updateAppWidget(id, rv);
|
mgr.updateAppWidget(id, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +77,10 @@ public final class WidgetUpdateManager {
|
||||||
boolean shuffleEnabled,
|
boolean shuffleEnabled,
|
||||||
int repeatMode,
|
int repeatMode,
|
||||||
long positionMs,
|
long positionMs,
|
||||||
long durationMs) {
|
long durationMs,
|
||||||
|
String songLink,
|
||||||
|
String albumLink,
|
||||||
|
String artistLink) {
|
||||||
final Context appCtx = ctx.getApplicationContext();
|
final Context appCtx = ctx.getApplicationContext();
|
||||||
final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title;
|
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 a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist;
|
||||||
|
|
@ -79,12 +89,15 @@ public final class WidgetUpdateManager {
|
||||||
final boolean sh = shuffleEnabled;
|
final boolean sh = shuffleEnabled;
|
||||||
final int rep = repeatMode;
|
final int rep = repeatMode;
|
||||||
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
|
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
|
||||||
|
final String songLinkFinal = songLink;
|
||||||
|
final String albumLinkFinal = albumLink;
|
||||||
|
final String artistLinkFinal = artistLink;
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(coverArtId)) {
|
if (!TextUtils.isEmpty(coverArtId)) {
|
||||||
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) {
|
||||||
|
|
@ -93,7 +106,7 @@ public final class WidgetUpdateManager {
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p,
|
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p,
|
||||||
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||||
WidgetProvider.attachIntents(appCtx, rv, id);
|
WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
|
||||||
mgr.updateAppWidget(id, rv);
|
mgr.updateAppWidget(id, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +118,7 @@ public final class WidgetUpdateManager {
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
||||||
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||||
WidgetProvider.attachIntents(appCtx, rv, id);
|
WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
|
||||||
mgr.updateAppWidget(id, rv);
|
mgr.updateAppWidget(id, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +130,7 @@ public final class WidgetUpdateManager {
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
||||||
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||||
WidgetProvider.attachIntents(appCtx, rv, id);
|
WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
|
||||||
mgr.updateAppWidget(id, rv);
|
mgr.updateAppWidget(id, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +146,7 @@ public final class WidgetUpdateManager {
|
||||||
MediaController c = future.get();
|
MediaController c = future.get();
|
||||||
androidx.media3.common.MediaItem mi = c.getCurrentMediaItem();
|
androidx.media3.common.MediaItem mi = c.getCurrentMediaItem();
|
||||||
String title = null, artist = null, album = null, coverId = null;
|
String title = null, artist = null, album = null, coverId = null;
|
||||||
|
String songLink = null, albumLink = null, artistLink = null;
|
||||||
if (mi != null && mi.mediaMetadata != null) {
|
if (mi != null && mi.mediaMetadata != null) {
|
||||||
if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString();
|
if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString();
|
||||||
if (mi.mediaMetadata.artist != null)
|
if (mi.mediaMetadata.artist != null)
|
||||||
|
|
@ -140,10 +154,26 @@ public final class WidgetUpdateManager {
|
||||||
if (mi.mediaMetadata.albumTitle != null)
|
if (mi.mediaMetadata.albumTitle != null)
|
||||||
album = mi.mediaMetadata.albumTitle.toString();
|
album = mi.mediaMetadata.albumTitle.toString();
|
||||||
if (mi.mediaMetadata.extras != null) {
|
if (mi.mediaMetadata.extras != null) {
|
||||||
|
Bundle extras = mi.mediaMetadata.extras;
|
||||||
if (title == null) title = mi.mediaMetadata.extras.getString("title");
|
if (title == null) title = mi.mediaMetadata.extras.getString("title");
|
||||||
if (artist == null) artist = mi.mediaMetadata.extras.getString("artist");
|
if (artist == null) artist = mi.mediaMetadata.extras.getString("artist");
|
||||||
if (album == null) album = mi.mediaMetadata.extras.getString("album");
|
if (album == null) album = mi.mediaMetadata.extras.getString("album");
|
||||||
coverId = mi.mediaMetadata.extras.getString("coverArtId");
|
coverId = extras.getString("coverArtId");
|
||||||
|
|
||||||
|
songLink = extras.getString("assetLinkSong");
|
||||||
|
if (songLink == null) {
|
||||||
|
songLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras.getString("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
albumLink = extras.getString("assetLinkAlbum");
|
||||||
|
if (albumLink == null) {
|
||||||
|
albumLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras.getString("albumId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
artistLink = extras.getString("assetLinkArtist");
|
||||||
|
if (artistLink == null) {
|
||||||
|
artistLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras.getString("artistId"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
long position = c.getCurrentPosition();
|
long position = c.getCurrentPosition();
|
||||||
|
|
@ -159,7 +189,10 @@ public final class WidgetUpdateManager {
|
||||||
c.getShuffleModeEnabled(),
|
c.getShuffleModeEnabled(),
|
||||||
c.getRepeatMode(),
|
c.getRepeatMode(),
|
||||||
position,
|
position,
|
||||||
duration);
|
duration,
|
||||||
|
songLink,
|
||||||
|
albumLink,
|
||||||
|
artistLink);
|
||||||
c.release();
|
c.release();
|
||||||
} catch (ExecutionException | InterruptedException ignored) {
|
} catch (ExecutionException | InterruptedException ignored) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
app/src/main/res/drawable/ic_link.xml
Normal file
10
app/src/main/res/drawable/ic_link.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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="?attr/colorOnSurfaceVariant"
|
||||||
|
android:pathData="M3.9,12c0,1.71 1.39,3.1 3.1,3.1h3v1.8h-3c-2.7,0 -4.9,-2.2 -4.9,-4.9s2.2,-4.9 4.9,-4.9h3v1.8h-3c-1.71,0 -3.1,1.39 -3.1,3.1zM7,13h10v-2H7v2zM17,6.9h-3v-1.8h3c2.7,0 4.9,2.2 4.9,4.9s-2.2,4.9 -4.9,4.9h-3v-1.8h3c1.71,0 3.1,-1.39 3.1,-3.1s-1.39,-3.1 -3.1,-3.1z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -68,6 +68,14 @@
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/song_asset_link_row"
|
||||||
|
layout="@layout/view_asset_link_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="12dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/option_linear_layout"
|
android:id="@+id/option_linear_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,17 @@
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/player_asset_link_row"
|
||||||
|
layout="@layout/view_asset_link_row"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/player_media_quality_sector" />
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/player_media_cover_view_pager"
|
android:id="@+id/player_media_cover_view_pager"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
@ -66,7 +77,7 @@
|
||||||
app:layout_constraintBottom_toTopOf="@id/guideline"
|
app:layout_constraintBottom_toTopOf="@id/guideline"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/player_media_quality_sector" />
|
app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/guideline"
|
android:id="@+id/guideline"
|
||||||
|
|
|
||||||
55
app/src/main/res/layout/view_asset_link_row.xml
Normal file
55
app/src/main/res/layout/view_asset_link_row.xml
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.chip.ChipGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/asset_link_chip_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
app:singleLine="true"
|
||||||
|
app:selectionRequired="false"
|
||||||
|
app:singleSelection="false">
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/asset_link_song_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Assist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text=""
|
||||||
|
app:chipIcon="@drawable/ic_link"
|
||||||
|
app:chipIconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:rippleColor="@color/ripple_material_light" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/asset_link_album_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Assist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text=""
|
||||||
|
app:chipIcon="@drawable/ic_link"
|
||||||
|
app:chipIconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:rippleColor="@color/ripple_material_light" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/asset_link_artist_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Assist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text=""
|
||||||
|
app:chipIcon="@drawable/ic_link"
|
||||||
|
app:chipIconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:rippleColor="@color/ripple_material_light" />
|
||||||
|
</com.google.android.material.chip.ChipGroup>
|
||||||
|
|
@ -88,6 +88,9 @@
|
||||||
<string name="error_required">Wymagane</string>
|
<string name="error_required">Wymagane</string>
|
||||||
<string name="error_server_prefix">wymagany jest prefiks http lub https</string>
|
<string name="error_server_prefix">wymagany jest prefiks http lub https</string>
|
||||||
<string name="exo_download_notification_channel_name">Pobieranie</string>
|
<string name="exo_download_notification_channel_name">Pobieranie</string>
|
||||||
|
<string name="exo_controls_heart_off_description">Wyłącz serce</string>
|
||||||
|
<string name="exo_controls_heart_on_description">Włącz serce</string>
|
||||||
|
<string name="cast_expanded_controller_loading">Ładowanie…</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_artist">Filtruj wykonawców</string>
|
||||||
|
|
@ -213,8 +216,9 @@
|
||||||
<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_success">Dodano piosenki do playlisty</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenki do playlisty</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenek do playlisty</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_all_skipped">Pominięto wszystkie piosenki jako duplikaty</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>
|
||||||
|
|
@ -281,6 +285,8 @@
|
||||||
<string name="settings_about_summary">Tempo jest otwarto-źródłowym i lekkim klientem muzycznym dla Subsonic, stworzonym i zbudowanym natywnie dla Androida.</string>
|
<string name="settings_about_summary">Tempo jest otwarto-źródłowym i lekkim klientem muzycznym dla Subsonic, stworzonym i zbudowanym natywnie dla Androida.</string>
|
||||||
<string name="settings_about_title">O aplikacji</string>
|
<string name="settings_about_title">O aplikacji</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">Zezwalaj na dodawania duplikatów do playlist</string>
|
||||||
|
<string name="settings_allow_playlist_duplicates_summary">Jeżeli włączone, duplikaty nie będą sprawdzane podczas dodawania do playlisty.</string>
|
||||||
<string name="settings_audio_transcode_download_format">Format transkodowania</string>
|
<string name="settings_audio_transcode_download_format">Format transkodowania</string>
|
||||||
<string name="settings_audio_transcode_download_priority_summary">Jeżeli włączone, Tempo nie będzię wymuszał pobierania utworu z ustawieniami transkodowania wybranymi poniżej.</string>
|
<string name="settings_audio_transcode_download_priority_summary">Jeżeli włączone, Tempo nie będzię wymuszał pobierania utworu z ustawieniami transkodowania wybranymi poniżej.</string>
|
||||||
<string name="settings_audio_transcode_download_priority_title">Priorytetyzuj ustawienia serwera używanego do strumieniowania w pobieraniach</string>
|
<string name="settings_audio_transcode_download_priority_title">Priorytetyzuj ustawienia serwera używanego do strumieniowania w pobieraniach</string>
|
||||||
|
|
@ -296,6 +302,8 @@
|
||||||
<string name="settings_audio_transcode_priority_toast">Priorytet przy transkodowaniu utworu danego serwerowi</string>
|
<string name="settings_audio_transcode_priority_toast">Priorytet przy transkodowaniu utworu danego serwerowi</string>
|
||||||
<string name="settings_buffering_strategy">Strategia buforowania</string>
|
<string name="settings_buffering_strategy">Strategia buforowania</string>
|
||||||
<string name="settings_buffering_strategy_summary">Aby zmiany przyniosły efekt, musisz ręcznie zrestartować aplikację.</string>
|
<string name="settings_buffering_strategy_summary">Aby zmiany przyniosły efekt, musisz ręcznie zrestartować aplikację.</string>
|
||||||
|
<string name="settings_choose_download_folder">Wybierz folder dla pobranych plików muzycznych</string>
|
||||||
|
<string name="settings_clear_download_folder">Wyczyść folder pobierania</string>
|
||||||
<string name="settings_continuous_play_summary">Pozwala muzyce odtwarzać się dalej po końcu playlisty, odtwarza podobne piosenki</string>
|
<string name="settings_continuous_play_summary">Pozwala muzyce odtwarzać się dalej po końcu playlisty, odtwarza podobne piosenki</string>
|
||||||
<string name="settings_continuous_play_title">Odtwarzanie bez przerwy</string>
|
<string name="settings_continuous_play_title">Odtwarzanie bez przerwy</string>
|
||||||
<string name="settings_covers_cache">Rozmiar cache dla okładek</string>
|
<string name="settings_covers_cache">Rozmiar cache dla okładek</string>
|
||||||
|
|
@ -304,6 +312,9 @@
|
||||||
<string name="settings_delete_download_storage_summary">Zatwierdzenie nieodwracalnie usunie wszystkie zapisane elementy</string>
|
<string name="settings_delete_download_storage_summary">Zatwierdzenie nieodwracalnie usunie wszystkie zapisane elementy</string>
|
||||||
<string name="settings_delete_download_storage_title">Usuń zapisane elementy</string>
|
<string name="settings_delete_download_storage_title">Usuń zapisane elementy</string>
|
||||||
<string name="settings_download_storage_title">Pamięć do pobierania</string>
|
<string name="settings_download_storage_title">Pamięć do pobierania</string>
|
||||||
|
<string name="settings_download_folder_cleared">Utworzono folder pobierania.</string>
|
||||||
|
<string name="settings_download_folder_set">Wybrano folder pobierania</string>
|
||||||
|
<string name="settings_set_download_folder">Ustaw folder pobierania</string>
|
||||||
<string name="settings_system_equalizer_summary">Zmień ustawienia audio</string>
|
<string name="settings_system_equalizer_summary">Zmień ustawienia audio</string>
|
||||||
<string name="settings_system_equalizer_title">Korektor systemowy</string>
|
<string name="settings_system_equalizer_title">Korektor systemowy</string>
|
||||||
<string name="settings_github_link">https://github.com/eddyizm/tempo</string>
|
<string name="settings_github_link">https://github.com/eddyizm/tempo</string>
|
||||||
|
|
@ -312,6 +323,7 @@
|
||||||
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempo/discussions</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_summary">Dołącz do dyskusji i wsparcia społeczności</string>
|
||||||
<string name="settings_support_title">Wsparcie użytkowników</string>
|
<string name="settings_support_title">Wsparcie użytkowników</string>
|
||||||
|
<string name="settings_scan_result">Skanowanie: naliczono %1$d utworó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>
|
||||||
|
|
@ -332,6 +344,8 @@
|
||||||
<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 [Niedokończone]</string>
|
<string name="settings_queue_syncing_title">Synchronizuj kolejkę odtwarzania dla tego użytkownika [Niedokończone]</string>
|
||||||
|
<string name="settings_show_mini_shuffle_button">Pokaż przycisk odtwarzania losowego</string>
|
||||||
|
<string name="settings_show_mini_shuffle_button_summary">Jeżeli włączone, pokazuje przycisk losowego odtwarzania, i usuwa przycisk serca w mini odtwarzaczu</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">Automatyczne pobieranie tesktów</string>
|
||||||
|
|
@ -366,6 +380,7 @@
|
||||||
<string name="settings_theme">Motyw</string>
|
<string name="settings_theme">Motyw</string>
|
||||||
<string name="settings_title_data">Dane</string>
|
<string name="settings_title_data">Dane</string>
|
||||||
<string name="settings_title_general">Ogólne</string>
|
<string name="settings_title_general">Ogólne</string>
|
||||||
|
<string name="settings_title_playlist">Playlisty</string>
|
||||||
<string name="settings_title_rating">Oceny</string>
|
<string name="settings_title_rating">Oceny</string>
|
||||||
<string name="settings_title_replay_gain">Wzmocnienie głośności przy ponownym odtwarzaniu</string>
|
<string name="settings_title_replay_gain">Wzmocnienie głośności przy ponownym odtwarzaniu</string>
|
||||||
<string name="settings_title_scrobble">Scrobble</string>
|
<string name="settings_title_scrobble">Scrobble</string>
|
||||||
|
|
@ -454,6 +469,17 @@
|
||||||
<string name="undraw_page">unDraw</string>
|
<string name="undraw_page">unDraw</string>
|
||||||
<string name="undraw_thanks">Specjalne podziękowania dla unDraw bez którego ilustracji nie mogliśmy uczynić tej aplikacji jeszcze piękniejszą.</string>
|
<string name="undraw_thanks">Specjalne podziękowania dla unDraw bez którego ilustracji nie mogliśmy uczynić tej aplikacji jeszcze piękniejszą.</string>
|
||||||
<string name="undraw_url">https://undraw.co/</string>
|
<string name="undraw_url">https://undraw.co/</string>
|
||||||
|
<string name="widget_label">Widget Tempo</string>
|
||||||
|
<string name="widget_not_playing">Nie odtwarza</string>
|
||||||
|
<string name="widget_placeholder_subtitle">Otwórz 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">Okładka albumu</string>
|
||||||
|
<string name="widget_content_desc_play_pause">Play lub pauza</string>
|
||||||
|
<string name="widget_content_desc_next">Następny utwór</string>
|
||||||
|
<string name="widget_content_desc_prev">Poprzedni utwór</string>
|
||||||
|
<string name="widget_content_desc_shuffle">Przełącznik odtwarzania losowego</string>
|
||||||
|
<string name="widget_content_desc_repeat">Zmień tryb powtarzania</string>
|
||||||
<plurals name="home_sync_starred_albums_count">
|
<plurals name="home_sync_starred_albums_count">
|
||||||
<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>
|
||||||
|
|
|
||||||
3
app/src/main/res/values/ids.xml
Normal file
3
app/src/main/res/values/ids.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<item name="tag_link_original_color" type="id" />
|
||||||
|
</resources>
|
||||||
|
|
@ -410,6 +410,22 @@
|
||||||
<string name="share_bottom_sheet_update">Update share</string>
|
<string name="share_bottom_sheet_update">Update share</string>
|
||||||
<string name="share_subtitle_item">Expiration date: %1$s</string>
|
<string name="share_subtitle_item">Expiration date: %1$s</string>
|
||||||
<string name="share_unsupported_error">Sharing is not supported or not enabled</string>
|
<string name="share_unsupported_error">Sharing is not supported or not enabled</string>
|
||||||
|
<string name="asset_link_clipboard_label">Tempo asset link</string>
|
||||||
|
<string name="asset_link_label_song">Song UID</string>
|
||||||
|
<string name="asset_link_label_album">Album UID</string>
|
||||||
|
<string name="asset_link_label_artist">Artist UID</string>
|
||||||
|
<string name="asset_link_label_playlist">Playlist UID</string>
|
||||||
|
<string name="asset_link_label_genre">Genre UID</string>
|
||||||
|
<string name="asset_link_label_year">Year UID</string>
|
||||||
|
<string name="asset_link_label_unknown">Asset UID</string>
|
||||||
|
<string name="asset_link_error_unsupported">Unsupported asset link</string>
|
||||||
|
<string name="asset_link_error_song">Song could not be opened</string>
|
||||||
|
<string name="asset_link_error_album">Album could not be opened</string>
|
||||||
|
<string name="asset_link_error_artist">Artist could not be opened</string>
|
||||||
|
<string name="asset_link_error_playlist">Playlist could not be opened</string>
|
||||||
|
<string name="asset_link_chip_text">%1$s • %2$s</string>
|
||||||
|
<string name="asset_link_copied_toast">Copied %1$s to clipboard</string>
|
||||||
|
<string name="asset_link_debug_toast">Asset link: %1$s</string>
|
||||||
<string name="share_update_dialog_hint_description">Description</string>
|
<string name="share_update_dialog_hint_description">Description</string>
|
||||||
<string name="share_update_dialog_hint_expiration_date">Expiration date</string>
|
<string name="share_update_dialog_hint_expiration_date">Expiration date</string>
|
||||||
<string name="share_update_dialog_negative_button">Cancel</string>
|
<string name="share_update_dialog_negative_button">Cancel</string>
|
||||||
|
|
|
||||||
|
|
@ -14,15 +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.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
|
||||||
|
|
@ -83,6 +87,7 @@ class MediaService : MediaLibraryService() {
|
||||||
initializeCustomCommands()
|
initializeCustomCommands()
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
initializeMediaLibrarySession()
|
initializeMediaLibrarySession()
|
||||||
|
restorePlayerFromQueue()
|
||||||
initializePlayerListener()
|
initializePlayerListener()
|
||||||
initializeEqualizerManager()
|
initializeEqualizerManager()
|
||||||
|
|
||||||
|
|
@ -118,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) {
|
||||||
|
|
@ -225,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)
|
||||||
|
|
@ -268,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) {
|
||||||
|
|
@ -398,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. */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,7 +455,14 @@ class MediaService : MediaLibraryService() {
|
||||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||||
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
val extras = mi?.mediaMetadata?.extras
|
||||||
|
val coverId = extras?.getString("coverArtId")
|
||||||
|
val songLink = extras?.getString("assetLinkSong")
|
||||||
|
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||||
|
val albumLink = extras?.getString("assetLinkAlbum")
|
||||||
|
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||||
|
val artistLink = extras?.getString("assetLinkArtist")
|
||||||
|
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
WidgetUpdateManager.updateFromState(
|
WidgetUpdateManager.updateFromState(
|
||||||
|
|
@ -434,7 +475,10 @@ class MediaService : MediaLibraryService() {
|
||||||
player.shuffleModeEnabled,
|
player.shuffleModeEnabled,
|
||||||
player.repeatMode,
|
player.repeatMode,
|
||||||
position,
|
position,
|
||||||
duration
|
duration,
|
||||||
|
songLink,
|
||||||
|
albumLink,
|
||||||
|
artistLink
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,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,10 +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.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
|
||||||
|
|
@ -70,9 +74,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
|
|
||||||
initializeRepository()
|
initializeRepository()
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
initializeCastPlayer()
|
|
||||||
initializeMediaLibrarySession()
|
initializeMediaLibrarySession()
|
||||||
|
restorePlayerFromQueue()
|
||||||
initializePlayerListener()
|
initializePlayerListener()
|
||||||
|
initializeCastPlayer()
|
||||||
initializeEqualizerManager()
|
initializeEqualizerManager()
|
||||||
|
|
||||||
setPlayer(
|
setPlayer(
|
||||||
|
|
@ -142,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,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)
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +302,14 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||||
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
val extras = mi?.mediaMetadata?.extras
|
||||||
|
val coverId = extras?.getString("coverArtId")
|
||||||
|
val songLink = extras?.getString("assetLinkSong")
|
||||||
|
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||||
|
val albumLink = extras?.getString("assetLinkAlbum")
|
||||||
|
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||||
|
val artistLink = extras?.getString("assetLinkArtist")
|
||||||
|
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||||
WidgetUpdateManager.updateFromState(
|
WidgetUpdateManager.updateFromState(
|
||||||
|
|
@ -275,7 +322,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||||
player.shuffleModeEnabled,
|
player.shuffleModeEnabled,
|
||||||
player.repeatMode,
|
player.repeatMode,
|
||||||
position,
|
position,
|
||||||
duration
|
duration,
|
||||||
|
songLink,
|
||||||
|
albumLink,
|
||||||
|
artistLink
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
mockup/usage/player_icons.png
Normal file
BIN
mockup/usage/player_icons.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Loading…
Add table
Add a link
Reference in a new issue