52 Commits

Author SHA1 Message Date
eddyizm
21ed78d959 chore: bumping version, fastlane and changelog 2026-02-08 16:14:22 -08:00
Tom
5ad99b9f27 feat: increase items per row on landscape view (#411)
* feat: increase items per row on landscape view

This covers the catalogues: artist, album and genre; also the list of albums on artist view.
This was implemented by adierebel/tempo fork, I only cherry-picked some commits.

Co-authored-by: adierebel <adie.rebel@gmail.com>

* feat: add landscape layout to song listing views

This includes the playlist page and the album page.

* fix: bad scaling on small screens

This rollbacks to the original code by adierebel/tempo fork

* fix: remove hardcoded height blocking scroll

This was addressed in 989ca35, forgot to fix it here as well

* fix: wrap content height rather than inheriting it from parent

* feat: add ui of choice selector in setting for items per row

* feat: link getter to landscapes items per row setting an implement it

* fix: wrong default value

Co-authored-by: eddyizm <wtfisup@hotmail.com>

* feat: add default value on setting string

To introduce the new feature of landscape layouts.

Co-authored-by: eddyizm <wtfisup@hotmail.com>

---------

Co-authored-by: adierebel <adie.rebel@gmail.com>
Co-authored-by: eddyizm <wtfisup@hotmail.com>
Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-02-08 15:20:53 -08:00
T R
3de5390140 fix: album art now displays on android auto (#414)
Co-authored-by: Thomas R <tdr@thomasr.co>
Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-02-08 10:34:44 -08:00
eddyizm
d215581e19 fix: keep observer until data is received on continuous play (#421) 2026-02-08 10:18:36 -08:00
tiltshiftfocus
54612c6b74 patch: Addressing some UI/UX quirks (#413)
* beautify lyrics display

* use dialog to select playback speed

to prevent accidental clicks
2026-02-08 10:18:01 -08:00
eddyizm
eaac728a26 chore: bump version and change logs 2026-02-02 20:25:40 -08:00
skajmer
65d2f8e33f chore(i18n): Update Polish translation (#402)
* Add #338

* Add #3700 (strings.xml)

* Add #370 (arrays.xml)

* Add #386

* Add #394
2026-02-02 07:09:29 -08:00
Tom
baf4e0f0fc chore(i18n): set links as untranslatable (#400) 2026-01-31 17:37:41 -08:00
Tom
26c7bee106 feat: Add selector for playlist visibility (#394)
* feat: add selector for playlist visiblity when adding a song

* fix: wrong number of arguments

* feat: make dialog text localized

* chore: add es, fr, it, pt localization for playlist visibility dialog

---------

Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-01-31 17:10:58 -08:00
Pascal Grittmann
6e51611867 Improve Synced Lyrics (#384)
* feature: click on synced lyrics to navigate in song

* only update lyrics if needed

improves performance and allows user to scroll synced lyrics

* fix: don't scroll to start after end of song
2026-01-31 08:16:13 -08:00
eddyizm
d67e432731 chore: added playlist strings for pr #394 2026-01-31 08:10:30 -08:00
Pascal Grittmann
8b61396b0f Fix missing Replay Gain metadata from .m4a files (#396)
fix missing replay gain metadata from .m4a files
2026-01-29 20:22:09 -08:00
eddyizm
0fb6e55b12 chore: update changelog and fastlane 2026-01-26 21:32:07 -08:00
eddyizm
dd7aa2291b chore: bump version 2026-01-26 21:29:40 -08:00
eddyizm
ec33c32c89 fix: updated dialog import to address crashing on android 15 (#392)
resolves #362
2026-01-26 21:25:27 -08:00
Jaime García
e0ad4e3701 fix: Avoid crash when server has no songs (#389) 2026-01-26 16:24:23 -08:00
eddyizm
253f8033c5 Merge branch 'development' 2026-01-25 11:41:50 -08:00
eddyizm
c1aed1a4c1 chore: version/changelog/fastlane bumps 2026-01-25 11:41:26 -08:00
eddyizm
23f58439ba Merge branch 'development' 2026-01-25 11:34:27 -08:00
eddyizm
4c99ced597 chore: version/changelog/fastlane bumps 2026-01-25 11:34:16 -08:00
eddyizm
8d215a7f1c feat: add configurable timeout (#386) 2026-01-25 11:27:12 -08:00
eddyizm
38fc4a0936 chore: forget to check in fastlane change log 2026-01-25 08:06:29 -08:00
eddyizm
d9949349da chore: forget to check in fastlane change log 2026-01-25 08:06:06 -08:00
Jaime García
877d29d285 chore(i18n): Update Spanish translation (#381) 2026-01-24 14:05:52 -08:00
Jaime García
9a17aa8b98 fix: Proper raw stream detection (#382) 2026-01-24 14:05:36 -08:00
eddyizm
fd41395ab8 chore: bump version for tag 2026-01-24 09:11:41 -08:00
eddyizm
269066e036 Merge branch 'development' 2026-01-24 09:09:23 -08:00
eddyizm
488460ea9d chore: bump version/changelog 2026-01-24 09:07:26 -08:00
Pascal Grittmann
d16a9c234f feat: Playback speed controls for music (#376)
Enable playback speed controls for music

Button is moved to the top left, next to bit rate, because it would
overlap with the "shuffle" button.
The speed rotation logic was cleaned up to 0.25x increments without all
the hard-coded constants and code duplication.

Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-01-24 08:39:57 -08:00
eddyizm
07b507691c chore: fixed read me donate link 2026-01-24 08:33:34 -08:00
drakeerv
bde34d3df0 feat: Implement duration and seeking for transcodes (#358)
Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-01-24 08:28:47 -08:00
Pascal Grittmann
e5b7756f96 fix: Check for OpenSubsonic extensions also with password authentication (#375)
check for OpenSubsonic extensions also with pwd auth

Co-authored-by: eddyizm <eddyizm@gmail.com>
2026-01-24 08:28:20 -08:00
eddyizm
04e692e5e9 chore: fixed read me donate link 2026-01-24 07:51:30 -08:00
skajmer
a23a663d32 chore(i18n): Update Polish translation (#374)
* Add #338

* Add #3700 (strings.xml)

* Add #370 (arrays.xml)
2026-01-23 22:15:51 -08:00
eddyizm
023bd8071a fix: use existing future when adding tracks, dialed random album trac… (#373)
fix: use existing future when adding tracks, dialed random album tracks back to 100, vs 1000.
2026-01-22 22:09:49 -08:00
eddyizm
72b1517f61 chore: added openapk badge, contributor shoutout, another link fix 2026-01-22 22:08:52 -08:00
eddyizm
e62ea72c2f merge-conflit 2026-01-21 07:54:51 -08:00
eddyizm
a24ccf2556 chore: added attribute 2026-01-21 07:53:08 -08:00
eddyizm
49838e2e0f chore: another attempt at fixing the links 2026-01-21 07:46:02 -08:00
eddyizm
8ed1248ee1 docs: fixed main links for github page 2026-01-21 07:21:24 -08:00
eddyizm
e9d54957ae chore: pending release notes update and href links on readme 2026-01-20 21:59:48 -08:00
eddyizm
3cd5843c4b feat: sort preference for playlists (#370) 2026-01-18 16:39:28 -08:00
eddyizm
75513d3bd4 fix: sort playlist catalog view (#368)
fix: sorts playlist catalog view
2026-01-18 08:54:25 -08:00
eddyizm
c0c84269ef fix: toast for made for you click indication (#365) 2026-01-17 18:21:14 -08:00
eddyizm
fa2e029f9f Merge branch 'main' into development 2026-01-17 18:18:32 -08:00
eddyizm
f1bfb095b7 chore: updated readme and added known issues for airsonic work around (#366) 2026-01-17 18:17:23 -08:00
Jaime García
4328415efc chore(i18n): Update Spanish translation (#364) 2026-01-17 08:43:24 -08:00
Benoît Smith
092ae14ea2 chore: French localization update (#356)
* Add toast message for no artist info

* Add French strings for instant mix generation messages

* Add French string for music download directory

* Add neutral button string for download storage dialog

* Add French strings for download refresh features

* Add French translations for heart controls and loading

* Update French strings for starred albums and artists

* Add album count string to French resources

* Add French translations for player lyrics features

* Update French strings for pluralization and playlist

* Fix French translation for podcast info title

* Add and update French radio station strings

* Add settings for playlist duplicates in French strings

* Add download folder settings in French strings

* Add download folder settings and update equalizer summary

* Add support discussion and update strings in French

* Update French strings for UI elements

Updated French translations for heart control descriptions, mobile bitrate settings, queue syncing title, and added mini shuffle button settings.

* Update French strings for settings and lyrics

* Update French strings for offline sync settings

* Add playlist string to French resources

* Add French translations for asset links

* Revise French subtitles for starred artists and albums

Updated subtitles for starred artists and albums in French localization.

* Add French strings for widget and settings
2026-01-14 21:44:36 -08:00
eddyizm
26af8a692f Merge branch 'main' into development 2026-01-14 21:27:18 -08:00
eddyizm
b870f4c866 fix: added country code to catalan 2026-01-14 21:27:14 -08:00
DevMatei
cf4e78eafc i18n: Add Romanian translation (including locale_config this time!) (#357)
* i18n: Add Romanian (ro) translation

* feat(i18n): add Romanian (ro) locale support

* fix: added Country code

---------

Co-authored-by: eddyizm <wtfisup@hotmail.com>
2026-01-14 21:25:45 -08:00
eddyizm
83e23c44d9 chore: updated instant mix verbage 2026-01-14 21:17:52 -08:00
61 changed files with 1924 additions and 268 deletions

View File

@@ -1,5 +1,66 @@
# Changelog
## Pending release
## What's Changed
* fix: Addressing some UI/UX quirks by @tiltshiftfocus in https://github.com/eddyizm/tempus/pull/413
* fix: keep observer until data is received on continuousPlay bug by @eddyizm in https://github.com/eddyizm/tempus/pull/421
* fix: album art now displays on android auto by @trobinson in https://github.com/eddyizm/tempus/pull/414
* feat: improve landscape view and increase items per row on landscape view by @tvillega in https://github.com/eddyizm/tempus/pull/411
## New Contributors
* @tiltshiftfocus made their first contribution in https://github.com/eddyizm/tempus/pull/413
* @trobinson made their first contribution in https://github.com/eddyizm/tempus/pull/414
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.8...v4.10.0
## What's Changed
## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02)
* fix: missing Replay Gain metadata from .m4a files by @pgrit in https://github.com/eddyizm/tempus/pull/396
* fix: Improve Synced Lyrics by @pgrit in https://github.com/eddyizm/tempus/pull/384
* fix: Add selector for playlist visibility by @tvillega in https://github.com/eddyizm/tempus/pull/394
* chore(i18n): set links as untranslatable by @tvillega in https://github.com/eddyizm/tempus/pull/400
## New Contributors
* @tvillega made their first contribution in https://github.com/eddyizm/tempus/pull/394
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.5...v4.5.8
## What's Changed
## [4.9.5](https://github.com/eddyizm/tempo/releases/tag/v4.9.5) (2026-01-26)
* fix: Avoid crash when server has no songs by @jaime-grj in https://github.com/eddyizm/tempus/pull/389
* fix: updated dialog import to address crashing on android 15 by @eddyizm in https://github.com/eddyizm/tempus/pull/392
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.3...v4.9.5
## What's Changed
## [4.9.3](https://github.com/eddyizm/tempo/releases/tag/v4.9.3) (2026-01-25)
* fix: Proper raw stream detection by @jaime-grj in https://github.com/eddyizm/tempus/pull/382
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/381
* feat: add configurable timeout by @eddyizm in https://github.com/eddyizm/tempus/pull/386
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.1...v4.9.3
## What's Changed
## [4.9.1](https://github.com/eddyizm/tempo/releases/tag/v4.9.1) (2026-01-24)
* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357
* French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364
* docs: updated readme and added known issues for airsonic work around by @eddyizm in https://github.com/eddyizm/tempus/pull/366
* fix: toast for made for you click indication by @eddyizm in https://github.com/eddyizm/tempus/pull/365
* fix: sort playlist view by @eddyizm in https://github.com/eddyizm/tempus/pull/368
* feat: sort preference for playlists by @eddyizm in https://github.com/eddyizm/tempus/pull/370
* fix: use existing future when adding tracks, dialed random album tracks off in instant mix by @eddyizm in https://github.com/eddyizm/tempus/pull/373
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/374
* fix: Check for OpenSubsonic extensions also with password authentication by @pgrit in https://github.com/eddyizm/tempus/pull/375
* feat: Implement duration and seeking for transcodes by @drakeerv in https://github.com/eddyizm/tempus/pull/358
* feat: Playback speed controls for music by @pgrit in https://github.com/eddyizm/tempus/pull/376
## New Contributors
* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.1
## What's Changed
## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13)
* fix: instant mix random songs and broken continuous play by @eddyizm in https://github.com/eddyizm/tempus/pull/354

View File

@@ -10,15 +10,21 @@
<div align="center">
<a href="https://github.com/eddyizm/tempus/releases/">
<img alt="Releases" src="https://img.shields.io/github/downloads/eddyizm/tempus/total.svg?color=4B95DE&style=flat">
</a>
<!-- Reproducible build -->
<a href="https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus"><img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0">
<img src="https://img.shields.io/badge/license-GPL%20v3-2B6DBE.svg?style=flat">
</a>
</div>
<p align="center">
<a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.eddyizm.tempus%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Feddyizm%2Ftempus%22%2C%22author%22%3A%22eddyizm%22%2C%22name%22%3A%22Tempus%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22tempus%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22overrideSource%22%3A%22GitHub%22%7D"><img width="200" src="https://github.com/user-attachments/assets/119e7ff4-2636-43cb-ab7f-1b6a58ac3570" /></a>
<a href="https://www.openapk.net/tempus/com.eddyizm.degoogled.tempus/"><img src="https://camo.githubusercontent.com/cd56895b28a73ebd781a65b4f567add5419e45797a5cf1485ce408e851c2318e/68747470733a2f2f7777772e6f70656e61706b2e6e65742f696d616765732f6f70656e61706b2d62616467652e706e67" width="200"></a>
</p>
<!--
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
@@ -31,6 +37,10 @@ Tempus does not rely on magic algorithms to decide what you should listen to. In
The project is a fork of [Tempo](#credits).
[Changelog](CHANGELOG.md)
[Wiki](USAGE.md)
[Donate](https://github.com/eddyizm/tempus#donate)
**If you find Tempus useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
**Use the Github version of the app for full Android Auto and Chromecast support.**
@@ -46,11 +56,6 @@ Please note the two variants in the release assets include release/debug and 32/
`app-degoogled*` <- The izzyOnDroid release that goes without any of the google stuff. It is now available on izzyOnDroid (64bit) I am releasing the both 32/64bit apk's here on github for those who need a 32bit version.
[CHANGELOG.md](CHANGELOG.md)
## Usage
[Documentation](USAGE.md) (work in progress)
## Features
- **Subsonic Integration**: Tempus seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
@@ -62,7 +67,7 @@ Please note the two variants in the release assets include release/debug and 32/
- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.*
- **Scrobbling Integration**: Optionally integrate Tempus with Last.fm or Listenbrainz.org to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempus, expanding your audio entertainment options.
- **Instant Mix**: Full refactor of instant mix function which leverages subsonics similar songs by artist/album and randomSongs endpoints to server a larger play queue more reliably.
- **Instant Mix**: Full refactor of instant mix function which leverages subsonics similarSongs2 by artist/album and similarSongs endpoints to server a larger play queue more reliably.
- **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.*
- **Multiple Libraries**: Tempus handles multi-library setups gracefully. They are displayed as Library folders.
@@ -115,7 +120,10 @@ 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
*Special Thanks*
All the amazing [contributors](https://github.com/eddyizm/tempus/graphs/contributors)❤️
## Donate
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
@@ -124,6 +132,7 @@ bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
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.
## Credits
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)

View File

@@ -6,14 +6,13 @@
- [Getting Started](#getting-started)
- [Server Configuration](#server-configuration)
- [Main Features](#main-features)
- [Navigation](#navigation)
- [Playback Controls](#playback-controls)
- [Favorites](#favorites)
- [Playlist Management](#playlist-management)
- [Android Auto](#android-auto)
- [Settings](#settings)
- [Troubleshooting](#troubleshooting)
- [Known Issues](#known-issues)
## Prerequisites
@@ -193,15 +192,12 @@ To allow the Tempus app on your car's head unit, "Unknown sources" needs to be e
### Appearance
**TODO**
## Troubleshooting
## Known Issues
### Connection Issues
### Airsonic Distorted Playback
**TODO**
### Common Issues
**TODO**
First reported in issue [#226](https://github.com/eddyizm/tempus/issues/226)
The work around is to disable the cache in the settings, (set to 0), and if needed, cleaning the (Android) cache fixes the problem.
### Support
For additional help:

View File

@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
versionCode 13
versionName '4.6.4'
versionCode 18
versionName '4.10.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {

View File

@@ -96,7 +96,12 @@
android:resource="@xml/widget_info"/>
</receiver>
<provider
android:name=".provider.AlbumArtContentProvider"
android:authorities="com.cappielloantonio.tempo.provider"
android:enabled="true"
android:exported="true"
/>
</application>
</manifest>

View File

@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.model
import android.content.ContentResolver
import android.net.Uri
import android.os.Bundle
import androidx.annotation.Keep
@@ -13,6 +14,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.glide.CustomGlideRequest
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider
import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
@@ -197,7 +199,7 @@ class SessionMediaItem() {
fun getMediaItem(): MediaItem {
val uri: Uri = getStreamUri()
val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize()))
val artworkUri = AlbumArtContentProvider.contentUri(coverArtId)
val bundle = Bundle()
bundle.putString("id", id)

View File

@@ -0,0 +1,149 @@
package com.cappielloantonio.tempo.provider;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.util.Preferences;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class AlbumArtContentProvider extends ContentProvider {
public static final String AUTHORITY = "com.cappielloantonio.tempo.provider";
public static final String ALBUM_ART = "albumArt";
private ExecutorService executor;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, "albumArt/*", 1);
}
public static Uri contentUri(String artworkId) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(ALBUM_ART)
.appendPath(artworkId)
.build();
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
Context context = getContext();
String albumId = uri.getLastPathSegment();
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
try {
// use pipe to communicate between background thread and caller of openFile()
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
// perform loading in background thread to avoid blocking UI
executor.execute(() -> {
try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
// request artwork from API using Glide
File file = Glide.with(context)
.asFile()
.load(artworkUri)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.submit()
.get();
// copy artwork down pipe returned by ContentProvider
try (InputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
writeSide.closeWithError("Failed to load image: " + e.getMessage());
}
} catch (Exception e) {
try {
writeSide.closeWithError("Failed to load image: " + e.getMessage());
} catch (IOException ignored) {}
}
});
return readSide;
} catch (IOException e) {
throw new FileNotFoundException("Could not create pipe: " + e.getMessage());
}
}
@Override
public boolean onCreate() {
executor = Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors() / 2)
);
return true;
}
@Override
public void shutdown() {
if (executor != null) {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return "";
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}

View File

@@ -1,6 +1,7 @@
package com.cappielloantonio.tempo.repository;
import android.content.ContentResolver;
import android.net.Uri;
import android.view.View;
@@ -22,6 +23,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
@@ -70,7 +72,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
@@ -217,7 +219,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
@@ -272,7 +274,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>();
for (ArtistID3 artist : artists) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
@@ -397,7 +399,7 @@ public class AutomotiveRepository {
List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren();
for (Child song : children) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(song.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(song.getTitle())
@@ -451,7 +453,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>();
for (Child child : directory.getChildren()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(child.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(child.getTitle())
@@ -550,7 +552,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>();
for (PodcastEpisode episode : episodes) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(episode.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(episode.getTitle())
@@ -687,7 +689,7 @@ public class AutomotiveRepository {
List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
@@ -800,7 +802,7 @@ public class AutomotiveRepository {
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
@@ -822,7 +824,7 @@ public class AutomotiveRepository {
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())

View File

@@ -1,8 +1,5 @@
package com.cappielloantonio.tempo.repository;
import static android.provider.Settings.System.getString;
import android.provider.Settings;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -107,13 +104,13 @@ public class PlaylistRepository {
return playlistLiveData;
}
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
if (songsId.isEmpty()) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
} else{
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.updatePlaylist(playlistId, null, true, songsId, null)
.updatePlaylist(playlistId, null, playlistVisibilityIsPublic, songsId, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {

View File

@@ -283,7 +283,10 @@ public class SongRepository {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
if (returned != null) {
songs.addAll(returned);
}
}
randomSongsSample.setValue(songs);
}
@@ -299,7 +302,10 @@ public class SongRepository {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs()));
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
if (returned != null) {
songs.addAll(returned);
}
}
randomSongsSample.setValue(songs);
}
@@ -342,7 +348,10 @@ public class SongRepository {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs()));
List<Child> returned = response.body().getSubsonicResponse().getSongsByGenre().getSongs();
if (returned != null) {
songs.addAll(returned);
}
}
songsByGenre.setValue(songs);
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
@@ -32,6 +33,8 @@ import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
private const val TAG = "BaseMediaService"
@UnstableApi
open class BaseMediaService : MediaLibraryService() {
companion object {
@@ -81,7 +84,7 @@ open class BaseMediaService : MediaLibraryService() {
}
fun updateMediaItems(player: Player) {
Log.d(javaClass.toString(), "update items")
Log.d(TAG, "update items")
val n = player.mediaItemCount
val k = player.currentMediaItemIndex
val current = player.currentPosition
@@ -120,7 +123,7 @@ open class BaseMediaService : MediaLibraryService() {
fun initializePlayerListener(player: Player) {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
Log.d(javaClass.toString(), "onMediaItemTransition" + player.currentMediaItemIndex)
Log.d(TAG, "onMediaItemTransition" + player.currentMediaItemIndex)
if (mediaItem == null) return
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
@@ -130,7 +133,7 @@ open class BaseMediaService : MediaLibraryService() {
}
override fun onTracksChanged(tracks: Tracks) {
Log.d(javaClass.toString(), "onTracksChanged " + player.currentMediaItemIndex)
Log.d(TAG, "onTracksChanged " + player.currentMediaItemIndex)
ReplayGainUtil.setReplayGain(player, tracks)
val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null) {
@@ -138,14 +141,19 @@ open class BaseMediaService : MediaLibraryService() {
if (item.mediaMetadata.extras != null)
MediaManager.scrobble(item, false)
if (player.nextMediaItemIndex == C.INDEX_UNSET)
MediaManager.continuousPlay(player.currentMediaItem)
if (player.nextMediaItemIndex == C.INDEX_UNSET) {
val browserFuture = MediaBrowser.Builder(
this@BaseMediaService,
SessionToken(this@BaseMediaService, ComponentName(this@BaseMediaService, this@BaseMediaService::class.java))
).buildAsync()
MediaManager.continuousPlay(player.currentMediaItem, browserFuture)
}
}
if (player is ExoPlayer) {
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
if (MediaManager.justStarted.get()) {
Log.d(javaClass.toString(), "update shuffle order")
Log.d(TAG, "update shuffle order")
MediaManager.justStarted.set(false)
val shuffledList = IntArray(player.mediaItemCount) { i -> i }
shuffledList.shuffle()
@@ -163,7 +171,7 @@ open class BaseMediaService : MediaLibraryService() {
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.d(javaClass.toString(), "onIsPlayingChanged " + player.currentMediaItemIndex)
Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem,
@@ -181,7 +189,7 @@ open class BaseMediaService : MediaLibraryService() {
}
override fun onPlaybackStateChanged(playbackState: Int) {
Log.d(javaClass.toString(), "onPlaybackStateChanged")
Log.d(TAG, "onPlaybackStateChanged")
super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED &&
@@ -198,7 +206,7 @@ open class BaseMediaService : MediaLibraryService() {
newPosition: Player.PositionInfo,
reason: Int
) {
Log.d(javaClass.toString(), "onPositionDiscontinuity")
Log.d(TAG, "onPositionDiscontinuity")
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
@@ -222,7 +230,7 @@ open class BaseMediaService : MediaLibraryService() {
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
Log.d(javaClass.toString(), "onAudioSessionIdChanged")
Log.d(TAG, "onAudioSessionIdChanged")
attachEqualizerIfPossible(audioSessionId)
}
})
@@ -314,7 +322,7 @@ open class BaseMediaService : MediaLibraryService() {
}
private fun initializeMediaLibrarySession(player: Player) {
Log.d(javaClass.toString(), "initializeMediaLibrarySession")
Log.d(TAG, "initializeMediaLibrarySession")
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(Intent(baseContext, MainActivity::class.java))
@@ -461,7 +469,7 @@ open class BaseMediaService : MediaLibraryService() {
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
Log.d(javaClass.toString(), "onCustomCommand")
Log.d(TAG, "onCustomCommand")
when (customCommand.customAction) {
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
@@ -486,7 +494,7 @@ open class BaseMediaService : MediaLibraryService() {
controller: ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
Log.d(javaClass.toString(), "onAddMediaItems")
Log.d(TAG, "onAddMediaItems")
val updatedMediaItems = mediaItems.map { mediaItem ->
val mediaMetadata = mediaItem.mediaMetadata
val newMetadata = mediaMetadata.buildUpon()

View File

@@ -444,29 +444,33 @@ public class MediaManager {
}
@OptIn(markerClass = UnstableApi.class)
public static void continuousPlay(MediaItem mediaItem) {
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId,25);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media != null) {
Log.e(TAG, "continuous play");
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
App.getContext(),
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))
).buildAsync();
enqueue(mediaBrowserListenableFuture, media, true);
}
instantMix.removeObserver(this);
}
});
public static void continuousPlay(MediaItem mediaItem,
ListenableFuture<MediaBrowser> existingBrowserFuture) {
if (mediaItem == null
|| !Preferences.isContinuousPlayEnabled()
|| !Preferences.isInstantMixUsable()) {
return;
}
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix =
getSongRepository().getContinuousMix(mediaItem.mediaId, 25);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media == null || media.isEmpty()) {
return;
}
if (existingBrowserFuture != null) {
Log.d(TAG, "Continuous play: adding " + media.size() + " tracks");
enqueue(existingBrowserFuture, media, true);
}
instantMix.removeObserver(this);
}
});
}
public static void saveChronology(MediaItem mediaItem) {

View File

@@ -24,13 +24,15 @@ public class SystemClient {
public Call<ApiResponse> ping() {
Log.d(TAG, "ping()");
int timeoutSeconds = Preferences.getNetworkPingTimeout();
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
if (Preferences.isInUseServerAddressLocal()) {
pingCall.timeout()
.timeout(1, TimeUnit.SECONDS);
.timeout(timeoutSeconds, TimeUnit.SECONDS);
} else {
int finalTimeout = Math.min(timeoutSeconds * 2, 10);
pingCall.timeout()
.timeout(3, TimeUnit.SECONDS);
.timeout(finalTimeout, TimeUnit.SECONDS);
}
return pingCall;
}

View File

@@ -2,6 +2,8 @@ package com.cappielloantonio.tempo.ui.activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
@@ -11,6 +13,7 @@ import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen;
@@ -62,6 +65,7 @@ public class MainActivity extends BaseActivity {
private BottomNavigationView bottomNavigationView;
public NavController navController;
private BottomSheetBehavior bottomSheetBehavior;
private boolean isLandscape = false;
private AssetLinkNavigator assetLinkNavigator;
private AssetLinkUtil.AssetLink pendingAssetLink;
@@ -85,6 +89,8 @@ public class MainActivity extends BaseActivity {
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
connectivityStatusReceiverManager(true);
isLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
init();
checkConnectionType();
getOpenSubsonicExtensions();
@@ -141,6 +147,15 @@ public class MainActivity extends BaseActivity {
} else {
goToLogin();
}
// Set bottom navigation height
if (isLandscape) {
ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams();
Rect windowRect = new Rect();
bottomNavigationView.getWindowVisibleDisplayFrame(windowRect);
layoutParams.width = windowRect.height();
bottomNavigationView.setLayoutParams(layoutParams);
}
}
// BOTTOM SHEET/NAVIGATION
@@ -215,7 +230,9 @@ public class MainActivity extends BaseActivity {
@Override
public void onSlide(@NonNull View view, float slideOffset) {
animateBottomSheet(slideOffset);
animateBottomNavigation(slideOffset, navigationHeight);
if (!isLandscape) {
animateBottomNavigation(slideOffset, navigationHeight);
}
}
};
@@ -354,7 +371,7 @@ public class MainActivity extends BaseActivity {
// TODO Enter all settings to be reset
Preferences.setOpenSubsonic(false);
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
Preferences.setPlaybackSpeed(1.0f);
Preferences.setSkipSilenceMode(false);
Preferences.setDataSavingMode(false);
Preferences.setStarredSyncEnabled(false);
@@ -384,7 +401,7 @@ public class MainActivity extends BaseActivity {
}
private void pingServer() {
if (Preferences.getToken() == null) return;
if (Preferences.getToken() == null && Preferences.getPassword() == null) return;
if (Preferences.isInUseServerAddressLocal()) {
mainViewModel.ping().observe(this, subsonicResponse -> {
@@ -428,7 +445,7 @@ public class MainActivity extends BaseActivity {
}
private void getOpenSubsonicExtensions() {
if (Preferences.getToken() != null) {
if (Preferences.getToken() != null || Preferences.getPassword() != null) {
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
if (openSubsonicExtensions != null) {
Preferences.setOpenSubsonicExtensions(openSubsonicExtensions);

View File

@@ -173,10 +173,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
)
)
) {
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
if (songs.get(position).getDiscNumber() != null && !Objects.requireNonNull(songs.get(position).getDiscNumber()).toString().isBlank()) {
holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titleless, songs.get(position).getDiscNumber().toString()));
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
} else {
holder.item.differentDiskDividerSector.setVisibility(View.GONE);
}
if (album.getDiscTitles() != null) {

View File

@@ -0,0 +1,57 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class PlaybackSpeedDialog extends DialogFragment {
private static final String TAG = "PlaybackSpeedDialog";
public interface PlaybackSpeedListener {
void onSpeedSelected(float speed);
}
private PlaybackSpeedListener listener;
private static final float[] SPEED_VALUES = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
private static final String[] SPEED_LABELS = {"0.5x", "0.75x", "1.0x", "1.25x", "1.5x", "1.75x", "2.0x"};
public void setPlaybackSpeedListener(PlaybackSpeedListener listener) {
this.listener = listener;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
float currentSpeed = Preferences.getPlaybackSpeed();
int selectedIndex = getSelectedIndex(currentSpeed);
return new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.playback_speed_dialog_title)
.setSingleChoiceItems(SPEED_LABELS, selectedIndex, (dialog, which) -> {
float selectedSpeed = SPEED_VALUES[which];
Preferences.setPlaybackSpeed(selectedSpeed);
if (listener != null) {
listener.onSpeedSelected(selectedSpeed);
}
dialog.dismiss();
})
.setNegativeButton(R.string.playback_speed_dialog_negative_button, (dialog, id) -> dialog.cancel())
.create();
}
private int getSelectedIndex(float currentSpeed) {
for (int i = 0; i < SPEED_VALUES.length; i++) {
if (Math.abs(SPEED_VALUES[i] - currentSpeed) < 0.01f) {
return i;
}
}
return 2; // Default to 1.0x
}
}

View File

@@ -6,6 +6,7 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
private DialogPlaylistChooserBinding bind;
@@ -35,9 +37,21 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
String[] playlistVisibilityChoice = {
getString(R.string.playlist_chooser_dialog_visibility_public),
getString(R.string.playlist_chooser_dialog_visibility_private)
};
return new MaterialAlertDialogBuilder(getActivity())
.setView(bind.getRoot())
.setTitle(R.string.playlist_chooser_dialog_title)
.setSingleChoiceItems(
playlistVisibilityChoice,
0,
(dialog, which) -> {
boolean isPublic = (which == 0);
playlistChooserViewModel.setIsPlaylistPublic(isPublic);
})
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
.create();

View File

@@ -1,6 +1,6 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import androidx.appcompat.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -49,6 +50,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
private AlbumCatalogueViewModel albumCatalogueViewModel;
private AlbumCatalogueAdapter albumAdapter;
private int spanCount = 2;
private String currentSortOrder;
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
@@ -90,6 +92,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
View view = bind.getRoot();
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
initAppBar();
initAlbumCatalogueView();
initProgressLoader();
@@ -133,8 +139,8 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
@SuppressLint("ClickableViewAccessibility")
private void initAlbumCatalogueView() {
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
albumAdapter = new AlbumCatalogueAdapter(this, true);

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -50,6 +51,7 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
private ArtistCatalogueViewModel artistCatalogueViewModel;
private ArtistCatalogueAdapter artistAdapter;
private int spanCount = 2;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -66,6 +68,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
View view = bind.getRoot();
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
initAppBar();
initArtistCatalogueView();
@@ -108,8 +114,8 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
@SuppressLint("ClickableViewAccessibility")
private void initArtistCatalogueView() {
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.artistCatalogueRecyclerView.setHasFixedSize(true);
artistAdapter = new ArtistCatalogueAdapter(this);

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
@@ -63,6 +64,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private int spanCount = 2;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@@ -72,6 +75,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
init(view);
initAppBar();
initArtistInfo();
@@ -277,8 +284,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
}
private void initAlbumsView() {
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.albumsRecyclerView.setHasFixedSize(true);
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
@@ -296,8 +303,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
}
private void initSimilarArtistsView() {
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
bind.similarArtistsRecyclerView.setHasFixedSize(true);
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -32,6 +33,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
@OptIn(markerClass = UnstableApi.class)
@@ -41,6 +43,7 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
private GenreCatalogueViewModel genreCatalogueViewModel;
private GenreCatalogueAdapter genreCatalogueAdapter;
private int spanCount = 2;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -56,6 +59,10 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
View view = bind.getRoot();
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = Preferences.getLandscapeItemsPerRow();
}
init();
initAppBar();
initGenreCatalogueView();
@@ -97,8 +104,8 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
@SuppressLint("ClickableViewAccessibility")
private void initGenreCatalogueView() {
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 16, false));
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 16, false));
bind.genreCatalogueRecyclerView.setHasFixedSize(true);
genreCatalogueAdapter = new GenreCatalogueAdapter(this);

View File

@@ -1262,7 +1262,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
if (mediaBrowserListenableFuture != null) {
final boolean[] playbackStarted = {false};
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), track)
.observe(getViewLifecycleOwner(), songs -> {
if (playbackStarted[0] || songs == null || songs.isEmpty()) return;

View File

@@ -39,6 +39,7 @@ import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBindi
import com.cappielloantonio.tempo.service.EqualizerManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.PlaybackSpeedDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
@@ -413,10 +414,10 @@ public class PlayerControllerFragment extends Fragment {
bind.getRoot().setShowNextButton(true);
bind.getRoot().setShowFastForwardButton(false);
bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL | RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE);
bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.GONE);
bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.VISIBLE);
bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.GONE);
bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.VISIBLE);
resetPlaybackParameters(mediaBrowser);
setPlaybackParameters(mediaBrowser);
break;
}
}
@@ -522,33 +523,12 @@ public class PlayerControllerFragment extends Fragment {
private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) {
playbackSpeedButton.setOnClickListener(view -> {
float currentSpeed = Preferences.getPlaybackSpeed();
if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_080) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_100));
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_100) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_125));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_125));
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_125);
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_125) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_150));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_150));
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_150);
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_150) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_175));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_175));
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_175);
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_175) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_200));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_200));
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_200);
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_200) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_080));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_080));
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_080);
}
PlaybackSpeedDialog dialog = new PlaybackSpeedDialog();
dialog.setPlaybackSpeedListener(speed -> {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(speed));
playbackSpeedButton.setText(getString(R.string.player_playback_speed, speed));
});
dialog.show(requireActivity().getSupportFragmentManager(), null);
});
skipSilenceToggleButton.setOnClickListener(view -> {
@@ -600,7 +580,7 @@ public class PlayerControllerFragment extends Fragment {
}
private void resetPlaybackParameters(MediaBrowser mediaBrowser) {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
mediaBrowser.setPlaybackParameters(new PlaybackParameters(1.0f));
// TODO Resettare lo skip del silenzio
}

View File

@@ -7,7 +7,9 @@ import android.os.Handler;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
@@ -51,6 +53,7 @@ public class PlayerLyricsFragment extends Fragment {
private Runnable syncLyricsRunnable;
private String currentLyrics;
private LyricsList currentLyricsList;
private Integer lastLineIdx;
private String currentDescription;
@Override
@@ -109,6 +112,7 @@ public class PlayerLyricsFragment extends Fragment {
currentLyrics = null;
currentLyricsList = null;
currentDescription = null;
lastLineIdx = null;
}
private void initOverlay() {
@@ -162,6 +166,7 @@ public class PlayerLyricsFragment extends Fragment {
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
currentLyricsList = lyricsList;
lastLineIdx = null;
updatePanelContent();
});
@@ -194,7 +199,7 @@ public class PlayerLyricsFragment extends Fragment {
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
if (hasStructuredLyrics(currentLyricsList)) {
setSyncLirics(currentLyricsList);
setSyncLyrics(currentLyricsList);
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
@@ -241,14 +246,14 @@ public class PlayerLyricsFragment extends Fragment {
}
@SuppressLint("DefaultLocale")
private void setSyncLirics(LyricsList lyricsList) {
private void setSyncLyrics(LyricsList lyricsList) {
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
StringBuilder lyricsBuilder = new StringBuilder();
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
if (lines != null) {
for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n");
lyricsBuilder.append(line.getValue().trim()).append("\n\n");
}
}
@@ -288,67 +293,75 @@ public class PlayerLyricsFragment extends Fragment {
int timestamp = (int) (mediaBrowser.getCurrentPosition());
if (hasStructuredLyrics(lyricsList)) {
StringBuilder lyricsBuilder = new StringBuilder();
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
if (lines == null || lines.isEmpty()) return;
for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n");
if (lines == null || lines.isEmpty()) {
return;
}
Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null);
if (toHighlight != null) {
String lyrics = lyricsBuilder.toString();
Spannable spannableString = new SpannableString(lyrics);
int startingPosition = getStartPosition(lines, toHighlight);
int endingPosition = startingPosition + toHighlight.getValue().length();
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
bind.nowPlayingSongLyricsTextView.setText(spannableString);
if (playerBottomSheetViewModel.getSyncLyricsState()) {
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight));
// Find the index of the currently playing line
int curIdx = 0;
for (; curIdx < lines.size(); ++curIdx) {
Integer start = lines.get(curIdx).getStart();
if (start != null && start > timestamp) {
curIdx--; // Found the first line that starts after the current timestamp
break;
}
}
}
}
private int getStartPosition(List<Line> lines, Line toHighlight) {
int start = 0;
// Only update if the highlighted line has changed
if (lastLineIdx != null && curIdx == lastLineIdx) {
return;
}
lastLineIdx = curIdx;
for (Line line : lines) {
if (line != toHighlight) {
start = start + line.getValue().length() + 1;
} else {
break;
StringBuilder lyricsBuilder = new StringBuilder();
for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n\n");
}
String lyrics = lyricsBuilder.toString();
Spannable spannableString = new SpannableString(lyrics);
// Make each line clickable for navigation and highlight the current one
int offset = 0;
int highlightStart = -1;
for (int i = 0; i < lines.size(); ++i) {
boolean highlight = i == curIdx;
if (highlight) highlightStart = offset;
int len = lines.get(i).getValue().length() + 2;
final int lineStart = lines.get(i).getStart();
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View view) {
// Seeking to 1ms after the actual start prevents scrolling / highlighting artifacts
mediaBrowser.seekTo(lineStart + 1);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
if (highlight) {
ds.setColor(requireContext().getResources().getColor(R.color.lyricsTextColor, null));
} else {
ds.setColor(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null));
}
}
}, offset, offset + len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
offset += len;
}
bind.nowPlayingSongLyricsTextView.setMovementMethod(LinkMovementMethod.getInstance());
bind.nowPlayingSongLyricsTextView.setText(spannableString);
// Scroll to the highlighted line, but only if there is one
if (highlightStart >= 0 && playerBottomSheetViewModel.getSyncLyricsState()) {
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(highlightStart));
}
}
return start;
}
private int getLineCount(List<Line> lines, Line toHighlight) {
int start = 0;
for (Line line : lines) {
if (line != toHighlight) {
bind.tempLyricsLineTextView.setText(line.getValue());
start = start + bind.tempLyricsLineTextView.getLineCount();
} else {
break;
}
}
return start;
}
private int getScroll(List<Line> lines, Line toHighlight) {
int startIndex = getStartPosition(lines, toHighlight);
private int getScroll(int startIndex) {
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
if (layout == null) return 0;

View File

@@ -156,10 +156,10 @@ public class PlaylistCatalogueFragment extends Fragment implements ClickCallback
popup.setOnMenuItemClickListener(menuItem -> {
if (menuItem.getItemId() == R.id.menu_playlist_sort_name) {
playlistHorizontalAdapter.sort(Constants.GENRE_ORDER_BY_NAME);
playlistHorizontalAdapter.sort(Constants.PLAYLIST_ORDER_BY_NAME);
return true;
} else if (menuItem.getItemId() == R.id.menu_playlist_sort_random) {
playlistHorizontalAdapter.sort(Constants.GENRE_ORDER_BY_RANDOM);
playlistHorizontalAdapter.sort(Constants.PLAYLIST_ORDER_BY_RANDOM);
return true;
}

View File

@@ -9,6 +9,8 @@ import android.media.audiofx.AudioEffect;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.text.InputFilter;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -27,6 +29,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.navigation.NavController;
import androidx.navigation.NavOptions;
import androidx.navigation.fragment.NavHostFragment;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
@@ -141,6 +144,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
setStreamingCacheSize();
setAppLanguage();
setVersion();
setNetorkPingTimeoutBase();
actionLogout();
actionScan();
@@ -261,6 +265,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
private void setNetorkPingTimeoutBase() {
EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base");
if (networkPingTimeoutBase != null) {
networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance());
networkPingTimeoutBase.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> {
for (int i = start; i < end; i++) {
if (!Character.isDigit(source.charAt(i))) {
return "";
}
}
return null;
}});
});
networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> {
String input = (String) newValue;
return input != null && !input.isEmpty();
});
}
}
private void setStreamingCacheSize() {
ListPreference streamingCachePreference = findPreference("streaming_cache_size");

View File

@@ -61,13 +61,6 @@ object Constants {
const val MEDIA_TYPE_VIDEO = "video"
const val MEDIA_TYPE_RADIO = "radio"
const val MEDIA_PLAYBACK_SPEED_080 = 0.8f
const val MEDIA_PLAYBACK_SPEED_100 = 1.0f
const val MEDIA_PLAYBACK_SPEED_125 = 1.25f
const val MEDIA_PLAYBACK_SPEED_150 = 1.50f
const val MEDIA_PLAYBACK_SPEED_175 = 1.75f
const val MEDIA_PLAYBACK_SPEED_200 = 2.0f
const val MEDIA_RECENTLY_PLAYED = "MEDIA_RECENTLY_PLAYED"
const val MEDIA_MOST_PLAYED = "MEDIA_MOST_PLAYED"
const val MEDIA_RECENTLY_ADDED = "MEDIA_RECENTLY_ADDED"

View File

@@ -46,8 +46,16 @@ class DynamicMediaSourceFactory(
else -> {
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
.createMediaSource(mediaItem)
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
val uri = mediaItem.localConfiguration?.uri
val isTranscoding = uri?.getQueryParameter("format") != null && uri.getQueryParameter("format") != "raw"
if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) {
TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory)
} else {
progressiveFactory.createMediaSource(mediaItem)
}
}
}
}

View File

@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.util;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
@@ -15,6 +16,7 @@ import androidx.media3.common.HeartRating;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
@@ -45,7 +47,7 @@ public class MappingUtil {
Uri artworkUri = null;
if (coverArtId != null) {
artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, Preferences.getImageSize()));
artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
}
Bundle bundle = new Bundle();
@@ -235,7 +237,7 @@ public class MappingUtil {
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
Uri uri = getUri(podcastEpisode);
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize()));
Uri artworkUri = AlbumArtContentProvider.contentUri(podcastEpisode.getCoverArtId());
Bundle bundle = new Bundle();
bundle.putString("id", podcastEpisode.getId());

View File

@@ -31,7 +31,7 @@ public class 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) {
public static Uri getStreamUri(String id, int timeOffset) {
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
StringBuilder uri = new StringBuilder();
@@ -58,6 +58,8 @@ public class MusicUtil {
uri.append("&format=").append(getTranscodingFormatPreference());
if (Preferences.askForEstimateContentLength())
uri.append("&estimateContentLength=true");
if (timeOffset > 0)
uri.append("&timeOffset=").append(timeOffset);
uri.append("&id=").append(id);
@@ -66,6 +68,10 @@ public class MusicUtil {
return Uri.parse(uri.toString());
}
public static Uri getStreamUri(String id) {
return getStreamUri(id, 0);
}
public static Uri updateStreamUri(Uri uri) {
String s = uri.toString();
Matcher m1 = BITRATE_PATTERN.matcher(s);

View File

@@ -29,6 +29,7 @@ object Preferences {
private const val REPEAT_MODE = "repeat_mode"
private const val IMAGE_CACHE_SIZE = "image_cache_size"
private const val STREAMING_CACHE_SIZE = "streaming_cache_size"
private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row"
private const val IMAGE_SIZE = "image_size"
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
@@ -74,6 +75,8 @@ object Preferences {
private const val CONTINUOUS_PLAY = "continuous_play"
private const val LAST_INSTANT_MIX = "last_instant_mix"
private const val ALLOW_PLAYLIST_DUPLICATES = "allow_playlist_duplicates"
private const val HOME_SORT_PLAYLISTS = "home_sort_playlists"
private const val DEFAULT_HOME_SORT_PLAYLISTS_SORT_ORDER = Constants.PLAYLIST_ORDER_BY_RANDOM
private const val EQUALIZER_ENABLED = "equalizer_enabled"
private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels"
private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility"
@@ -83,6 +86,8 @@ object Preferences {
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
@JvmStatic
fun getServer(): String? {
@@ -94,6 +99,19 @@ object Preferences {
App.getInstance().preferences.edit().putString(SERVER, server).apply()
}
@JvmStatic
fun getNetworkPingTimeout(): Int {
val timeoutString = App.getInstance().preferences.getString(NETWORK_PING_TIMEOUT, "2") ?: "2"
return (timeoutString.toIntOrNull() ?: 2).coerceAtLeast(1)
}
@JvmStatic
fun setNetworkPingTimeout(pingTimeout: String?) {
App.getInstance().preferences.edit().putString(NETWORK_PING_TIMEOUT, pingTimeout).apply()
}
@JvmStatic
fun getUser(): String? {
return App.getInstance().preferences.getString(USER, null)
@@ -287,6 +305,11 @@ object Preferences {
return App.getInstance().preferences.getString(IMAGE_CACHE_SIZE, "500")!!.toInt()
}
@JvmStatic
fun getLandscapeItemsPerRow(): Int {
return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt()
}
@JvmStatic
fun getImageSize(): Int {
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()
@@ -625,6 +648,16 @@ object Preferences {
return App.getInstance().preferences.getBoolean(ALLOW_PLAYLIST_DUPLICATES, false)
}
@JvmStatic
fun getHomeSortPlaylists(): String {
return App.getInstance().preferences.getString(HOME_SORT_PLAYLISTS, DEFAULT_HOME_SORT_PLAYLISTS_SORT_ORDER) ?: DEFAULT_HOME_SORT_PLAYLISTS_SORT_ORDER
}
@JvmStatic
fun getHomeSortPlaylists(sortOrder: String) {
App.getInstance().preferences.edit().putString(HOME_SORT_PLAYLISTS, sortOrder).apply()
}
@JvmStatic
fun setEqualizerEnabled(enabled: Boolean) {
App.getInstance().preferences.edit().putBoolean(EQUALIZER_ENABLED, enabled).apply()

View File

@@ -7,6 +7,7 @@ import androidx.media3.common.Metadata;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.Player;
import androidx.media3.extractor.metadata.id3.InternalFrame;
import com.cappielloantonio.tempo.model.ReplayGain;
@@ -82,26 +83,32 @@ public class ReplayGainUtil {
private static ReplayGain setReplayGains(Metadata.Entry entry) {
ReplayGain replayGain = new ReplayGain();
if (entry.toString().contains(tags[0])) {
replayGain.setTrackGain(parseReplayGainTag(entry));
// The logic below assumes .toString() contains the dB value. That's not the case for InternalFrame
String str = entry.toString();
if (entry instanceof InternalFrame) {
str = ((InternalFrame) entry).description + ((InternalFrame) entry).text;
}
if (entry.toString().contains(tags[1])) {
replayGain.setAlbumGain(parseReplayGainTag(entry));
if (str.contains(tags[0])) {
replayGain.setTrackGain(parseReplayGainTag(str));
}
if (entry.toString().contains(tags[2])) {
replayGain.setTrackGain(parseReplayGainTag(entry) / 256f);
if (str.contains(tags[1])) {
replayGain.setAlbumGain(parseReplayGainTag(str));
}
if (entry.toString().contains(tags[3])) {
replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f);
if (str.contains(tags[2])) {
replayGain.setTrackGain(parseReplayGainTag(str) / 256f);
}
if (str.contains(tags[3])) {
replayGain.setAlbumGain(parseReplayGainTag(str) / 256f);
}
return replayGain;
}
private static Float parseReplayGainTag(Metadata.Entry entry) {
private static Float parseReplayGainTag(String entry) {
try {
return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", ""));
} catch (NumberFormatException exception) {

View File

@@ -0,0 +1,322 @@
package com.cappielloantonio.tempo.util
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Timeline
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.TransferListener
import androidx.media3.decoder.DecoderInputBuffer
import androidx.media3.exoplayer.FormatHolder
import androidx.media3.exoplayer.LoadingInfo
import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.source.CompositeMediaSource
import androidx.media3.exoplayer.source.ForwardingTimeline
import androidx.media3.exoplayer.source.MediaPeriod
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SampleStream
import androidx.media3.exoplayer.trackselection.ExoTrackSelection
import androidx.media3.exoplayer.upstream.Allocator
@OptIn(UnstableApi::class)
class TranscodingMediaSource(
private val mediaItem: MediaItem,
private val dataSourceFactory: DataSource.Factory,
private val progressiveMediaSourceFactory: ProgressiveMediaSource.Factory
) : CompositeMediaSource<Void>() {
private var durationUs: Long = C.TIME_UNSET
private var currentSource: MediaSource? = null
init {
val extras = mediaItem.mediaMetadata.extras
if (extras != null && extras.containsKey("duration")) {
val seconds = extras.getInt("duration")
if (seconds > 0) {
durationUs = Util.msToUs(seconds * 1000L)
}
}
}
override fun getMediaItem() = mediaItem
override fun prepareSourceInternal(mediaTransferListener: TransferListener?) {
super.prepareSourceInternal(mediaTransferListener)
val initialSource = progressiveMediaSourceFactory.createMediaSource(mediaItem)
currentSource = initialSource
prepareChildSource(null, initialSource)
}
override fun onChildSourceInfoRefreshed(
childSourceId: Void?,
mediaSource: MediaSource,
newTimeline: Timeline
) {
val timeline =
if (durationUs != C.TIME_UNSET) {
DurationOverridingTimeline(newTimeline, durationUs)
} else {
newTimeline
}
refreshSourceInfo(timeline)
}
override fun createPeriod(
id: MediaSource.MediaPeriodId,
allocator: Allocator,
startPositionUs: Long
): MediaPeriod {
val source = currentSource ?: throw IllegalStateException("Source not ready")
val childPeriod = source.createPeriod(id, allocator, startPositionUs)
return TranscodingMediaPeriod(childPeriod, source, id, allocator)
}
override fun releasePeriod(mediaPeriod: MediaPeriod) {
val transcodingPeriod = mediaPeriod as TranscodingMediaPeriod
transcodingPeriod.release()
if (transcodingPeriod.currentOffsetUs > 0) {
releaseChildSource(null)
val initialSource = progressiveMediaSourceFactory.createMediaSource(mediaItem)
currentSource = initialSource
prepareChildSource(null, initialSource)
}
}
override fun getMediaPeriodIdForChildMediaPeriodId(
childSourceId: Void?,
mediaPeriodId: MediaSource.MediaPeriodId
) = mediaPeriodId
private inner class TranscodingMediaPeriod(
private var currentPeriod: MediaPeriod,
private var source: MediaSource,
private val id: MediaSource.MediaPeriodId,
private val allocator: Allocator
) : MediaPeriod, MediaPeriod.Callback {
private var localCallback: MediaPeriod.Callback? = null
internal var currentOffsetUs: Long = 0
private var isReloading = false
private var lastSelections: Array<out ExoTrackSelection?>? = null
private var lastMayRetainStreamFlags: BooleanArray? = null
private var activeWrappers: Array<OffsetSampleStream?> = emptyArray()
fun release() {
source.releasePeriod(currentPeriod)
}
override fun prepare(callback: MediaPeriod.Callback, positionUs: Long) {
localCallback = callback
currentPeriod.prepare(this, positionUs)
}
override fun maybeThrowPrepareError() {
if (!isReloading) currentPeriod.maybeThrowPrepareError()
}
override fun getTrackGroups() = currentPeriod.trackGroups
override fun getStreamKeys(trackSelections: MutableList<ExoTrackSelection>) =
currentPeriod.getStreamKeys(trackSelections)
override fun selectTracks(
selections: Array<out ExoTrackSelection?>,
mayRetainStreamFlags: BooleanArray,
streams: Array<SampleStream?>,
streamResetFlags: BooleanArray,
positionUs: Long
): Long {
lastSelections = selections
lastMayRetainStreamFlags = mayRetainStreamFlags
val childStreams = arrayOfNulls<SampleStream>(streams.size)
streams.forEachIndexed { i, stream ->
childStreams[i] = (stream as? OffsetSampleStream)?.childStream
}
val startPos =
currentPeriod.selectTracks(
selections,
mayRetainStreamFlags,
childStreams,
streamResetFlags,
positionUs - currentOffsetUs
)
val newWrappers = arrayOfNulls<OffsetSampleStream>(streams.size)
for (i in streams.indices) {
val child = childStreams[i]
if (child == null) {
streams[i] = null
} else {
val existingWrapper = streams[i] as? OffsetSampleStream
if (existingWrapper != null && existingWrapper.childStream === child) {
newWrappers[i] = existingWrapper
} else {
val wrapper = OffsetSampleStream(child)
newWrappers[i] = wrapper
streams[i] = wrapper
}
}
}
activeWrappers = newWrappers
return startPos + currentOffsetUs
}
override fun discardBuffer(positionUs: Long, toKeyframe: Boolean) {
if (!isReloading) {
currentPeriod.discardBuffer(positionUs - currentOffsetUs, toKeyframe)
}
}
override fun readDiscontinuity(): Long {
if (isReloading) return C.TIME_UNSET
val discontinuity = currentPeriod.readDiscontinuity()
return if (discontinuity == C.TIME_UNSET) C.TIME_UNSET
else discontinuity + currentOffsetUs
}
override fun seekToUs(positionUs: Long): Long {
if (positionUs == 0L && currentOffsetUs == 0L) {
return currentPeriod.seekToUs(positionUs)
}
reloadSource(positionUs)
return positionUs
}
override fun getAdjustedSeekPositionUs(positionUs: Long, seekParameters: SeekParameters) =
positionUs
override fun getBufferedPositionUs(): Long {
if (isReloading) return currentOffsetUs
val buffered = currentPeriod.bufferedPositionUs
if (buffered == C.TIME_END_OF_SOURCE) return C.TIME_END_OF_SOURCE
return if (buffered == C.TIME_UNSET) C.TIME_UNSET else buffered + currentOffsetUs
}
override fun getNextLoadPositionUs(): Long {
if (isReloading) return C.TIME_UNSET
val next = currentPeriod.nextLoadPositionUs
if (next == C.TIME_END_OF_SOURCE) return C.TIME_END_OF_SOURCE
return if (next == C.TIME_UNSET) C.TIME_UNSET else next + currentOffsetUs
}
override fun reevaluateBuffer(positionUs: Long) {
if (!isReloading) currentPeriod.reevaluateBuffer(positionUs - currentOffsetUs)
}
override fun continueLoading(isLoading: LoadingInfo): Boolean {
if (isReloading) return false
val builder = isLoading.buildUpon()
builder.setPlaybackPositionUs(isLoading.playbackPositionUs - currentOffsetUs)
return currentPeriod.continueLoading(builder.build())
}
override fun isLoading() = isReloading || currentPeriod.isLoading
override fun onPrepared(mediaPeriod: MediaPeriod) {
if (isReloading && mediaPeriod == currentPeriod) {
isReloading = false
restoreTracks()
localCallback?.onContinueLoadingRequested(this)
} else {
localCallback?.onPrepared(this)
}
}
override fun onContinueLoadingRequested(source: MediaPeriod) {
if (!isReloading) localCallback?.onContinueLoadingRequested(this)
}
private fun reloadSource(positionUs: Long) {
isReloading = true
currentOffsetUs = positionUs
activeWrappers.forEach { it?.childStream = null }
source.releasePeriod(currentPeriod)
releaseChildSource(null)
val seconds = Util.usToMs(positionUs) / 1000
val newUri = MusicUtil.getStreamUri(mediaItem.mediaId, seconds.toInt())
val newMediaItem = mediaItem.buildUpon().setUri(newUri).build()
val newSource = progressiveMediaSourceFactory.createMediaSource(newMediaItem)
source = newSource
currentSource = newSource
prepareChildSource(null, newSource)
val newPeriod = newSource.createPeriod(id, allocator, 0)
currentPeriod = newPeriod
newPeriod.prepare(this, 0)
}
private fun restoreTracks() {
val selections = lastSelections ?: return
val flags = lastMayRetainStreamFlags ?: return
val childStreams = arrayOfNulls<SampleStream>(activeWrappers.size)
val streamResetFlags = BooleanArray(activeWrappers.size)
currentPeriod.selectTracks(selections, flags, childStreams, streamResetFlags, 0)
for (i in activeWrappers.indices) {
activeWrappers[i]?.childStream = childStreams[i]
}
}
private inner class OffsetSampleStream(var childStream: SampleStream?) : SampleStream {
override fun isReady() = childStream?.isReady ?: false
override fun maybeThrowError() {
childStream?.maybeThrowError()
}
override fun readData(
formatHolder: FormatHolder,
buffer: DecoderInputBuffer,
readFlags: Int
): Int {
val stream = childStream ?: return C.RESULT_NOTHING_READ
val result = stream.readData(formatHolder, buffer, readFlags)
if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream) {
buffer.timeUs += currentOffsetUs
}
return result
}
override fun skipData(positionUs: Long) =
childStream?.skipData(positionUs - currentOffsetUs) ?: 0
}
}
private class DurationOverridingTimeline(timeline: Timeline, private val durationUs: Long) :
ForwardingTimeline(timeline) {
override fun getWindow(
windowIndex: Int,
window: Window,
defaultPositionProjectionUs: Long
): Window {
super.getWindow(windowIndex, window, defaultPositionProjectionUs)
window.durationUs = durationUs
window.isSeekable = true
window.isDynamic = false
window.liveConfiguration = null
return window
}
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
super.getPeriod(periodIndex, period, setIds)
period.durationUs = durationUs
return period
}
}
}

View File

@@ -24,6 +24,7 @@ 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.Share;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Constants.SeedType;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.reflect.TypeToken;
@@ -100,7 +101,7 @@ public class HomeViewModel extends AndroidViewModel {
}
public LiveData<List<Child>> getRandomShuffleSample() {
return songRepository.getRandomSample(1000, null, null);
return songRepository.getRandomSample(100, null, null);
}
public LiveData<List<Chronology>> getChronologySample(LifecycleOwner owner) {
@@ -250,12 +251,19 @@ public class HomeViewModel extends AndroidViewModel {
playlistRepository.getPlaylists(false, -1).observe(owner, remotes -> {
if (remotes != null && !remotes.isEmpty()) {
List<Playlist> playlists = new ArrayList<>(remotes);
Collections.shuffle(playlists);
List<Playlist> randomPlaylists = playlists.size() > 5
String result = Preferences.getHomeSortPlaylists();
if (Preferences.getHomeSortPlaylists().equals(Constants.PLAYLIST_ORDER_BY_RANDOM))
{
Collections.shuffle(playlists);
}
else {
playlists.sort(Comparator.comparing(Playlist::getName));
}
List<Playlist> subsetPlaylists = playlists.size() > 5
? playlists.subList(0, 5)
: playlists;
pinnedPlaylists.setValue(randomPlaylists);
pinnedPlaylists.setValue(subsetPlaylists);
}
});

View File

@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.viewmodel;
import android.app.Application;
import android.app.Dialog;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
@@ -21,8 +20,17 @@ import java.util.List;
public class PlaylistChooserViewModel extends AndroidViewModel {
private final PlaylistRepository playlistRepository;
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> playlistIsPublic = new MutableLiveData<>(false);
public Boolean getIsPlaylistPublic() {
return playlistIsPublic.getValue();
}
public void setIsPlaylistPublic(boolean isPublic) {
playlistIsPublic.setValue(isPublic);
}
private ArrayList<Child> toAdd = new ArrayList<>();
public PlaylistChooserViewModel(@NonNull Application application) {
@@ -39,7 +47,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
List<String> songIds = Lists.transform(toAdd, Child::getId);
if (Preferences.allowPlaylistDuplicates()) {
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
dialog.dismiss();
} else {
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
@@ -47,7 +55,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
songIds.removeAll(playlistSongIds);
}
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
dialog.dismiss();
});
}

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotY="12"
android:pivotX="12"
android:rotation="270">
<path
android:fillColor="#FF000000"
android:pathData="M8,18c0.55,0 1,-0.45 1,-1L9,7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v10c0,0.55 0.45,1 1,1zM12,22c0.55,0 1,-0.45 1,-1L13,3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v18c0,0.55 0.45,1 1,1zM4,14c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM16,18c0.55,0 1,-0.45 1,-1L17,7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v10c0,0.55 0.45,1 1,1zM19,11v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1z" />
</group>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotY="12"
android:pivotX="12"
android:rotation="270">
<path
android:fillColor="#FF000000"
android:pathData="M12,5.69l5,4.5V18h-2v-6H9v6H7v-7.81l5,-4.5M12,3L2,12h3v8h6v-6h2v6h6v-8h3L12,3z" />
</group>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotY="12"
android:pivotX="12"
android:rotation="270">
<path
android:fillColor="#FF000000"
android:pathData="M11,5v5.59L7.5,10.59l4.5,4.5 4.5,-4.5L13,10.59L13,5h-2zM6,14c0,3.31 2.69,6 6,6s6,-2.69 6,-6h-2c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4L6,14z"/>
</group>
</vector>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:orientation="vertical">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:layout_width="75dp"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="250dp"
android:layout_height="75dp"
android:rotation="90"
android:layout_gravity="center"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:visibility="gone"
app:menu="@menu/bottom_nav_menu" />
</FrameLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</LinearLayout>
<FrameLayout
android:id="@+id/player_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
app:layout_behavior="@string/bottom_sheet_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<TextView
android:id="@+id/offline_mode_text_view"
style="@style/NoConnectionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/activity_info_offline_mode"
android:textSize="6sp"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,305 @@
<?xml version="1.0" encoding="utf-8"?>
<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:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/album_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:paddingStart="20dp"
app:layout_scrollFlags="exitUntilCollapsed">
<ImageView
android:id="@+id/album_cover_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="64dp"
android:layout_marginTop="8dp"
android:layout_marginRight="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/album_name_label"
style="@style/LabelExtraLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingTop="8dp"
android:singleLine="false"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="@+id/album_cover_image_view"
app:layout_constraintStart_toStartOf="@+id/album_cover_image_view"
app:layout_constraintTop_toBottomOf="@+id/album_cover_image_view" />
<FrameLayout
android:id="@+id/album_other_info_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@+id/album_name_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/album_name_label"
app:layout_constraintTop_toTopOf="@+id/album_name_label">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/ic_arrow_down" />
</FrameLayout>
<TextView
android:id="@+id/album_artist_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_name_label" />
<TextView
android:id="@+id/album_release_year_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_artist_label" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/album_detail_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_release_year_label"
tools:visibility="visible">
<TextView
android:id="@+id/album_genres_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/album_song_count_duration_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:paddingVertical="2dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_genres_textview" />
<TextView
android:id="@+id/album_notes_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:justificationMode="inter_word"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_song_count_duration_textview" />
<TextView
android:id="@+id/album_release_years_textview"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:paddingVertical="4dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_notes_textview" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_detail_view" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
<LinearLayout
android:id="@+id/album_page_button_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/album_page_play_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/album_page_play_button"
android:textAllCaps="false"
app:icon="@drawable/ic_play"
app:iconGravity="textStart"
app:iconPadding="18dp" />
<Button
android:id="@+id/album_page_shuffle_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/album_page_shuffle_button"
android:textAllCaps="false"
app:icon="@drawable/ic_shuffle"
app:iconGravity="textStart"
app:iconPadding="18dp" />
</LinearLayout>
<ToggleButton
android:id="@+id/button_favorite"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="0dp"
android:background="@drawable/button_favorite_selector"
android:checked="false"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text=""
android:textOff=""
android:textOn="" />
</LinearLayout>
<TextView
android:id="@+id/album_bio_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/album_page_button_layout"
tools:ignore="NotSibling" />
<View
android:id="@+id/bottom_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_bio_label" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:nestedScrollingEnabled="false"
android:paddingTop="0dp"
android:paddingBottom="75dp"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:paddingBottom="@dimen/global_padding_bottom"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/playlist_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/playlist_cover_image_view_top_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_top_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/playlist_cover_image_view_top_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="64dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_top_left"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/playlist_cover_image_view_bottom_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_bottom_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/playlist_cover_image_view_top_left" />
<ImageView
android:id="@+id/playlist_cover_image_view_bottom_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_bottom_left"
app:layout_constraintTop_toTopOf="@id/playlist_cover_image_view_bottom_left" />
<TextView
android:id="@+id/playlist_name_label"
style="@style/LabelExtraLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:ellipsize="end"
android:maxLines="2"
android:paddingTop="8dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_cover_image_view_bottom_left" />
<TextView
android:id="@+id/playlist_song_count_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_name_label" />
<TextView
android:id="@+id/playlist_duration_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_song_count_label" />
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_duration_label" />
<LinearLayout
android:id="@+id/playlist_page_button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
<Button
android:id="@+id/playlist_page_play_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/playlist_page_play_button"
android:textAllCaps="false"
app:icon="@drawable/ic_play"
app:iconGravity="textStart"
app:iconPadding="18dp" />
<Button
android:id="@+id/playlist_page_shuffle_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:padding="10dp"
android:text="@string/playlist_page_shuffle_button"
android:textAllCaps="false"
app:icon="@drawable/ic_shuffle"
app:iconGravity="textStart"
app:iconPadding="18dp" />
</LinearLayout>
<TextView
android:id="@+id/album_bio_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:text="@string/label_placeholder"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
<View
android:id="@+id/bottom_button_divider"
style="@style/Divider"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -16,6 +16,23 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/player_playback_speed_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="80dp"
android:layout_height="48dp"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="?attr/colorOnPrimaryContainer" />
<com.google.android.material.chip.Chip
android:id="@+id/player_media_extension"
style="@style/Widget.Material3.Chip.Suggestion"
@@ -253,23 +270,6 @@
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right" />
<Button
android:id="@+id/player_playback_speed_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_shuffle"
android:layout_width="32dp"

View File

@@ -46,6 +46,8 @@
style="@style/BodyLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:lineSpacingExtra="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/homeFragment"
android:icon="@drawable/ic_home_land"
android:title="@string/menu_home_label" />
<item
android:id="@+id/libraryFragment"
android:icon="@drawable/ic_graphic_eq_land"
android:title="@string/menu_library_label" />
<item
android:id="@+id/downloadFragment"
android:icon="@drawable/ic_play_for_work_land"
android:title="@string/menu_download_label" />
</menu>

View File

@@ -240,6 +240,15 @@
<item>8</item>
</string-array>
<string-array name="playlist_sort_option_titles">
<item>Por nombre</item>
<item>Aleatoriamente</item>
</string-array>
<string-array name="playlist_sort_option_values">
<item>ORDER_BY_NAME</item>
<item>ORDER_BY_RANDOM</item>
</string-array>
<string-array name="skip_min_star_rating_titles">
<item>0 estrellas como mínimo</item>
<item>1 estrella como mínimo</item>

View File

@@ -3,7 +3,7 @@
<string name="album_page_tracks_count_and_duration">%1$d pistas • %2$d minutos</string>
<string name="app_name">Tempus</string>
<string name="activity_battery_optimizations_conclusion">Si tienes problemas, visita https://dontkillmyapp.com. Ofrece instrucciones detalladas para desactivar características de ahorro de energía que podrían afectar al rendimiento de la app.</string>
<string name="activity_battery_optimizations_summary">Por favor, desactive las optimizaciones de batería para continuar la reproducción multimedia mientras la pantalla está apagada.</string>
<string name="activity_battery_optimizations_summary">Por favor, desactiva las optimizaciones de batería para continuar la reproducción multimedia mientras la pantalla está apagada.</string>
<string name="activity_battery_optimizations_title">Optimizaciones de batería</string>
<string name="activity_info_offline_mode">Modo sin conexión</string>
<string name="album_bottom_sheet_add_to_playlist">Añadir a la lista de reproducción</string>
@@ -40,6 +40,7 @@
<string name="artist_list_page_downloaded">Artistas descargados</string>
<string name="artist_list_page_starred">Artistas destacados</string>
<string name="artist_list_page_title">Artistas</string>
<string name="artist_no_artist_info_toast">No hay más información del artista</string>
<string name="artist_page_radio_button">Radio</string>
<string name="artist_page_shuffle_button">Aleatorio</string>
<string name="artist_page_switch_layout_button">Cambiar disposición</string>
@@ -52,6 +53,7 @@
<string name="battery_optimization_negative_button">Ignorar</string>
<string name="battery_optimization_neutral_button">No volver a preguntar</string>
<string name="battery_optimization_positive_button">Desactivar</string>
<string name="bottom_sheet_problem_generating_instant_mix">No se pudieron obtener las pistas del servidor.</string>
<string name="connection_alert_dialog_negative_button">Cancelar</string>
<string name="connection_alert_dialog_neutral_button">Habilitar el ahorro de datos</string>
<string name="connection_alert_dialog_positive_button">Aceptar</string>
@@ -60,7 +62,7 @@
<string name="content_description_shuffle_button">Aleatorio</string>
<string name="delete_download_storage_dialog_negative_button">Cancelar</string>
<string name="delete_download_storage_dialog_positive_button">Continuar</string>
<string name="delete_download_storage_dialog_summary">Por favor, sea consciente de que si continúa, todos los elementos descargados de todos los servidores se eliminarán.</string>
<string name="delete_download_storage_dialog_summary">Por favor, ten en cuenta que si continúas, todos los elementos descargados de todos los servidores se eliminarán.</string>
<string name="delete_download_storage_dialog_title">Eliminar elementos guardados</string>
<string name="description_empty_title">Letra no disponible</string>
<string name="disc_titlefull">Disco %1$s - %2$s</string>
@@ -111,12 +113,12 @@
<string name="home_rearrangement_dialog_neutral_button">Restablecer</string>
<string name="home_rearrangement_dialog_positive_button">Guardar</string>
<string name="home_rearrangement_dialog_title">Reorganizar la página de inicio</string>
<string name="home_rearrangement_dialog_subtitle">Tenga en cuenta que para que los cambios surtan efecto, hay que reiniciar la aplicación.</string>
<string name="home_rearrangement_dialog_subtitle">Ten en cuenta que para que los cambios surtan efecto, hay que reiniciar la aplicación.</string>
<string name="home_section_music">Música</string>
<string name="home_section_podcast">Pódcasts</string>
<string name="home_section_radio">Radio</string>
<string name="home_subtitle_best_of">Mejores pistas de tus artistas favoritos</string>
<string name="home_subtitle_made_for_you">Iniciar mix desde una cación que te gustó</string>
<string name="home_subtitle_made_for_you">Iniciar mix desde una canción que te gustó</string>
<string name="home_subtitle_new_internet_radio_station">Añadir una nueva emisora de radio</string>
<string name="home_subtitle_new_podcast_channel">Añadir un nuevo canal de pódcasts</string>
<string name="home_sync_starred_cancel">Cancelar</string>
@@ -225,6 +227,8 @@
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
<string name="playlist_chooser_dialog_visibility_public">Público</string>
<string name="playlist_chooser_dialog_visibility_private">Privado</string>
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
<string name="playlist_duration">Duración • %1$s</string>
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
@@ -246,6 +250,7 @@
<string name="podcast_channel_catalogue_title_expanded">Explorar canales</string>
<string name="podcast_channel_editor_dialog_hint_rss_url">URL del feed RSS</string>
<string name="podcast_channel_editor_dialog_title">Canal del pódcast</string>
<string name="podcast_channel_not_supported_snackbar">Este servidor no soporta pódcasts.</string>
<string name="podcast_channel_page_title_description_section">Descripción</string>
<string name="podcast_channel_page_title_episode_section">Episodios</string>
<string name="podcast_channel_page_title_no_episode_available">No hay episodios disponibles</string>
@@ -260,10 +265,12 @@
<string name="radio_editor_dialog_negative_button">Cancelar</string>
<string name="radio_editor_dialog_neutral_button">Eliminar</string>
<string name="radio_editor_dialog_positive_button">Guardar</string>
<string name="radio_editor_dialog_title">"Emisora "</string>
<string name="radio_editor_dialog_updated">Emisora de radio actualizada</string>
<string name="radio_editor_dialog_title">Emisora</string>
<string name="radio_station_info_empty_button">Pulsa para ocultar la sección\nLos cambios serán visibles al reiniciar la app</string>
<string name="radio_station_info_empty_subtitle">Una vez que añadas una emisora de radio, la encontrarás aquí</string>
<string name="radio_station_info_empty_title">No hay emisoras de radio</string>
<string name="radio_dialog_not_supported_snackbar">Este servidor no soporta emisoras de radio en Internet.</string>
<string name="rating_dialog_negative_button">Cancelar</string>
<string name="rating_dialog_positive_button">Guardar</string>
<string name="rating_dialog_title">Valorar</string>
@@ -289,34 +296,34 @@
<string name="server_unreachable_dialog_negative_button">Cancelar</string>
<string name="server_unreachable_dialog_neutral_button">Ir al inicio de sesión</string>
<string name="server_unreachable_dialog_positive_button">Continuar de todas formas</string>
<string name="server_unreachable_dialog_summary">El servidor no está disponible. Si decide continuar, este diálogo no aparecerá de nuevo durante una hora.</string>
<string name="server_unreachable_dialog_summary">El servidor no está disponible. Si decides continuar, este diálogo no aparecerá de nuevo durante una hora.</string>
<string name="server_unreachable_dialog_title">No se puede conectar con el servidor</string>
<string name="settings_about_summary">Tempus es un cliente de música Subsonic ligero y de código abierto, diseñado nativamente para Android.</string>
<string name="settings_about_title">Acerca de</string>
<string name="settings_always_on_display">Pantalla siempre activa</string>
<string name="settings_allow_playlist_duplicates_summary">Si está habilitada, no se comprobará si hay pistas repetidas cuando se añadan a la lista.</string>
<string name="settings_allow_playlist_duplicates_summary">Si se habilita, no se comprobará si hay pistas repetidas cuando se añadan a la lista.</string>
<string name="settings_audio_transcode_download_format">Formato de transcodificación</string>
<string name="settings_audio_transcode_download_priority_summary">Si está habilitada, Tempus no descargará la pista con las opciones de transcodificación que aparecen a continuación.</string>
<string name="settings_audio_transcode_download_priority_summary">Si se habilita, Tempus no descargará la pista con las opciones de transcodificación que aparecen a continuación.</string>
<string name="settings_audio_transcode_download_priority_title">Dar prioridad a las opciones del servidor usadas para el streaming en las descargas</string>
<string name="settings_audio_transcode_download_summary">Si está habilitada, Tempus descargará las pistas transcodificadas.</string>
<string name="settings_audio_transcode_download_summary">Si se habilita, Tempus descargará las pistas transcodificadas.</string>
<string name="settings_audio_transcode_download_title">Descargas pistas transcodificadas</string>
<string name="settings_audio_transcode_estimate_content_length_summary">Si está habilitada, se pedirá al servidor la duración estimada de la pista.</string>
<string name="settings_audio_transcode_estimate_content_length_summary">Si se habilita, se pedirá al servidor la duración estimada de la pista.</string>
<string name="settings_audio_transcode_estimate_content_length_title">Calcular la duración del contenido</string>
<string name="settings_audio_transcode_format_download">Formato de transcodificación para las descargas</string>
<string name="settings_audio_transcode_format_mobile">Formato de transcodificación en red de datos móviles</string>
<string name="settings_audio_transcode_format_wifi">Formato de transcodificación en red Wi-Fi</string>
<string name="settings_audio_transcode_priority_summary">Si está habilitada, Tempus no reproducirá la pista con las opciones de transcodificación que aparecen a continuación.</string>
<string name="settings_audio_transcode_priority_summary">Si se habilita, Tempus no reproducirá la pista con las opciones de transcodificación que aparecen a continuación.</string>
<string name="settings_audio_transcode_priority_title">Dar prioridad a las opciones de transcodificación del servidor</string>
<string name="settings_audio_transcode_priority_toast">Prioridad a la hora de transcodificar una pista</string>
<string name="settings_buffering_strategy">Estrategia de buffer</string>
<string name="settings_buffering_strategy_summary">Para que los cambios surtan efecto, debes reinciar la app.</string>
<string name="settings_buffering_strategy_summary">Para que los cambios surtan efecto, debes reiniciar la app.</string>
<string name="settings_choose_download_folder">Elige una carpeta para descargar los archivos de música</string>
<string name="settings_clear_download_folder">Limpiar la carpeta de descargas</string>
<string name="settings_continuous_play_summary">Permite que la música siga reproduciéndose una vez que la lista de reproducción ha terminado, reproduciendo pistas similares</string>
<string name="settings_continuous_play_title">Reproducción continua</string>
<string name="settings_covers_cache">Tamaño de la caché de portadas de álbumes</string>
<string name="settings_data_saving_mode_summary">Evitar descargar las portadas de álbumes para reducir el uso de datos</string>
<string name="settings_data_saving_mode_title">Limitr el uso de datos móviles</string>
<string name="settings_data_saving_mode_title">Limitar el uso de datos móviles</string>
<string name="settings_delete_download_storage_summary">Al continuar se eliminarán de forma irreversible todos los elementos guardados.</string>
<string name="settings_delete_download_storage_title">Eliminar elementos guardados</string>
<string name="settings_download_storage_title">Almacenamiento de descargas</string>
@@ -325,21 +332,22 @@
<string name="settings_max_bitrate_wifi">Tasa de bits en Wi-Fi</string>
<string name="settings_media_cache">Tamaño de la caché multimedia</string>
<string name="settings_music_directory">Mostrar carpetas de música</string>
<string name="settings_music_directory_summary">Si está habilitada, se mostrará la sección de carpetas de música. Tenga en cuenta que para que la navegación funcione correctamente, el servidor debe soportar esta característica.</string>
<string name="settings_music_directory_summary">Si se habilita, se mostrará la sección de carpetas de música. Tenga en cuenta que para que la navegación funcione correctamente, el servidor debe soportar esta característica.</string>
<string name="settings_podcast">Mostrar pódcasts</string>
<string name="settings_podcast_summary">Si está habilitada, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
<string name="settings_podcast_summary">Si se habilita, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
<string name="settings_playlist_sort">Ordenar listas de reproducción</string>
<string name="settings_audio_quality">Mostrar calidad de audio</string>
<string name="settings_audio_quality_summary">La tasa de bits y el formato de audio se mostrarán para cada pista de audio.</string>
<string name="settings_song_rating_summary">Si está habilitada, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
<string name="settings_song_rating_summary">Si se habilita, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
<string name="settings_item_rating">Mostrar valoración de los elementos</string>
<string name="settings_queue_syncing_title">Sincronizar cola de reproducción para este usuario</string>
<string name="settings_show_mini_shuffle_button_summary">Si está habilitada, muestra el botón de reproducción aleatoria y oculta el botón de «Favoritos» en el minirreproductor</string>
<string name="settings_show_mini_shuffle_button_summary">Si se habilita, muestra el botón de reproducción aleatoria y oculta el botón de «Favoritos» en el minirreproductor.</string>
<string name="settings_radio">Mostrar emisoras de radio</string>
<string name="settings_auto_download_lyrics_summary">Descargar las letras automáticamente cuando estén disponibles para que se puedan mostrar cuando no hay conexión.</string>
<string name="settings_replay_gain">Configurar el modo de ganancia de reproducción</string>
<string name="settings_rounded_corner">Esquinas redondeadas</string>
<string name="settings_rounded_corner_size">Tamaño de las esquinas</string>
<string name="settings_rounded_corner_summary">Si está habilitada, establece un ángulo de curvatura para todas las portadas de álbumes. Los cambios se aplicarán después de reiniciar la app.</string>
<string name="settings_rounded_corner_summary">Si se habilita, establece un ángulo de curvatura para todas las portadas de álbumes. Los cambios se aplicarán después de reiniciar la app.</string>
<string name="settings_scan_title">Escanear biblioteca</string>
<string name="streaming_cache_storage_dialog_summary">Cambiar la ubicación de los archivos en caché a otro almacenamiento puede causar el borrado de todos los archivos en caché en el anterior almacenamiento.</string>
<string name="streaming_cache_storage_dialog_title">Seleccióna un tipo de almacenamiento</string>
@@ -368,10 +376,10 @@
<string name="settings_summary_streaming_cache_size">%1$s\nEn uso: %2$s MiB</string>
<string name="undraw_url">https://undraw.co/</string>
<string name="track_info_track_number">Número de pista</string>
<string name="settings_item_rating_summary">Si está habilitada, se mostrará la valoración del elemento y si está marcado como favorito.</string>
<string name="settings_item_rating_summary">Si se habilita, se mostrará la valoración del elemento y si está marcado como favorito.</string>
<string name="settings_queue_syncing_countdown">Temporizador de sincronización</string>
<string name="settings_queue_syncing_summary">Si está habilitada, el usuario podrá guardar la cola de reproducción y restaurarla cuando abra la aplicación.</string>
<string name="settings_radio_summary">Si está habilitada, se mostrará la sección de emisoras de radio. Reinicia la app para que los cambios surtan efecto.</string>
<string name="settings_queue_syncing_summary">Si se habilita, el usuario podrá guardar la cola de reproducción y restaurarla cuando abra la aplicación.</string>
<string name="settings_radio_summary">Si se habilita, se mostrará la sección de emisoras de radio. Reinicia la app para que los cambios surtan efecto.</string>
<string name="settings_rounded_corner_size_summary">Establece la proporción del ángulo de curvatura.</string>
<string name="track_info_dialog_title">Información de la pista</string>
<string name="track_info_disc_number">Número de disco</string>
@@ -443,10 +451,10 @@
<string name="starred_sync_dialog_title">Sincronizar las pistas destacadas</string>
<string name="starred_album_sync_dialog_title">Sincronizar álbumes favoritos</string>
<string name="streaming_cache_storage_dialog_sub_summary">Para que los cambios tengan efecto, reinicia la app.</string>
<string name="settings_summary_transcoding_download">Descarga los archivos multimedia transcodificados. Si esta opción está habilitada, no se usará el endpoint de descarga, sino las siguientes opciones.\n\nSi el formato de transcodificación para las descargas se establece en \"Descarga directa\", no se modificará la tasa de bits del archivo.</string>
<string name="settings_summary_transcoding_download">Descarga los archivos multimedia transcodificados. Si se habilita, no se usará el endpoint de descarga, sino las siguientes opciones.\n\nSi el formato de transcodificación para las descargas se establece en \"Descarga directa\", no se modificará la tasa de bits del archivo.</string>
<string name="settings_summary_transcoding_estimate_content_length">Cuando el archivo se transcodifica en tiempo real, el cliente normalmente no muestra la duración de la pista. Es posible solicitar a los servidores que soporten esta característica, que calculen la duración de la pista que se está reproduciendo, pero los tiempos de respuesta podrían aumentar.</string>
<string name="settings_sync_starred_albums_for_offline_use_title">Sincronizar álbumes favoritos para uso sin conexión</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si está habilitada, las pistas destacadas se descargarán para uso sin conexión.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si se habilita, las pistas destacadas se descargarán para uso sin conexión.</string>
<string name="track_info_summary_downloaded_file">El archivo se ha descargado usando las APIs de Subsonic. El códec y la tasa de bits del archivo se mantienen sin cambios respecto al archivo de origen.</string>
<string name="track_info_summary_full_transcode">La aplicación pedirá al servidor transcodificar el archivo y modificar su tasa de bits. El códec pedido por el usuario es %1$s, con una tasa de bits de %2$s. Cualquier cambio en el códec y tasa de bits del archivo en el formato elegido será manejado por el servidor, que puede, o no, soportar esta operación.</string>
<string name="track_info_summary_original_file">La aplicación solo leerá el archivo original ofrecido por el servidor. La app pedirá al servidor, de forma explícita, el archivo sin transcodificar con la tasa de bits de origen.</string>
@@ -456,7 +464,7 @@
<string name="settings_song_rating">Mostrar valoración de las pistas</string>
<string name="home_sync_starred_albums_title">Sincronizar álbumes favoritos</string>
<string name="settings_sync_starred_artists_for_offline_use_title">Sincronizar artistas destacados para uso sin conexión</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">Si está habilitada, los álbumes favoritos se descargarán para uso sin conexión.</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">Si se habilita, los álbumes favoritos se descargarán para uso sin conexión.</string>
<string name="starred_artist_sync_dialog_title">Sincronizar artistas destacados</string>
<string name="starred_album_sync_dialog_summary">Descargar los álbumes favoritos puede consumir una gran cantidad de datos.</string>
<string name="equalizer_fragment_title">Ecualizador</string>
@@ -479,6 +487,7 @@
<string name="widget_content_desc_prev">Pista anterior</string>
<string name="download_refresh_no_directory">Establece una carpeta de descarga para actualizar tus descargas</string>
<string name="home_sync_starred_artists_title">Sincronizar artistas destacados</string>
<string name="player_queue_load_queue">Cargar cola de reproducción</string>
<string name="player_lyrics_download_content_description">Descargar letras para uso sin conexión</string>
<string name="player_lyrics_downloaded_content_description">Letras descargadas para uso sin conexión</string>
<string name="player_lyrics_download_success">Letra guardada para uso sin conexión</string>
@@ -488,13 +497,13 @@
<string name="settings_show_mini_shuffle_button">Mostrar el botón «Aleatorio»</string>
<string name="settings_auto_download_lyrics">Descargar automáticamente las letras</string>
<string name="starred_artist_sync_dialog_summary">Descargar los artistas destacados podría consumir una gran cantidad de datos.</string>
<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="settings_sync_starred_artists_for_offline_use_summary">Si se habilita, 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="settings_album_detail_summary">Si se habilita, 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>
@@ -509,6 +518,14 @@
<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>
<string name="settings_github_update">Actualizaciones</string>
<string name="bottom_sheet_generating_instant_mix">Generando mix instantáneo…</string>
<string name="player_queue_save_to_playlist">Guardar cola en una lista de reproducción</string>
<string name="radio_editor_dialog_added">Emisora de radio añadida</string>
<string name="settings_artist_sort_by_album_count">Ordenar artistas por número de álbumes</string>
<string name="settings_artist_sort_by_album_count_summary">Ordena los artistas por número de álbumes si la opción está habilitada. Si no, los ordena por nombre.</string>
<string name="settings_artist_sort_by_album_count_summary">Si se habilita, ordena los artistas por número de álbumes. En caso contrario, se ordenan por nombre.</string>
<string name="folder_play_collecting">Obteniendo pistas de la carpeta…</string>
<string name="folder_play_playing">Reproduciendo %d pistas</string>
<string name="folder_play_no_songs">No se encontraron pistas en la carpeta</string>
<string name="search_sort_title">Ordenar las búsquedas recientes cronológicamente</string>
<string name="search_sort_summary">Si se habilita, se ordenan las búsquedas en orden cronológico. En caso contrario, se ordenan por nombre.</string>
</resources>

View File

@@ -39,6 +39,7 @@
<string name="artist_list_page_downloaded">Artistes téléchargés</string>
<string name="artist_list_page_starred">Artistes favoris</string>
<string name="artist_list_page_title">Artistes</string>
<string name="artist_no_artist_info_toast">Pas d\'autre information sur l\'artiste</string>
<string name="artist_page_radio_button">Radio</string>
<string name="artist_page_shuffle_button">Mélanger</string>
<string name="artist_page_switch_layout_button">Changer la disposition</string>
@@ -51,6 +52,8 @@
<string name="battery_optimization_negative_button">Ignorer</string>
<string name="battery_optimization_neutral_button">Ne pas me redemander</string>
<string name="battery_optimization_positive_button">Désactiver</string>
<string name="bottom_sheet_generating_instant_mix">Génération du mix instantané…</string>
<string name="bottom_sheet_problem_generating_instant_mix">Echec de récupération de pistes du serveur Subsonic.</string>
<string name="connection_alert_dialog_negative_button">Annuler</string>
<string name="connection_alert_dialog_neutral_button">Activer l\'économie de données</string>
<string name="connection_alert_dialog_positive_button">OK</string>
@@ -68,6 +71,7 @@
<string name="download_directory_dialog_positive_button">Télécharger</string>
<string name="download_directory_dialog_summary">Toutes les pistes dans ce dossier seront téléchargées. Les pistes dans les sous-dossiers ne seront pas téléchargées.</string>
<string name="download_directory_dialog_title">Télécharger toutes les pistes.</string>
<string name="download_directory_set">Répertoire de téléchargement de la musique</string>
<string name="download_info_empty_subtitle">Dès que vous téléchargerez une musique, vous la trouverez ici</string>
<string name="download_info_empty_title">Aucun téléchargement pour l\'instant</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s éléments</string>
@@ -78,7 +82,15 @@
<string name="download_storage_dialog_title">Sélectionnez l\'option de stockage</string>
<string name="download_storage_external_dialog_positive_button">Externe</string>
<string name="download_storage_internal_dialog_negative_button">Interne</string>
<string name="download_storage_directory_dialog_neutral_button">Répertoire</string>
<string name="download_title_section">Téléchargements</string>
<string name="download_refresh_no_directory">Sélectionner un répertoire pour rafraîchir vos téléchargements.</string>
<string name="download_refresh_no_changes">Aucun téléchargement manquant détecté.</string>
<plurals name="download_refresh_removed">
<item quantity="one">%d téléchargement manquant retiré.</item>
<item quantity="other">%d téléchargements manquants retirés.</item>
</plurals>
<string name="download_refresh_button_content_description">Rafraîchir les téléchargements</string>
<string name="downloaded_bottom_sheet_add_to_queue">Ajouter à la liste d\'attente</string>
<string name="downloaded_bottom_sheet_play_next">Lire juste après</string>
<string name="downloaded_bottom_sheet_remove">Retirer</string>
@@ -88,6 +100,9 @@
<string name="error_required">Requis</string>
<string name="error_server_prefix">préfixe http ou https requis</string>
<string name="exo_download_notification_channel_name">Téléchargements</string>
<string name="exo_controls_heart_off_description">Désactiver les boutons Cœur</string>
<string name="exo_controls_heart_on_description">Activer les boutons Cœur</string>
<string name="cast_expanded_controller_loading">Chargement…</string>
<string name="filter_info_selection">Sélectionnez deux filtres ou plus</string>
<string name="filter_title">Filtrer</string>
<string name="filter_artist">Filtrer par artiste</string>
@@ -118,7 +133,13 @@
<string name="home_sync_starred_subtitle">Télécharger ces titres peut entraîner une utilisation importante de données</string>
<string name="home_sync_starred_title">On dirait qu\'il y a des titres favoris à synchroniser</string>
<string name="home_sync_starred_albums_title">Synchroniser les albums favoris</string>
<string name="home_sync_starred_albums_subtitle">Les albums marqués d\'une étoile seront disponibles hors-ligne</string>
<string name="home_sync_starred_albums_subtitle">Les albums favoris seront disponibles hors-ligne</string>
<string name="home_sync_starred_artists_title">Synchroniser les artistes favoris</string>
<string name="home_sync_starred_artists_subtitle">Pour certains de vos artistes favoris, il reste des titres à télécharger</string>
<plurals name="home_sync_starred_songs_count">
<item quantity="one">%d titre doit être synchronisé</item>
<item quantity="other">%d titres doivent être synchronisés</item>
</plurals>
<string name="home_title_best_of">Best of</string>
<string name="home_title_discovery">Découverte</string>
<string name="home_title_discovery_shuffle_all_button">Tout mélanger</string>
@@ -184,6 +205,7 @@
<string name="menu_sort_artist">Artiste</string>
<string name="menu_sort_name">Nom</string>
<string name="menu_sort_random">Aléatoire</string>
<string name="menu_sort_album_count">Nombre d\'albums</string>
<string name="menu_sort_recently_added">Récemment ajoutés</string>
<string name="menu_sort_recently_played">Récemment lus</string>
<string name="menu_sort_most_played">Plus lus</string>
@@ -195,6 +217,12 @@
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">Vider la file d\'attente</string>
<string name="player_queue_save_queue_success">File d\'attente sauvegardée</string>
<string name="player_queue_save_to_playlist">Sauvegarder la file d\'attente dans une playlist</string>
<string name="player_queue_load_queue">Charger la file d\'attente</string>
<string name="player_lyrics_download_content_description">Télécharger les paroles pour lecture hors-ligne</string>
<string name="player_lyrics_downloaded_content_description">Paroles téléchargées pour lecture hors-ligne</string>
<string name="player_lyrics_download_success">Paroles sauvegardées pour lecture hors-ligne.</string>
<string name="player_lyrics_download_failure">Impossible de télécharger les paroles.</string>
<string name="player_server_priority">Priorité serveur</string>
<string name="player_unknown_format">Format inconnu</string>
<string name="player_transcoding">Transcodage</string>
@@ -207,6 +235,9 @@
<string name="playlist_chooser_dialog_title">Ajouter à une playlist</string>
<string name="playlist_chooser_dialog_toast_add_success">Titre ajouté à la playlist</string>
<string name="playlist_chooser_dialog_toast_add_failure">Échec d\'ajout du titre à la playlist</string>
<string name="playlist_chooser_dialog_toast_all_skipped">Tous les titres ont été traités comme des doublons et ignorés</string>
<string name="playlist_chooser_dialog_visibility_public">Publique</string>
<string name="playlist_chooser_dialog_visibility_private">Privé</string>
<string name="playlist_counted_tracks">%1$d titres • %2$s</string>
<string name="playlist_duration">Durée • %1$s</string>
<string name="playlist_editor_dialog_action_delete_toast">Appui long pour supprimer</string>
@@ -228,13 +259,14 @@
<string name="podcast_channel_catalogue_title_expanded">Parcourir les chaînes</string>
<string name="podcast_channel_editor_dialog_hint_rss_url">Url RSS</string>
<string name="podcast_channel_editor_dialog_title">Chaîne</string>
<string name="podcast_channel_not_supported_snackbar">Ce serveur ne supporte pas les podcasts.</string>
<string name="podcast_channel_page_title_description_section">Description</string>
<string name="podcast_channel_page_title_episode_section">Épisodes</string>
<string name="podcast_channel_page_title_no_episode_available">Aucun épisode disponible</string>
<string name="podcast_episode_download_request_snackbar">Votre requête a été envoyée au serveur</string>
<string name="podcast_info_empty_button">Cliquez pour cacher la section\nLes changements seront visibles au redémarrage de l\'app</string>
<string name="podcast_info_empty_subtitle">Dès que vous ajouterez une chaîne, vous la retrouverez ici</string>
<string name="podcast_info_empty_title">Aucun podcast trouvé!</string>
<string name="podcast_info_empty_title">Aucun podcast trouvé !</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="radio_editor_dialog_hint_homepage_url">URL de la page d\'accueil de la radio</string>
<string name="radio_editor_dialog_hint_name">Nom de la radio</string>
@@ -242,10 +274,13 @@
<string name="radio_editor_dialog_negative_button">Annuler</string>
<string name="radio_editor_dialog_neutral_button">Supprimer</string>
<string name="radio_editor_dialog_positive_button">Enregistrer</string>
<string name="radio_editor_dialog_title">Station Radio Internet</string>
<string name="radio_editor_dialog_added">Station de radio ajoutée</string>
<string name="radio_editor_dialog_updated">Station de radio modifiée</string>
<string name="radio_editor_dialog_title">Station de radio Internet</string>
<string name="radio_station_info_empty_button">Cliquez pour cacher la section\nLes changements seront visibles au redémarrage de l\'app</string>
<string name="radio_station_info_empty_subtitle">Dès que vous ajouterez une station radio, vous la retrouverez ici</string>
<string name="radio_station_info_empty_title">Aucune station trouvée!</string>
<string name="radio_station_info_empty_title">Aucune station trouvée !</string>
<string name="radio_dialog_not_supported_snackbar">Ce serveur ne supporte pas la gestion de radios Internet.</string>
<string name="rating_dialog_negative_button">Annuler</string>
<string name="rating_dialog_positive_button">Enregistrer</string>
<string name="rating_dialog_title">Noter</string>
@@ -273,6 +308,8 @@
<string name="settings_about_summary">Tempus est un client open source et léger pour Subsonic, développé et compilé nativement pour Android.</string>
<string name="settings_about_title">À propos</string>
<string name="settings_always_on_display">Toujours visible</string>
<string name="settings_allow_playlist_duplicates">Autoriser l\'ajout de doublons à une playlist</string>
<string name="settings_allow_playlist_duplicates_summary">Si activé, les doublons ne seront pas détectés à l\'ajout d\'un titre à une playlist.</string>
<string name="settings_audio_transcode_download_format">Format de transcodage</string>
<string name="settings_audio_transcode_download_priority_summary">Si activé, Tempus ne forcera pas le téléchargement de la piste avec les paramètres de transcodage ci-dessous.</string>
<string name="settings_audio_transcode_download_priority_title">Prioriser les paramètres du serveurs, utilisés pour le streaming, dans les téléchargements</string>
@@ -288,6 +325,8 @@
<string name="settings_audio_transcode_priority_toast">La priorité au transcodage de la piste est donnée au serveur</string>
<string name="settings_buffering_strategy">Stratégie de mise en mémoire tampon</string>
<string name="settings_buffering_strategy_summary">Redémarrez l\'application pour appliquer les changements.</string>
<string name="settings_choose_download_folder">Sélectionner un répertoire pour le téléchargement des fichiers de musique</string>
<string name="settings_clear_download_folder">Vider le répertoire de téléchargement</string>
<string name="settings_continuous_play_summary">Permet de prolonger la lecture après la fin d\'une playlist avec des titres similaires</string>
<string name="settings_continuous_play_title">Lecture continue</string>
<string name="settings_covers_cache">Taille du cache des illustrations</string>
@@ -296,16 +335,26 @@
<string name="settings_delete_download_storage_summary">Continuer entraînera la suppression irréversible de tous les éléments sauvegardés.</string>
<string name="settings_delete_download_storage_title">Supprimer les éléments sauvegardés</string>
<string name="settings_download_storage_title">Stockage des téléchargements</string>
<string name="settings_system_equalizer_summary">Ajuster les paramètres audios</string>
<string name="settings_download_folder_cleared">Répertoire de téléchargement vidé.</string>
<string name="settings_download_folder_set">Répertoire de téléchargement réglé</string>
<string name="settings_set_download_folder">Régler le répertoire de téléchargement</string>
<string name="settings_system_equalizer_summary">Ajuster les paramètres audio</string>
<string name="settings_system_equalizer_title">Égaliseur du système</string>
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
<string name="settings_github_summary">Suivre le développement</string>
<string name="settings_github_title">Github</string>
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</string>
<string name="settings_github_update">Mises à jour</string>
<string name="settings_github_update_title">Vérifier les mises à jour sur Github</string>
<string name="settings_github_update_summary">Si vous utilisez l\'app publiée sur Github, elle vérifiera par défaut les sorties de nouvelles versions. Cliquer ici pour désactiver les vérifications automatiques</string>
<string name="settings_support_summary">Rejoindre les discussions et le support de la communauté</string>
<string name="settings_support_title">Support utilisateur</string>
<string name="settings_scan_result">Analyse : comptage de %1$d pistes</string>
<string name="settings_image_size">Définir la résolution des images</string>
<string name="settings_language">Langue</string>
<string name="settings_logout_title">Se déconnecter</string>
<string name="settings_max_bitrate_download">Débit binaire pour les téléchargements</string>
<string name="settings_max_bitrate_mobile">Débit binaire en données mobile</string>
<string name="settings_max_bitrate_mobile">Débit binaire en données mobiles</string>
<string name="settings_max_bitrate_wifi">Débit binaire en Wi-Fi</string>
<string name="settings_media_cache">Taille du cache des fichiers audio</string>
<string name="settings_music_directory">Afficher les dossiers</string>
@@ -320,14 +369,18 @@
<string name="settings_item_rating_summary">Si activé, la note et le statut de mise en favori de l\'élément seront affichés.</string>
<string name="settings_queue_syncing_countdown">Minuteur de synchronisation</string>
<string name="settings_queue_syncing_summary">Si activé, l\'utilisateur pourra sauvegarder sa file d\'attente et la recharger au démarrage de l\'application.</string>
<string name="settings_queue_syncing_title">Synchroniser la file d\'attente pour cet utilisateur</string>
<string name="settings_queue_syncing_title">Synchroniser la file d\'attente pour cet utilisateur [fonctionnalité en évolution]</string>
<string name="settings_show_mini_shuffle_button">Afficher le bouton Mélanger</string>
<string name="settings_show_mini_shuffle_button_summary">Si activé, le bouton Mélanger sera visible et le bouton Cœur caché sur le mini-lecteur</string>
<string name="settings_radio">Voir les radios</string>
<string name="settings_radio_summary">Si activé, rend visible la section Radio</string>
<string name="settings_radio_summary">Si activé, rend visible la section Radio. Redémarrez l\'application pour appliquer ce paramètre.</string>
<string name="settings_auto_download_lyrics">Téléchargement automatique des paroles</string>
<string name="settings_auto_download_lyrics_summary">Ce paramètre active la sauvegarde automatique des paroles pour affichage hors-ligne, si elles sont disponibles.</string>
<string name="settings_replay_gain">Ajuster le Replay Gain</string>
<string name="settings_rounded_corner">Coins arrondis</string>
<string name="settings_rounded_corner_size">Taille des arrondis</string>
<string name="settings_rounded_corner_size_summary">Définit l\'ampleur de l\'angle de courbure.</string>
<string name="settings_rounded_corner_summary">Si activé, arrondi les angles des illustrations. Les modifications prendront effet au redémarrage.</string>
<string name="settings_rounded_corner_summary">Si activé, arrondit les angles des illustrations. Les modifications prendront effet au redémarrage.</string>
<string name="settings_scan_title">Scanner la bibliothèque</string>
<string name="settings_scrobble_title">Activer le scrobbling</string>
<string name="settings_system_language">Langue du système</string>
@@ -344,13 +397,16 @@
<string name="settings_summary_transcoding">Le mode de transcodage à prioriser. Si réglé sur \"Lecture directe\", le débit binaire du fichier ne sera pas modifié.</string>
<string name="settings_summary_transcoding_download">Télécharge les médias transcodés. Si activé, les paramètres de transcodage suivants seront utilisés pour les téléchargements.\n\n Si le format de transcodage est reglé à \"Téléchargement direct\", le débit binaire du fichier ne sera pas modifé.</string>
<string name="settings_summary_transcoding_estimate_content_length">Quand le fichier est transcodé à la volée, en général, le client n\'affiche pas la durée de la piste. Il est possible de demander aux serveurs qui le supportent d\'estimer la durée de la piste écoutée, mais les temps de réponses peuvent être plus longs.</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">Si activé, les albums favoris seront téléchargés pour l\'écoute hors-ligne</string>
<string name="settings_sync_starred_artists_for_offline_use_summary">Si activé, les titres des artistes favoris seront téléchargés pour l\'écoute hors-ligne.</string>
<string name="settings_sync_starred_artists_for_offline_use_title">Synchronisation des artistes favoris pour écoute hors-ligne</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">Si activé, les albums favoris seront téléchargés pour l\'écoute hors-ligne.</string>
<string name="settings_sync_starred_albums_for_offline_use_title">Synchronisation des albums favoris pour écoute hors-ligne</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si activé, les pistes favorites seront téléchargées pour l\'écoute hors-ligne</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si activé, les pistes favorites seront téléchargées pour l\'écoute hors-ligne.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Synchronisation des pistes favorites pour écoute hors-ligne</string>
<string name="settings_theme">Thème</string>
<string name="settings_title_data">Données</string>
<string name="settings_title_general">Géneral</string>
<string name="settings_title_playlist">Playlist</string>
<string name="settings_title_rating">Note</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_scrobble">Scrobble</string>
@@ -369,7 +425,24 @@
<string name="share_bottom_sheet_delete">Supprimer le partage</string>
<string name="share_bottom_sheet_update">Mettre à jour le partage</string>
<string name="share_subtitle_item">Date d\'expiration : %1$s</string>
<string name="share_no_expiration">Jamais</string>
<string name="share_unsupported_error">Le partage n\'est pas supporté ou pas activé</string>
<string name="asset_link_clipboard_label">Lien ressource Tempus</string>
<string name="asset_link_label_song">UID titre</string>
<string name="asset_link_label_album">UID album</string>
<string name="asset_link_label_artist">UID artiste</string>
<string name="asset_link_label_playlist">UID playlist</string>
<string name="asset_link_label_genre">UID genre</string>
<string name="asset_link_label_year">UID année</string>
<string name="asset_link_label_unknown">UID ressource</string>
<string name="asset_link_error_unsupported">Lien ressource non supportée</string>
<string name="asset_link_error_song">Echec d\'accès au titre</string>
<string name="asset_link_error_album">Echec d\'accès à l\'album</string>
<string name="asset_link_error_artist">Echec d\'accès à l\'artiste</string>
<string name="asset_link_error_playlist">Echec d\'accès à la playlist</string>
<string name="asset_link_chip_text">%1$s • %2$s</string>
<string name="asset_link_copied_toast">%1$s ressource(s) copiée(s) dans le presse-papier</string>
<string name="asset_link_debug_toast">Lien ressource : %1$s</string>
<string name="share_update_dialog_hint_description">Description</string>
<string name="share_update_dialog_hint_expiration_date">Date d\'expiration</string>
<string name="share_update_dialog_negative_button">Annuler</string>
@@ -400,7 +473,9 @@
<string name="starred_sync_dialog_positive_button">Continuer et télécharger</string>
<string name="starred_sync_dialog_summary">Le téléchargement des titres favoris pourrait consommer beaucoup de données.</string>
<string name="starred_sync_dialog_title">Synchroniser les titres favoris</string>
<string name="starred_album_sync_dialog_summary">Le téléchargement des titres favoris pourrait consommer beaucoup de données.</string>
<string name="starred_artist_sync_dialog_summary">Le téléchargement des titres d\'artistes favoris pourrait consommer beaucoup de données.</string>
<string name="starred_artist_sync_dialog_title">Synchroniser les artistes favoris</string>
<string name="starred_album_sync_dialog_summary">Le téléchargement des albums favoris pourrait consommer beaucoup de données.</string>
<string name="starred_album_sync_dialog_title">Synchroniser les albums favoris</string>
<string name="streaming_cache_storage_dialog_sub_summary">Veuillez redémarrer l\'app pour appliquer les changements.</string>
<string name="streaming_cache_storage_dialog_summary">Modifier le chemin de stockage des fichiers mis en cache risque de provoquer la suppression de tous les fichiers précédemment mis en cache dans le nouvel espace de stockage.</string>
@@ -436,14 +511,45 @@
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">Un grand merci à unDraw, nous n\'aurions pas pu rendre cette application aussi belle sans leurs illustrations.</string>
<string name="undraw_url">https://undraw.co/</string>
<string name="widget_label">Widget Tempus</string>
<string name="widget_not_playing">Pas de lecture</string>
<string name="widget_placeholder_subtitle">Ouvrir Tempus</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">Couverture de l\'album</string>
<string name="widget_content_desc_play_pause">Lecture ou pause</string>
<string name="widget_content_desc_next">Piste suivante</string>
<string name="widget_content_desc_prev">Piste précédente</string>
<string name="widget_content_desc_shuffle">Activer/désactiver le mélange</string>
<string name="widget_content_desc_repeat">Changer le mode de répétition</string>
<plurals name="home_sync_starred_albums_count">
<item quantity="one">%d album à synchroniser</item>
<item quantity="other">%d albums à synchroniser</item>
</plurals>
<plurals name="home_sync_starred_artists_count">
<item quantity="one">%d artiste à synchroniser</item>
<item quantity="other">%d artistes à synchroniser</item>
</plurals>
<plurals name="songs_download_started">
<item quantity="one">Téléchargement de %d titre</item>
<item quantity="other">Téléchargement de %d titres</item>
</plurals>
<string name="equalizer_fragment_title">Égaliseur</string>
<string name="equalizer_reset">Réinitialiser</string>
<string name="equalizer_enable">Activer</string>
<string name="equalizer_not_supported">Non supporté sur cet appareil</string>
<string name="settings_app_equalizer">Égaliseur</string>
<string name="settings_app_equalizer_summary">Ouvrir l\'égaliseur intégré</string>
<string name="settings_album_detail">Afficher les détails de l\'album</string>
<string name="settings_album_detail_summary">Si activé, affiche les détails de l\'album tels que le genre, le nombre de titres etc. sur l\'écran de l\'album</string>
<string name="settings_artist_sort_by_album_count">Trier les artistes par nombre d\'albums</string>
<string name="settings_artist_sort_by_album_count_summary">Si activé, les artistes seront triés par ordre des nombres d\'albums. Si désactivé, ils le seront par ordre alphabétique.</string>
<string name="folder_play_collecting">Collecte des titres du répertoire…</string>
<string name="folder_play_playing">Lecture de %d titres</string>
<string name="folder_play_no_songs">Aucun titre trouvé dans le répertoire</string>
<string name="search_sort_title">Trier les recherches récentes par ordre chronologique</string>
<string name="search_sort_summary">Si activé, les recherches récentes seront triées par ordre chronologique. Si désactivé, elles le seront par ordre alphabétique.</string>
</resources>

View File

@@ -229,6 +229,8 @@
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
<string name="playlist_chooser_dialog_visibility_public">Pubblico</string>
<string name="playlist_chooser_dialog_visibility_private">Privato</string>
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
<string name="playlist_duration">Durata • %1$s</string>
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>

View File

@@ -239,7 +239,16 @@
<item>4</item>
<item>8</item>
</string-array>
<string-array name="playlist_sort_option_titles">
<item>Po nazwie</item>
<item>Losowo</item>
</string-array>
<string-array name="playlist_sort_option_values">
<item>ORDER_BY_NAME</item>
<item>ORDER_BY_RANDOM</item>
</string-array>
<string-array name="skip_min_star_rating_titles">
<item>Minimum 0 gwiazdek</item>
<item>Minimum 1 gwiazdka</item>
@@ -254,4 +263,4 @@
<item>3</item>
<item>4</item>
</string-array>
</resources>
</resources>

View File

@@ -39,6 +39,7 @@
<string name="artist_list_page_downloaded">Pobrani wykonawcy</string>
<string name="artist_list_page_starred">Wykonawcy oznaczeni gwiazdką</string>
<string name="artist_list_page_title">Wykonawcy</string>
<string name="artist_no_artist_info_toast">Brak dodatkowych informacji o wykonawcy</string>
<string name="artist_page_radio_button">Radio</string>
<string name="artist_page_shuffle_button">Odtwarzanie losowe</string>
<string name="artist_page_switch_layout_button">Zmień układ</string>
@@ -227,6 +228,8 @@
<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ć piosenek do playlisty</string>
<string name="playlist_chooser_dialog_toast_all_skipped">Pominięto wszystkie piosenki jako duplikaty</string>
<string name="playlist_chooser_dialog_visibility_public">Publiczna</string>
<string name="playlist_chooser_dialog_visibility_private">Prywatna</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_editor_dialog_action_delete_toast">Przytrzymaj aby usunąć</string>
@@ -342,6 +345,9 @@
<string name="settings_image_size">Rozdzielczość obrazów</string>
<string name="settings_language">Język</string>
<string name="settings_logout_title">Wyloguj</string>
<string name="settings_ping_timeout_title">Timeout pingów serwera</string>
<string name="settings_ping_timeout_summary">Timeout lokalnego adresu URL. Domyślnie to 2 sekundy. (Serwer zdalny będzie używał trzykrotności tej wartości maksymalnie do 10 sekund.)</string>
<string name="settings_ping_timeout_dialog">Bazowy timeout w sekundach.</string>
<string name="settings_max_bitrate_download">Bitrate dla pobierania</string>
<string name="settings_max_bitrate_mobile">Bitrate dla danych komórkowych</string>
<string name="settings_max_bitrate_wifi">Bitrate dla Wi-Fi</string>
@@ -350,6 +356,7 @@
<string name="settings_music_directory_summary">Jeżeli włączone, widoczna będzie sekcja z folderami z muzyką. Weź pod uwagę że żeby funkcja nawigacji po folderach działała poprawnie, serwer musi wspierać tę funkcję.</string>
<string name="settings_podcast">Pokazuj podcasty</string>
<string name="settings_podcast_summary">Jeżeli włączone, widoczna będzie sekcja z podcastami. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.</string>
<string name="settings_playlist_sort">Sortowanie playlist</string>
<string name="settings_audio_quality">Pokaż jakość audio</string>
<string name="settings_audio_quality_summary">Bitrate i format audio będzie pokazywany dla każdego utworu.</string>
<string name="settings_song_rating">Pokaż ocenę piosenek w gwiazdkach</string>

View File

@@ -164,6 +164,8 @@
<string name="playlist_chooser_dialog_title">Adicionar a uma playlist</string>
<string name="playlist_chooser_dialog_toast_add_success">Adicionada playlist de reprodução</string>
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
<string name="playlist_chooser_dialog_visibility_public">Pública</string>
<string name="playlist_chooser_dialog_visibility_private">Privada</string>
<string name="playlist_counted_tracks">%1$d faixas • %2$s</string>
<string name="playlist_duration">Duração • %1$s</string>
<string name="playlist_editor_dialog_hint_name">Nome da playlist</string>

View File

@@ -240,6 +240,16 @@
<item>8</item>
</string-array>
<string-array name="playlist_sort_option_titles">
<item>Name</item>
<item>Random</item>
</string-array>
<string-array name="playlist_sort_option_values">
<item>ORDER_BY_NAME</item>
<item>ORDER_BY_RANDOM</item>
</string-array>
<string-array name="skip_min_star_rating_titles">
<item>0 star minimum</item>
<item>1 star minimum</item>
@@ -254,4 +264,18 @@
<item>3</item>
<item>4</item>
</string-array>
<string-array name="landscape_items_per_row">
<item>3"</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
</string-array>
<string-array name="landscape_items_per_row_values">
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
</string-array>
</resources>

View File

@@ -217,6 +217,8 @@
<string name="menu_unpin_button">Remove from home screen</string>
<string name="menu_sort_year">Year</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="playback_speed_dialog_title">Playback Speed</string>
<string name="playback_speed_dialog_negative_button">Cancel</string>
<string name="player_queue_clean_all_button">Clean play queue</string>
<string name="player_queue_save_queue_success">Saved play queue</string>
<string name="player_queue_save_to_playlist">Save Queue to Playlist</string>
@@ -238,6 +240,8 @@
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
<string name="playlist_chooser_dialog_visibility_public">Public</string>
<string name="playlist_chooser_dialog_visibility_private">Private</string>
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
<string name="playlist_duration">Duration • %1$s</string>
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
@@ -343,7 +347,7 @@
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
<string name="settings_github_summary">Follow the development</string>
<string name="settings_github_title">Github</string>
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</string>
<string name="settings_support_discussion_link" translatable="false">https://github.com/eddyizm/tempus/discussions</string>
<string name="settings_github_update">Updates</string>
<string name="settings_github_update_title">Check github for release updates</string>
<string name="settings_github_update_summary">If using the github version, by default app will check for new apk release. Toggle to disable automatic github checks</string>
@@ -353,6 +357,9 @@
<string name="settings_image_size">Set image resolution</string>
<string name="settings_language">Language</string>
<string name="settings_logout_title">Log out</string>
<string name="settings_ping_timeout_title">Server Ping Timeout</string>
<string name="settings_ping_timeout_summary">Set Local URL timeout. Default 2 seconds. (Remote server will use this value x3 up to 10 seconds max.)</string>
<string name="settings_ping_timeout_dialog">Set base timeout in seconds.</string>
<string name="settings_max_bitrate_download">Bitrate for downloads</string>
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
@@ -361,6 +368,7 @@
<string name="settings_music_directory_summary">If enabled, show the music directory section. Please note that for folder navigation to work properly, the server must support this feature.</string>
<string name="settings_podcast">Show podcast</string>
<string name="settings_podcast_summary">If enabled, show the podcast section. Restart the app for it to take full effect.</string>
<string name="settings_playlist_sort">Playlist sorting</string>
<string name="settings_audio_quality">Show audio quality</string>
<string name="settings_audio_quality_summary">The bitrate and audio format will be shown for each audio track.</string>
<string name="settings_song_rating">Show song star rating</string>
@@ -397,6 +405,7 @@
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for downloads\" is set to \"Direct download\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_estimate_content_length">When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer.</string>
<string name="settings_summary_landscape_items_per_row">Applies to all album and artist listings. Defaults to 4</string>
<string name="settings_sync_starred_artists_for_offline_use_summary">If enabled, starred artists will be downloaded for offline use.</string>
<string name="settings_sync_starred_artists_for_offline_use_title">Sync starred artists for offline use</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">If enabled, starred albums will be downloaded for offline use.</string>
@@ -417,6 +426,8 @@
<string name="settings_title_transcoding">Transcoding</string>
<string name="settings_title_transcoding_download">Transcoding Download</string>
<string name="settings_title_ui">UI</string>
<string name="settings_title_ui_landscape_items_per_row">Items per row on landscape</string>
<string name="settings_title_ui_landscape_items_per_row_dialog">Number of items per row</string>
<string name="settings_transcoded_download">Transcoded download</string>
<string name="settings_version_summary" translatable="false">3.1.0</string>
<string name="settings_version_title">Version</string>
@@ -483,7 +494,7 @@
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
<string name="support_url">https://ko-fi.com/eddyizm</string>
<string name="support_url" translatable="false">https://ko-fi.com/eddyizm</string>
<string name="track_info_album">Album</string>
<string name="track_info_artist">Artist</string>
<string name="track_info_bit_depth">Bit depth</string>
@@ -511,7 +522,7 @@
<string name="track_info_year">Year</string>
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
<string name="undraw_url">https://undraw.co/</string>
<string name="undraw_url" translatable="false">https://undraw.co/</string>
<string name="widget_label">Tempus Widget</string>
<string name="widget_not_playing">Not playing</string>
<string name="widget_placeholder_subtitle">Open Tempus</string>

View File

@@ -17,6 +17,15 @@
android:key="scan_library"
android:title="@string/settings_scan_title" />
<EditTextPreference
android:key="network_ping_timeout_base"
android:title="@string/settings_ping_timeout_title"
app:summary="@string/settings_ping_timeout_summary"
android:dialogTitle="@string/settings_ping_timeout_dialog"
android:inputType="number"
android:singleLine="true"
android:defaultValue="2" />
<Preference
android:key="logout"
android:title="@string/settings_logout_title"/>
@@ -128,6 +137,15 @@
android:summary="@string/search_sort_summary"
android:key="sort_search_chronologically" />
<ListPreference
app:defaultValue="4"
app:dialogTitle="@string/settings_title_ui_landscape_items_per_row_dialog"
app:entries="@array/landscape_items_per_row"
app:entryValues="@array/landscape_items_per_row_values"
app:key="landscape_items_per_row"
android:summary="@string/settings_summary_landscape_items_per_row"
app:title="@string/settings_title_ui_landscape_items_per_row" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_playlist">
@@ -136,6 +154,14 @@
android:defaultValue="false"
android:summary="@string/settings_allow_playlist_duplicates_summary"
android:key="allow_playlist_duplicates" />
<ListPreference
app:defaultValue="ORDER_BY_NAME"
app:dialogTitle="@string/settings_playlist_sort"
app:entries="@array/playlist_sort_option_titles"
app:entryValues="@array/playlist_sort_option_values"
app:key="home_sort_playlists"
app:title="@string/settings_playlist_sort"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>

View File

@@ -12,4 +12,5 @@
<locale android:name="pl-PL"/> <!-- Polish -->
<locale android:name="tr-TR"/> <!-- Turkish -->
<locale android:name="ca"/> <!-- Catalan -->
<locale android:name="ro"/> <!-- Romanian -->
</locale-config>

View File

@@ -0,0 +1,12 @@
chore: i18n: Add Romanian translation (including locale_config this time!)
chore: French localization update
chore(i18n): Update Spanish translation
docs: updated readme and added known issues for airsonic work around
fix: toast for made for you click indication
fix: sort playlist view
feat: sort preference for playlists
fix: use existing future when adding tracks, dialed random album tracks off in instant mix
chore(i18n): Update Polish translation
fix: Check for OpenSubsonic extensions also with password authentication, addressing lyric sync
feat: Implement duration and seeking for transcodes
feat: Playback speed controls for music

View File

@@ -0,0 +1,3 @@
fix: Proper raw stream detection
chore(i18n): Update Spanish translation
feat: add configurable timeout

View File

@@ -0,0 +1,2 @@
fix: Avoid crash when server has no songs
fix: updated dialog import to address crashing on android 15

View File

@@ -0,0 +1,3 @@
fix: missing Replay Gain metadata from .m4a files
fix: Improve Synced Lyrics
fix: Add selector for playlist visibility

View File

@@ -0,0 +1,4 @@
fix: Addressing some UI/UX quirks
fix: keep observer until data is received on continuousPlay bug
fix: album art now displays on android auto
feat: improve landscape view and increase items per row on landscape view