12 Commits

Author SHA1 Message Date
eddyizm
82c22ed247 chore: bumped version for release 2025-10-10 22:17:09 -07:00
eddyizm
48ce3a2a4f chore: updated release info 2025-10-10 22:02:01 -07:00
eddyizm
b93acc6563 fix: Glide module incorrectly encoding IPv6 addresses (#159) 2025-10-09 22:18:49 -07:00
eddyizm
9c088a7e88 feat: Make all objects in Tempo references for quick access (#158) 2025-10-09 22:18:11 -07:00
eddyizm
de2f1067a7 chore: Update Polish translation (#160) 2025-10-09 22:11:19 -07:00
eddyizm
1c21546461 chore: adding screenshot and docs for 4 icons/buttons in player control (#162) 2025-10-09 21:54:38 -07:00
eddyizm
a4121e8d49 chore: adding screenshot and docs for 4 icons/buttons in player control 2025-10-09 21:53:52 -07:00
skajmer
4cc4cc7363 Update Polish translation
Stuff from:
#140 
#135 
#98 
#152
2025-10-09 21:41:12 +02:00
le-firehawk
c5ef274916 fix: Glide module incorrectly encoding IPv6 addresses 2025-10-09 23:14:54 +10:30
le-firehawk
2c53f36a18 fix: Support content URIs for external downloader 2025-10-09 23:03:57 +10:30
le-firehawk
6c637dcbcb feat: Make all objects in Tempo references for quick access 2025-10-09 23:03:57 +10:30
eddyizm
89fa38f5a0 chore: updated change log 2025-10-08 21:55:00 -07:00
31 changed files with 1316 additions and 63 deletions

View File

@@ -2,6 +2,18 @@
***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.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08)
## What's Changed
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151
* fix: Re-add new equalizer settings that got lost by @jaime-grj in https://github.com/eddyizm/tempo/pull/153
* chore: removed play variant by @eddyizm in https://github.com/eddyizm/tempo/pull/155
* fix: updating release workflow to account for the 32/64 bit builds an… by @eddyizm in https://github.com/eddyizm/tempo/pull/156
* feat: Show sampling rate and bit depth in downloads by @jaime-grj in https://github.com/eddyizm/tempo/pull/154
* fix: Replace hardcoded strings in SettingsFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/152
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.0...v3.16.6
## [3.16.0](https://github.com/eddyizm/tempo/releases/tag/v3.16.0) (2025-10-07) ## [3.16.0](https://github.com/eddyizm/tempo/releases/tag/v3.16.0) (2025-10-07)
## What's Changed ## What's Changed
* chore: add sha256 fingerprint for validation by @eddyizm in https://github.com/eddyizm/tempo/commit/3c58e6fbb2157a804853259dfadbbffe3b6793b5 * chore: add sha256 fingerprint for validation by @eddyizm in https://github.com/eddyizm/tempo/commit/3c58e6fbb2157a804853259dfadbbffe3b6793b5

View File

@@ -29,6 +29,16 @@ sha256 signing key fingerprint
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.
### 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](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md) Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
Fork [**sponsorship here**](https://ko-fi.com/eddyizm). Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
@@ -50,12 +60,9 @@ Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
- **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.
## 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 +97,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 a UI change, please provide a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
Currently there are not tests but I would love to start on some unit tests.
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.

View File

@@ -60,7 +60,20 @@ This app works with any service that implements the Subsonic API, including:
**TODO** **TODO**
### 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

View File

@@ -10,8 +10,8 @@ android {
minSdkVersion 24 minSdkVersion 24
targetSdk 35 targetSdk 35
versionCode 34 versionCode 35
versionName '3.16.6' versionName '3.17.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions { javaCompileOptions {

View File

@@ -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

View File

@@ -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());
}
} }

View File

@@ -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);

View File

@@ -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
}
}
}

View File

@@ -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();

View File

@@ -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)) {
@@ -500,4 +553,4 @@ public class MainActivity extends BaseActivity {
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
} }
} }

View File

@@ -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;
});
}
} }

View File

@@ -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());
@@ -347,4 +375,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());
}
}

View File

@@ -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)) {

View File

@@ -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);
@@ -548,4 +665,4 @@ public class PlayerControllerFragment extends Fragment {
isServiceBound = false; isServiceBound = false;
} }
} }
} }

View File

@@ -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();
} }
@@ -293,4 +397,4 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private void refreshShares() { private void refreshShares() {
homeViewModel.refreshShares(requireActivity()); homeViewModel.refreshShares(requireActivity());
} }
} }

View File

@@ -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);
}
});
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
} }

View File

@@ -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())

View File

@@ -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);
} }
} }

View File

@@ -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;
@@ -34,7 +36,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 +51,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 +61,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 +75,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,6 +87,9 @@ 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(
@@ -93,7 +104,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 +116,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 +128,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 +144,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 +152,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 +187,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) {
} }

View 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>

View File

@@ -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"
@@ -209,4 +217,4 @@
android:text="@string/song_bottom_sheet_share" android:text="@string/song_bottom_sheet_share"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -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"
@@ -400,4 +411,4 @@
app:srcCompat="@drawable/ic_eq" /> app:srcCompat="@drawable/ic_eq" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View 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>

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
<resources>
<item name="tag_link_original_color" type="id" />
</resources>

View File

@@ -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>

View File

@@ -20,6 +20,7 @@ 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.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
@@ -421,7 +422,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 +442,10 @@ class MediaService : MediaLibraryService() {
player.shuffleModeEnabled, player.shuffleModeEnabled,
player.repeatMode, player.repeatMode,
position, position,
duration duration,
songLink,
albumLink,
artistLink
) )
} }

View File

@@ -22,6 +22,7 @@ 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.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
@@ -262,7 +263,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 +283,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.shuffleModeEnabled, player.shuffleModeEnabled,
player.repeatMode, player.repeatMode,
position, position,
duration duration,
songLink,
albumLink,
artistLink
) )
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB