Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
611b5001be | ||
|
|
923cfd5bc9 | ||
|
|
36d2320e70 | ||
|
|
17713ee400 | ||
|
|
ee3465868e | ||
|
|
9e8141a8d9 | ||
|
|
043e1b39b0 | ||
|
|
938c1de906 | ||
|
|
a0dfe63660 | ||
|
|
28c2f87b26 | ||
|
|
9ab22bfede | ||
|
|
20900fb557 | ||
|
|
7457c5b6e3 | ||
|
|
e5a928ec0f | ||
|
|
147c8360a6 | ||
|
|
d5d504fc64 | ||
|
|
24eead2d0a | ||
|
|
2644fa52b6 | ||
|
|
38c144c073 | ||
|
|
1dca1ef68d | ||
|
|
ba94d7e5cc | ||
|
|
0028872e3f | ||
|
|
be9eec625a | ||
|
|
b335ddec01 | ||
|
|
eb5c4721d1 | ||
|
|
b0e8fa75ca | ||
|
|
27d7288ee9 | ||
|
|
287921de09 | ||
|
|
e5cb8793b0 | ||
|
|
f25e7f250a | ||
|
|
911acc3c2d | ||
|
|
4b7f60bb8c | ||
|
|
d35146dba3 | ||
|
|
6c3897a400 | ||
|
|
1002499d92 | ||
|
|
77c0b86dac | ||
|
|
0abdfc6b19 | ||
|
|
52b2ca8fa7 | ||
|
|
7f66124614 | ||
|
|
9930537486 | ||
|
|
5d51132921 | ||
|
|
4b2e963a81 | ||
|
|
4c865e199d | ||
|
|
fc58869354 | ||
|
|
5e1a2b41e9 | ||
|
|
77bdd71d79 |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -26,6 +26,7 @@ Outline the steps required to reproduce the bug, including any specific actions,
|
||||
- Android device: [Device Model]
|
||||
- Android OS version: [Android Version]
|
||||
- App version: [App Version]
|
||||
- App variant: [goole play services, degoogled]
|
||||
- Other relevant details: [e.g., specific network conditions, external dependencies]
|
||||
|
||||
## Logs or Screenshots
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## Pending release..
|
||||
* fix: reverts change causing album disc/track list to get out of order by @eddyizm in https://github.com/eddyizm/tempus/pull/237
|
||||
* fix: Add listener to enable equalizer when audioSessionId changes by @jaime-grj in https://github.com/eddyizm/tempus/pull/235
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.0...v4.1.2
|
||||
|
||||
## [4.1.0](https://github.com/eddyizm/tempo/releases/tag/v4.1.0) (2025-11-05)
|
||||
## What's Changed
|
||||
* chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205
|
||||
* shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207
|
||||
* Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220
|
||||
* feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206
|
||||
* Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210
|
||||
* fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216
|
||||
* fix: playlist page should not snap by @pca006132 in https://github.com/eddyizm/tempus/pull/218
|
||||
* fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221
|
||||
* chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217
|
||||
* fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222
|
||||
* fix: skip mapping downloaded item by @pca006132 in https://github.com/eddyizm/tempus/pull/228
|
||||
|
||||
## New Contributors
|
||||
* @pca006132 made their first contribution in https://github.com/eddyizm/tempus/pull/207
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.7...v4.1.0
|
||||
|
||||
## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28)
|
||||
## What's Changed
|
||||
* chore: updated tempo references to tempus including github check by @eddyizm in https://github.com/eddyizm/tempus/pull/197
|
||||
|
||||
@@ -47,9 +47,6 @@ Please note the two variants in the release assets include release/debug and 32/
|
||||
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
[Documentation](USAGE.md) (work in progress)
|
||||
@@ -114,6 +111,11 @@ Currently there are no tests but I would love to start on some unit tests.
|
||||
|
||||
Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
|
||||
|
||||
## Support
|
||||
|
||||
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
|
||||
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
|
||||
|
||||
## License
|
||||
|
||||
Tempus 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.
|
||||
|
||||
5
USAGE.md
5
USAGE.md
@@ -78,6 +78,9 @@ On the main player control screen, tapping on the artwork will reveal a small co
|
||||
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
|
||||
* TBD: what is the _instant mix function_?
|
||||
* Uses [getSimilarSongs](https://opensubsonic.netlify.app/docs/endpoints/getsimilarsongs/) of OpenSubsonic API.
|
||||
Which tracks to be mixed depends on the server implementation. For example, Navidrome gets 15 similar artists from LastFM, then 20 top songs from each.
|
||||
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)
|
||||
|
||||
@@ -163,4 +166,4 @@ For additional help:
|
||||
|
||||
---
|
||||
|
||||
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*
|
||||
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
minSdkVersion 24
|
||||
targetSdk 35
|
||||
|
||||
versionCode 2
|
||||
versionName '4.0.7'
|
||||
versionCode 4
|
||||
versionName '4.1.3'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
@@ -110,12 +110,12 @@ dependencies {
|
||||
implementation 'com.github.bumptech.glide:annotations:4.16.0'
|
||||
|
||||
// Media3
|
||||
implementation 'androidx.media3:media3-session:1.5.1'
|
||||
implementation 'androidx.media3:media3-common:1.5.1'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.5.1'
|
||||
implementation 'androidx.media3:media3-ui:1.5.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
|
||||
tempusImplementation 'androidx.media3:media3-cast:1.5.1'
|
||||
implementation 'androidx.media3:media3-session:1.8.0'
|
||||
implementation 'androidx.media3:media3-common:1.8.0'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.8.0'
|
||||
implementation 'androidx.media3:media3-ui:1.8.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
|
||||
tempusImplementation 'androidx.media3:media3-cast:1.8.0'
|
||||
|
||||
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
|
||||
@@ -5,18 +5,20 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray
|
||||
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.R
|
||||
@@ -43,6 +45,7 @@ class MediaService : MediaLibraryService() {
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var shuffleCommands: List<CommandButton>
|
||||
private lateinit var repeatCommands: List<CommandButton>
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
private var customLayout = ImmutableList.of<CommandButton>()
|
||||
@@ -79,6 +82,39 @@ class MediaService : MediaLibraryService() {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
||||
"android.media3.session.demo.REPEAT_ALL"
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
|
||||
}
|
||||
|
||||
fun updateMediaItems() {
|
||||
Log.d("MediaService", "update items");
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post(Runnable {
|
||||
updateMediaItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -90,6 +126,7 @@ class MediaService : MediaLibraryService() {
|
||||
restorePlayerFromQueue()
|
||||
initializePlayerListener()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
|
||||
setPlayer(player)
|
||||
}
|
||||
@@ -99,6 +136,7 @@ class MediaService : MediaLibraryService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
@@ -246,16 +284,7 @@ class MediaService : MediaLibraryService() {
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = player.audioSessionId
|
||||
if (equalizerManager.attachToSession(audioSessionId)) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
}
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession() {
|
||||
@@ -275,6 +304,12 @@ class MediaService : MediaLibraryService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
|
||||
updateMediaItems()
|
||||
}
|
||||
|
||||
private fun restorePlayerFromQueue() {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
@@ -383,6 +418,10 @@ class MediaService : MediaLibraryService() {
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
|
||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
@@ -398,6 +437,10 @@ class MediaService : MediaLibraryService() {
|
||||
mediaLibrarySession.release()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
|
||||
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
@@ -494,6 +537,21 @@ class MediaService : MediaLibraryService() {
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
if (attached) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
|
||||
}
|
||||
return attached
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
@@ -312,24 +314,42 @@ public class ArtistRepository {
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getTopSongs(artist.getName(), count)
|
||||
.getArtist(artist.getId())
|
||||
.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().getTopSongs() != null && response.body().getSubsonicResponse().getTopSongs().getSongs() != null) {
|
||||
List<Child> songs = response.body().getSubsonicResponse().getTopSongs().getSongs();
|
||||
if (response.isSuccessful() && response.body() != null &&
|
||||
response.body().getSubsonicResponse().getArtist() != null &&
|
||||
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
|
||||
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
Collections.shuffle(songs);
|
||||
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
|
||||
Log.d("ArtistRepository", "Got albums directly: " + albums.size());
|
||||
if (albums.isEmpty()) {
|
||||
Log.d("ArtistRepository", "No albums found in artist response");
|
||||
return;
|
||||
}
|
||||
|
||||
randomSongs.setValue(songs);
|
||||
Collections.shuffle(albums);
|
||||
int[] counts = albums.stream().mapToInt(AlbumID3::getSongCount).toArray();
|
||||
Arrays.parallelPrefix(counts, Integer::sum);
|
||||
int albumLimit = 0;
|
||||
int multiplier = 4; // get more than the limit so we can shuffle them
|
||||
while (albumLimit < albums.size() && counts[albumLimit] < count * multiplier)
|
||||
albumLimit++;
|
||||
Log.d("ArtistRepository", String.format("Retaining %d/%d albums", albumLimit, albums.size()));
|
||||
|
||||
fetchAllAlbumSongsWithCallback(albums.stream().limit(albumLimit).collect(Collectors.toList()), songs -> {
|
||||
Collections.shuffle(songs);
|
||||
randomSongs.setValue(songs.stream().limit(count).collect(Collectors.toList()));
|
||||
});
|
||||
} else {
|
||||
Log.d("ArtistRepository", "Failed to get artist info");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
Log.d("ArtistRepository", "Error getting artist info: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -151,6 +151,9 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
|
||||
case Constants.ARTIST_ORDER_BY_RANDOM:
|
||||
Collections.shuffle(artists);
|
||||
break;
|
||||
case Constants.ARTIST_ORDER_BY_ALBUM_COUNT:
|
||||
artists.sort(Comparator.comparing(ArtistID3::getAlbumCount).reversed());
|
||||
break;
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
|
||||
@@ -114,7 +115,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
artistAdapter = new ArtistCatalogueAdapter(this);
|
||||
artistAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
|
||||
bind.artistCatalogueRecyclerView.setAdapter(artistAdapter);
|
||||
artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> artistAdapter.setItems(artistList));
|
||||
artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> {
|
||||
artistAdapter.setItems(artistList);
|
||||
artistAdapter.sort(Preferences.getArtistSortOrder());
|
||||
});
|
||||
|
||||
bind.artistCatalogueRecyclerView.setOnTouchListener((v, event) -> {
|
||||
hideKeyboard(v);
|
||||
@@ -192,6 +196,9 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
} else if (menuItem.getItemId() == R.id.menu_artist_sort_random) {
|
||||
artistAdapter.sort(Constants.ARTIST_ORDER_BY_RANDOM);
|
||||
return true;
|
||||
} else if (menuItem.getItemId() == R.id.menu_artist_sort_album_count) {
|
||||
artistAdapter.sort(Constants.ARTIST_ORDER_BY_ALBUM_COUNT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -188,8 +188,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
} else {
|
||||
if (bind != null)
|
||||
bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
if (bind != null)
|
||||
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
}
|
||||
|
||||
@@ -117,14 +117,12 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
if (songs.isEmpty()) {
|
||||
if (bind != null) {
|
||||
bind.emptyDownloadLayout.setVisibility(View.VISIBLE);
|
||||
bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE);
|
||||
bind.downloadDownloadedSector.setVisibility(View.GONE);
|
||||
bind.downloadedGroupByImageView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
if (bind != null) {
|
||||
bind.emptyDownloadLayout.setVisibility(View.GONE);
|
||||
bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE);
|
||||
bind.downloadDownloadedSector.setVisibility(View.VISIBLE);
|
||||
bind.downloadedGroupByImageView.setVisibility(View.VISIBLE);
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.cappielloantonio.tempo.ui.fragment
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.BroadcastReceiver
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
@@ -12,6 +14,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.cappielloantonio.tempo.R
|
||||
@@ -28,6 +31,17 @@ class EqualizerFragment : Fragment() {
|
||||
private lateinit var safeSpace: Space
|
||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||
|
||||
private var receiverRegistered = false
|
||||
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == MediaService.ACTION_EQUALIZER_UPDATED) {
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
@@ -49,12 +63,29 @@ class EqualizerFragment : Fragment() {
|
||||
intent.action = MediaService.ACTION_BIND_EQUALIZER
|
||||
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
if (!receiverRegistered) {
|
||||
ContextCompat.registerReceiver(
|
||||
requireContext(),
|
||||
equalizerUpdatedReceiver,
|
||||
IntentFilter(MediaService.ACTION_EQUALIZER_UPDATED),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
receiverRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
requireActivity().unbindService(connection)
|
||||
equalizerManager = null
|
||||
if (receiverRegistered) {
|
||||
try {
|
||||
requireContext().unregisterReceiver(equalizerUpdatedReceiver)
|
||||
} catch (_: Exception) {
|
||||
// ignore if not registered
|
||||
}
|
||||
receiverRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -234,4 +265,4 @@ class EqualizerFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(context: Context): Int =
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
|
||||
@@ -89,6 +89,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
ArtistRepository artistRepository = new ArtistRepository();
|
||||
|
||||
artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
|
||||
// navidrome may return null for this
|
||||
if (songs == null)
|
||||
return;
|
||||
MusicUtil.ratingFilter(songs);
|
||||
|
||||
if (!songs.isEmpty()) {
|
||||
|
||||
@@ -40,6 +40,7 @@ object Constants {
|
||||
const val ARTIST_STARRED = "ARTIST_STARRED"
|
||||
const val ARTIST_ORDER_BY_NAME = "ARTIST_ORDER_BY_NAME"
|
||||
const val ARTIST_ORDER_BY_RANDOM = "ARTIST_ORDER_BY_RANDOM"
|
||||
const val ARTIST_ORDER_BY_ALBUM_COUNT = "ARTIST_ORDER_BY_ALBUM_COUNT"
|
||||
const val ARTIST_ORDER_BY_MOST_RECENTLY_STARRED = "ARTIST_ORDER_BY_MOST_RECENTLY_STARRED"
|
||||
const val ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED = "ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED"
|
||||
|
||||
|
||||
@@ -115,6 +115,29 @@ public class MappingUtil {
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MediaItem mapMediaItem(MediaItem old) {
|
||||
String mediaId = null;
|
||||
if (old.requestMetadata.extras != null)
|
||||
mediaId = old.requestMetadata.extras.getString("id");
|
||||
|
||||
if (mediaId != null && DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(mediaId)) {
|
||||
return old;
|
||||
}
|
||||
Uri uri = old.requestMetadata.mediaUri == null ? null : MusicUtil.updateStreamUri(old.requestMetadata.mediaUri);
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(old.mediaId)
|
||||
.setMediaMetadata(old.mediaMetadata)
|
||||
.setRequestMetadata(
|
||||
new MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setExtras(old.requestMetadata.extras)
|
||||
.build()
|
||||
)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.setUri(uri)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<MediaItem> mapDownloads(List<Child> items) {
|
||||
ArrayList<MediaItem> downloads = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -21,11 +21,16 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MusicUtil {
|
||||
private static final String TAG = "MusicUtil";
|
||||
|
||||
private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+");
|
||||
|
||||
public static Uri getStreamUri(String id) {
|
||||
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
|
||||
|
||||
@@ -61,6 +66,24 @@ public class MusicUtil {
|
||||
return Uri.parse(uri.toString());
|
||||
}
|
||||
|
||||
public static Uri updateStreamUri(Uri uri) {
|
||||
String s = uri.toString();
|
||||
Matcher m1 = BITRATE_PATTERN.matcher(s);
|
||||
s = m1.replaceAll("");
|
||||
Matcher m2 = FORMAT_PATTERN.matcher(s);
|
||||
s = m2.replaceAll("");
|
||||
s = s.replace("&estimateContentLength=true", "");
|
||||
|
||||
if (!Preferences.isServerPrioritized())
|
||||
s += "&maxBitRate=" + getBitratePreference();
|
||||
if (!Preferences.isServerPrioritized())
|
||||
s += "&format=" + getTranscodingFormatPreference();
|
||||
if (Preferences.askForEstimateContentLength())
|
||||
s += "&estimateContentLength=true";
|
||||
|
||||
return Uri.parse(s);
|
||||
}
|
||||
|
||||
public static Uri getDownloadUri(String id) {
|
||||
StringBuilder uri = new StringBuilder();
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ object Preferences {
|
||||
private const val ALBUM_DETAIL = "album_detail"
|
||||
private const val ALBUM_SORT_ORDER = "album_sort_order"
|
||||
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
|
||||
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
|
||||
|
||||
@JvmStatic
|
||||
fun getServer(): String? {
|
||||
@@ -656,4 +657,14 @@ object Preferences {
|
||||
fun setAlbumSortOrder(sortOrder: String) {
|
||||
App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getArtistSortOrder(): String {
|
||||
val sort_by_album_count = App.getInstance().preferences.getBoolean(ARTIST_SORT_BY_ALBUM_COUNT, false)
|
||||
Log.d("Preferences", "getSortOrder")
|
||||
if (sort_by_album_count)
|
||||
return Constants.ARTIST_ORDER_BY_ALBUM_COUNT
|
||||
else
|
||||
return Constants.ARTIST_ORDER_BY_NAME
|
||||
}
|
||||
}
|
||||
@@ -14,22 +14,21 @@
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_arrow_back" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/fragment_album_page_nested_scroll_view"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_height="match_parent">
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/album_info_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:paddingTop="8dp">
|
||||
android:paddingTop="8dp"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_cover_image_view"
|
||||
@@ -252,53 +251,15 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_page_button_layout" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/global_padding_bottom"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/song_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingTop="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/similar_album_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="32dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/album_page_extra_info_button" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/similar_albums_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/song_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="@dimen/global_padding_bottom"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/toolbar_fragment"
|
||||
@@ -26,6 +27,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/global_padding_bottom"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
@@ -57,92 +59,78 @@
|
||||
android:text="@string/download_info_empty_subtitle" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/fragment_download_nested_scroll_view"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_downloaded_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_downloaded_sector"
|
||||
<TextView
|
||||
android:id="@+id/downloaded_text_view_refreshable"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/download_title_section"
|
||||
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shuffle_downloaded_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="@dimen/global_padding_bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
android:text="@string/download_shuffle_all_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloaded_text_view_refreshable"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/download_title_section"
|
||||
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<ImageView
|
||||
android:id="@+id/downloaded_refresh_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/ic_refresh"
|
||||
android:contentDescription="@string/download_refresh_button_content_description"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
|
||||
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shuffle_downloaded_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/download_shuffle_all_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
||||
<ImageView
|
||||
android:id="@+id/downloaded_go_back_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:background="@drawable/ic_arrow_back"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
||||
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
|
||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/downloaded_refresh_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/ic_refresh"
|
||||
android:contentDescription="@string/download_refresh_button_content_description"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
|
||||
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||
<ImageView
|
||||
android:id="@+id/downloaded_group_by_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_filter_list"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/downloaded_go_back_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_arrow_back"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
||||
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
|
||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/downloaded_group_by_image_view"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_filter_list"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/downloaded_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shuffle_downloaded_text_view_clickable" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/downloaded_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="@dimen/global_padding_bottom" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playlist_cover_image_view_top_left"
|
||||
|
||||
@@ -6,4 +6,7 @@
|
||||
<item
|
||||
android:id="@+id/menu_artist_sort_random"
|
||||
android:title="@string/menu_sort_random" />
|
||||
<item
|
||||
android:id="@+id/menu_artist_sort_album_count"
|
||||
android:title="@string/menu_sort_album_count" />
|
||||
</menu>
|
||||
@@ -413,7 +413,9 @@
|
||||
<string name="share_bottom_sheet_delete">Eliminar compartición</string>
|
||||
<string name="share_bottom_sheet_update">Actualizar compartición</string>
|
||||
<string name="share_subtitle_item">Fecha de caducidad: %1$s</string>
|
||||
<string name="share_no_expiration">Nunca</string>
|
||||
<string name="share_unsupported_error">El uso compartido no está soportado o no está habilitado</string>
|
||||
<string name="asset_link_debug_toast">Enlace de recurso: %1$s</string>
|
||||
<string name="share_update_dialog_hint_description">Descripción</string>
|
||||
<string name="share_update_dialog_hint_expiration_date">Fecha de caducidad</string>
|
||||
<string name="song_bottom_sheet_add_to_queue">Añadir a la cola</string>
|
||||
@@ -486,4 +488,21 @@
|
||||
<string name="settings_sync_starred_artists_for_offline_use_summary">Si está habilitada, los artistas destacados se descargarán para uso sin conexión.</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="exo_controls_heart_off_description">Eliminar de favoritos</string>
|
||||
<string name="asset_link_chip_text">%1$s • %2$s</string>
|
||||
<string name="asset_link_copied_toast">Copiado %1$s al portapapeles</string>
|
||||
<string name="settings_album_detail">Mostrar los detalles del álbum</string>
|
||||
<string name="settings_album_detail_summary">Si está habilitada, muestra los detalles del álbum, como el género, el número de pistas, etc. en la página de álbum</string>
|
||||
<string name="asset_link_clipboard_label">Enlace de recurso de Tempus</string>
|
||||
<string name="asset_link_label_song">UID de la pista</string>
|
||||
<string name="asset_link_label_album">UID del álbum</string>
|
||||
<string name="asset_link_label_artist">UID del artista</string>
|
||||
<string name="asset_link_label_playlist">UID de la lista de reproducción</string>
|
||||
<string name="asset_link_label_genre">UID del género</string>
|
||||
<string name="asset_link_label_year">UID del año</string>
|
||||
<string name="asset_link_label_unknown">UID del recurso</string>
|
||||
<string name="asset_link_error_unsupported">Enlace de recurso no válido</string>
|
||||
<string name="asset_link_error_song">No se ha podido abrir la pista</string>
|
||||
<string name="asset_link_error_album">No se ha podido abrir el álbum</string>
|
||||
<string name="asset_link_error_artist">No se ha podido abrir el artista</string>
|
||||
<string name="asset_link_error_playlist">No se ha podido abrir la lista de reproducción</string>
|
||||
</resources>
|
||||
@@ -200,6 +200,7 @@
|
||||
<string name="menu_sort_artist">Artist</string>
|
||||
<string name="menu_sort_name">Name</string>
|
||||
<string name="menu_sort_random">Random</string>
|
||||
<string name="menu_sort_album_count">Album Count</string>
|
||||
<string name="menu_sort_recently_added">Recently added</string>
|
||||
<string name="menu_sort_recently_played">Recently played</string>
|
||||
<string name="menu_sort_most_played">Most played</string>
|
||||
@@ -527,4 +528,6 @@
|
||||
|
||||
<string name="settings_album_detail">Show album detail</string>
|
||||
<string name="settings_album_detail_summary">If enabled, show the album details like genre, song count etc. on the album page</string>
|
||||
<string name="settings_artist_sort_by_album_count">Sort artists by album count</string>
|
||||
<string name="settings_artist_sort_by_album_count_summary">If enabled, sort the artists by album count. Sort by name if disabled.</string>
|
||||
</resources>
|
||||
|
||||
@@ -116,6 +116,12 @@
|
||||
android:summary="@string/settings_album_detail_summary"
|
||||
android:key="album_detail" />
|
||||
|
||||
<SwitchPreference
|
||||
android:title="@string/settings_artist_sort_by_album_count"
|
||||
android:defaultValue="false"
|
||||
android:summary="@string/settings_artist_sort_by_album_count_summary"
|
||||
android:key="artist_sort_by_album_count" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/settings_title_playlist">
|
||||
|
||||
@@ -4,10 +4,14 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
@@ -43,6 +47,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
private lateinit var castPlayer: CastPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var librarySessionCallback: MediaLibrarySessionCallback
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
@@ -55,6 +60,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
|
||||
companion object {
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
|
||||
}
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
@@ -69,6 +75,38 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMediaItems() {
|
||||
Log.d("MediaService", "update items");
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post(Runnable {
|
||||
updateMediaItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -79,6 +117,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
initializePlayerListener()
|
||||
initializeCastPlayer()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
|
||||
setPlayer(
|
||||
null,
|
||||
@@ -99,6 +138,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
@@ -178,6 +218,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
|
||||
updateMediaItems()
|
||||
}
|
||||
|
||||
private fun restorePlayerFromQueue() {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
@@ -374,6 +420,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
automotiveRepository.deleteMetadata()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
override fun onCastSessionAvailable() {
|
||||
|
||||
10
fastlane/metadata/android/en-US/changelogs/3.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/3.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Update Spanish (es-ES) translation
|
||||
Shuffle for artists without using `getTopSongs`
|
||||
Update USAGE.md with instant mix detils
|
||||
Sort artists by album count
|
||||
Fix downloaded tab performance
|
||||
Remove NestedScrollViews for fragment_album_page
|
||||
Playlist page should not snap
|
||||
Do not override getItemViewType and getItemId
|
||||
Update media3 dependencies
|
||||
Update MediaItems after network change
|
||||
2
fastlane/metadata/android/en-US/changelogs/4.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/4.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
reverts change causing album disc/track list to get out of order
|
||||
Add listener to enable equalizer when audioSessionId
|
||||
Reference in New Issue
Block a user