104 Commits

Author SHA1 Message Date
eddyizm
f6f24acfdf chore: bumped version 2025-11-15 09:31:52 -08:00
eddyizm
ca8bcba0d7 fix: add podcast channel visible when empty podcasts (#260) 2025-11-14 21:55:13 -08:00
eddyizm
9a36f8541f chore: added tested backends to docs as well as podcast/radio feed screenshots 2025-11-14 21:47:46 -08:00
eddyizm
0026dc287f Update Polish translation (#257) 2025-11-14 06:16:30 -08:00
skajmer
c65077172d Update strings.xml 2025-11-14 13:38:19 +01:00
skajmer
6e6c261f35 Update strings.xml 2025-11-14 13:35:05 +01:00
skajmer
130cbbd7dd Missing strings
sort albums by count stuff I missed
2025-11-14 13:28:21 +01:00
eddyizm
63668f5a8c fix: made sure the empty graphic was there when list was empty 2025-11-13 21:37:48 -08:00
eddyizm
193b551773 fix: home radio add station missing from view 2025-11-13 19:07:45 -08:00
eddyizm
76a0e12222 fix: add podcast channel visible when empty podcasts 2025-11-13 16:07:10 -08:00
skajmer
887d8c85ee Accidental typo 2025-11-13 10:38:03 +01:00
skajmer
6a90f06084 Added #253 2025-11-13 10:34:52 +01:00
skajmer
f7a21cbb52 Merge branch 'eddyizm:development' into development 2025-11-13 10:23:43 +01:00
eddyizm
3c6c240b9d chore: more pending release updates 2025-11-12 21:53:35 -08:00
eddyizm
2553c06a9f Fixed crash when viewing share (#255) 2025-11-12 07:01:43 -08:00
eddyizm
62a10d142e fix:github release check (#253) 2025-11-12 06:59:05 -08:00
drakeerv
955dc1b015 Fixed crash when viewing share 2025-11-12 09:45:07 -05:00
eddyizm
6124ec66f3 feat: added setting to disable github check and completely disable/hide when using the degoogled version 2025-11-11 18:06:01 -08:00
eddyizm
2c6287405e fix:github release check 2025-11-11 12:00:05 -08:00
eddyizm
0be309fb22 chore: updated pending release notes 2025-11-11 11:12:54 -08:00
eddyizm
a79543569d fix: disallow duplicate songs in queue (#252) 2025-11-11 11:08:26 -08:00
eddyizm
e3d7120193 chore: Update russian lang strings.xml (#249) 2025-11-11 11:08:02 -08:00
eddyizm
80a3a54476 Merge branch 'development' into patch-1 2025-11-11 11:05:25 -08:00
eddyizm
6448cc598d fix: disallow duplicate songs in queue 2025-11-10 10:53:16 -08:00
skajmer
c4bd30d512 Merge branch 'eddyizm:development' into development 2025-11-10 11:41:33 +01:00
eddyizm
b2e3596d87 chore: updated change log 2025-11-09 17:07:07 -08:00
eddyizm
ccfe74a6ea chore: bump version for release 2025-11-09 16:49:17 -08:00
eddyizm
f4ffdc985e chore: updating pending release and typo in settings 2025-11-09 16:48:16 -08:00
eddyizm
33981f9885 Fix shuffling genres only queuing 25 songs (#246) 2025-11-09 16:41:17 -08:00
observer
1bc93cce0e use overloads instead of new methods 2025-11-09 22:01:33 +00:00
eddyizm
ec0eee9d3f implement scroll to currently playing feature (#247) 2025-11-09 13:49:29 -08:00
observer
140546ca4d log in catch statement in PlayerQueueFragment:onResume 2025-11-09 21:27:17 +00:00
eddyizm
fd075a02c5 chore: updating change log and pending changes 2025-11-09 09:33:23 -08:00
observer
4d1d953a3a implement scroll to currently playing feature 2025-11-09 16:03:02 +00:00
eddyizm
9dd509be22 feat: Make artist and album clickable (#243) 2025-11-09 07:06:41 -08:00
observer
efaae35976 uncap mediamanager queue previously at 25 at 500 2025-11-09 15:05:49 +00:00
observer
35784216bc use getRandomSampleWithGenre() rather than getSongsByGenre() 2025-11-09 15:05:05 +00:00
observer
8ce0a82506 getRandomSongs2(): use new subsonic getRandomSongs param 'genre' instead of getSongsByGenre 2025-11-09 15:02:54 +00:00
skajmer
51883cd82b Merge branch 'eddyizm:development' into development 2025-11-09 13:06:27 +01:00
eddyizm
829c5d85f6 fix: Images not filling holder (#244) 2025-11-08 19:31:36 -08:00
eddyizm
748a19ef44 fix: discovery image fills holder at start 2025-11-08 09:34:25 -08:00
eddyizm
e987226954 feat: makes discovery media item clickable on home page #53 2025-11-08 09:09:09 -08:00
Max
59e2f4a7fa Update strings.xml
Adding new translation strings
2025-11-07 15:04:56 +03:00
eddyizm
817b53efaa fix: Equalizer fix in main build variant (#239) 2025-11-06 17:22:56 -08:00
Jaime García
12c7ec86a9 fix: Add listener to enable equalizer when audioSessionId changes (main build variant) 2025-11-07 02:06:08 +01:00
eddyizm
611b5001be chore: bumped version for new tag 2025-11-06 16:08:55 -08:00
eddyizm
923cfd5bc9 fix: equalizer missing referenced value 2025-11-06 16:08:07 -08:00
eddyizm
36d2320e70 chore: bump version and changelog for release 2025-11-06 15:44:46 -08:00
eddyizm
17713ee400 fix: Add listener to enable equalizer when audioSessionId changes (#235) 2025-11-06 15:40:39 -08:00
eddyizm
ee3465868e Album track list bug (#237) 2025-11-06 15:40:14 -08:00
eddyizm
9e8141a8d9 chore: name version 2025-11-06 14:49:19 -08:00
eddyizm
043e1b39b0 Revert "fix: do not override itemType and itemId"
This reverts commit d35146dba3.
2025-11-06 14:35:29 -08:00
Jaime García
938c1de906 chore: Remove comment 2025-11-06 04:48:24 +01:00
Jaime García
a0dfe63660 fix: Add listener to enable equalizer when audioSessionId changes 2025-11-06 03:18:39 +01:00
skajmer
42b7441467 Merge branch 'eddyizm:development' into development 2025-11-05 22:20:18 +01:00
eddyizm
28c2f87b26 chore: updated change log 2025-11-05 09:15:11 -08:00
eddyizm
9ab22bfede chore: bump version 2025-11-05 07:51:55 -08:00
eddyizm
20900fb557 chore: pre release prep 2025-11-04 22:11:02 -08:00
eddyizm
7457c5b6e3 fix: skip mapping downloaded item (#228) 2025-11-04 15:43:04 -08:00
pca006132
e5a928ec0f fix: skip mapping downloaded item 2025-11-05 00:35:27 +08:00
eddyizm
147c8360a6 Revert "improve battery consumption" (#227) 2025-11-04 07:02:50 -08:00
eddyizm
d5d504fc64 Revert "improve battery consumption" 2025-11-04 07:02:19 -08:00
eddyizm
24eead2d0a improve battery consumption (#223) 2025-11-03 21:20:06 -08:00
pca006132
2644fa52b6 enable offload 2025-11-03 22:33:42 +08:00
pca006132
38c144c073 avoid rebuffering after track change 2025-11-03 22:33:21 +08:00
pca006132
1dca1ef68d avoid updating player bottom sheet when not visible 2025-11-03 16:16:06 +08:00
pca006132
ba94d7e5cc fix null 2025-11-03 16:01:15 +08:00
pca006132
0028872e3f update one media item only 2025-11-03 15:07:29 +08:00
pca006132
be9eec625a avoid full updates 2025-11-03 14:50:06 +08:00
pca006132
b335ddec01 cache artwork bitmap 2025-11-03 13:33:22 +08:00
skajmer
8ad35ce83a Merge branch 'eddyizm:development' into development 2025-11-02 18:15:01 +01:00
eddyizm
eb5c4721d1 fix: update MediaItems after network change (#222) 2025-11-02 08:01:43 -08:00
eddyizm
b0e8fa75ca chore: update media3 dependencies (#217) 2025-11-02 08:00:55 -08:00
eddyizm
27d7288ee9 fix: do not override getItemViewType and getItemId (#221) 2025-11-02 08:00:37 -08:00
eddyizm
287921de09 fix: playlist page should not snap (#218) 2025-11-02 07:59:51 -08:00
eddyizm
e5cb8793b0 fix: remove NestedScrollViews for fragment_album_page (#216) 2025-11-02 07:59:34 -08:00
eddyizm
f25e7f250a Fix downloaded tab performance (#210) 2025-11-02 07:59:10 -08:00
eddyizm
911acc3c2d feat: sort artists by album count (#206) 2025-11-02 07:58:46 -08:00
pca006132
4b7f60bb8c fix: update MediaItems after network change 2025-11-02 16:44:15 +08:00
pca006132
d35146dba3 fix: do not override itemType and itemId 2025-11-02 14:54:32 +08:00
eddyizm
6c3897a400 Update USAGE.md with instant mix details (#220) 2025-11-01 16:50:47 -07:00
Thomas Anderson
1002499d92 Update USAGE.md with instant mix details
Follow up of https://github.com/eddyizm/tempus/issues/184#issuecomment-3475333928
2025-11-01 22:43:09 +03:00
skajmer
4464b5b34d Merge branch 'eddyizm:development' into development 2025-11-01 09:52:55 +01:00
pca006132
77c0b86dac fix: playlist page should not snap 2025-11-01 13:34:47 +08:00
pca006132
0abdfc6b19 fix: remove NestedScrollViews for fragment_album_page 2025-11-01 13:31:05 +08:00
pca006132
52b2ca8fa7 chore: update media3 dependencies 2025-11-01 11:44:40 +08:00
pca006132
7f66124614 fix: download tab performance 2025-10-31 20:16:01 +08:00
eddyizm
9930537486 shuffle for artists without using getTopSongs (#207) 2025-10-30 18:54:19 -07:00
pca006132
5d51132921 feat: add preference to sort artists by album count 2025-10-30 20:01:47 +08:00
pca006132
4b2e963a81 fix: shuffle for artists without using getTopSongs 2025-10-30 20:01:28 +08:00
pca006132
4c865e199d feat: sort artists by album count 2025-10-30 18:01:07 +08:00
eddyizm
fc58869354 chore(i18n): Update Spanish (es-ES) translation (#205) 2025-10-29 17:47:03 -07:00
Jaime García
5e1a2b41e9 chore(i18n): Update Spanish (es-ES) translation 2025-10-29 22:06:13 +01:00
eddyizm
77bdd71d79 chore: updated bug issue 2025-10-29 09:29:36 -07:00
skajmer
7267c13ee0 Add #199 2025-10-29 12:38:15 +01:00
eddyizm
4ab1f034d8 chore: bumped version for release, added changelogs 2025-10-28 18:53:45 -07:00
eddyizm
4bd8bbfa4c chore: removed build badge since its not live yet 2025-10-28 18:39:41 -07:00
eddyizm
3fc03114e2 chore: updated github url 2025-10-28 18:37:45 -07:00
eddyizm
576c93e6cb Crash on share no expiration date or field returned from api (#199) 2025-10-27 20:57:36 -07:00
eddyizm
ac674d937a fix: handle null or no expiry field being sent back from server 2025-10-27 20:47:46 -07:00
eddyizm
0ed329022e fix: gradle updated to exclude offending blobs, change log link fixed, removed old screenshots. 2025-10-27 19:55:29 -07:00
eddyizm
b8b4a77349 chore: updated tempo references to tempus including github check (#197) 2025-10-27 09:08:13 -07:00
eddyizm
de14663b25 chore: updated tempo references to tempus including github check 2025-10-27 09:06:57 -07:00
eddyizm
747af0d81c fix: updated crash report and degoogled icon for izzydroid #195 2025-10-27 07:39:07 -07:00
74 changed files with 735 additions and 285 deletions

View File

@@ -26,6 +26,7 @@ Outline the steps required to reproduce the bug, including any specific actions,
- Android device: [Device Model]
- Android OS version: [Android Version]
- App version: [App Version]
- App variant: [goole play services, degoogled]
- Other relevant details: [e.g., specific network conditions, external dependencies]
## Logs or Screenshots

View File

@@ -23,10 +23,11 @@ Please provide the steps to reproduce the crash:
- Android device: [Device Model]
- Android OS version: [Android Version]
- App version: [App Version]
- App variant: [goole play services, degoogled]
- Other relevant details: [e.g., specific network conditions, external dependencies]
## Crash Logs/Stack trace
<!-- If available, please provide the crash log or stack trace related to the crash. Include it inside a code block (surround with triple backticks ```). Please use the unsigned apk (app-tempo-debug.apk), as the logs would be illegible and therefore useless for this purpose. -->
<!-- If available, please provide the crash log or stack trace related to the crash. Include it inside a code block (surround with triple backticks ```). Please use the unsigned apk (app-tempus-debug.apk), as the logs would be illegible and therefore useless for this purpose. -->
## Screenshots
<!-- If applicable, add screenshots to help explain the problem. -->

View File

@@ -1,6 +1,65 @@
# Changelog
## [4.0.2](https://github.com/eddyizm/tempo/releases/tag/v4.0.2) (2025-10-26)
## Pending release...
* chore: Update russian strings.xml by @Sevinfolds in https://github.com/eddyizm/tempus/pull/249
* fix: disallow duplicate songs in queue by @eddyizm in https://github.com/eddyizm/tempus/pull/252
* fix:github release check by @eddyizm in https://github.com/eddyizm/tempus/pull/253
* fix: Fixed crash when viewing share by @drakeerv in https://github.com/eddyizm/tempus/pull/255
* chore: Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/257
* fix: add podcast/radio channel visible when empty podcasts/radio by @eddyizm in https://github.com/eddyizm/tempus/pull/260
## New Contributors
* @Sevinfolds made their first contribution in https://github.com/eddyizm/tempus/pull/249
* @drakeerv made their first contribution in https://github.com/eddyizm/tempus/pull/255
## [4.2.0](https://github.com/eddyizm/tempo/releases/tag/v4.2.0) (2025-11-09)
## What's Changed
* fix: Equalizer fix in main build variant by @jaime-grj in https://github.com/eddyizm/tempus/pull/239
* fix: Images not filling holder by @eddyizm in https://github.com/eddyizm/tempus/pull/244
* feat: Make artist and album clickable by @eddyizm in https://github.com/eddyizm/tempus/pull/243
* feat: implement scroll to currently playing feature by @shrapnelnet in https://github.com/eddyizm/tempus/pull/247
* fix: shuffling genres only queuing 25 songs by @shrapnelnet in https://github.com/eddyizm/tempus/pull/246
## New Contributors
* @shrapnelnet made their first contribution in https://github.com/eddyizm/tempus/pull/247
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.3...v4.2.0
## [4.1.3](https://github.com/eddyizm/tempo/releases/tag/v4.1.3) (2025-11-06)
## What's Changed
* [fix: equalizer missing referenced value](https://github.com/eddyizm/tempus/commit/923cfd5bc97ed7db28c90348e3619d0a784fc434)
* Fix: Album track list bug by @eddyizm in https://github.com/eddyizm/tempus/pull/237
* fix: Add listener to enable equalizer when audioSessionId changes by @jaime-grj in https://github.com/eddyizm/tempus/pull/235
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.0...v4.1.3
## [4.1.0](https://github.com/eddyizm/tempo/releases/tag/v4.1.0) (2025-11-05)
## What's Changed
* chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205
* shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207
* Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220
* feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206
* Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210
* fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216
* fix: playlist page should not snap by @pca006132 in https://github.com/eddyizm/tempus/pull/218
* fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221
* chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217
* fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222
* fix: skip mapping downloaded item by @pca006132 in https://github.com/eddyizm/tempus/pull/228
## New Contributors
* @pca006132 made their first contribution in https://github.com/eddyizm/tempus/pull/207
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.7...v4.1.0
## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28)
## What's Changed
* chore: updated tempo references to tempus including github check by @eddyizm in https://github.com/eddyizm/tempus/pull/197
* fix: Crash on share no expiration date or field returned from api by @eddyizm in https://github.com/eddyizm/tempus/pull/199
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.6...v4.0.7
## [4.0.6](https://github.com/eddyizm/tempo/releases/tag/v4.0.6) (2025-10-26)
## Attention
This release will not update previous installs as it is considered a new app, no longer `Tempo`, new icon, new app id, and new app name. Hoping it will not be a huge inconvenience but was necessary in order to publish to app stores like IzzyDroid and FDroid.

View File

@@ -2,17 +2,27 @@
<img alt="Tempus" title="Tempus" src="mockup/svg/tempus_horizontal_logo.png" width="250">
</p>
---
<p align="center">
<b>Access your music library on all your android devices</b>
</p>
<div align="center">
<!-- Reproducible build -->
<!-- [<img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status">](https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus) -->
</div>
<p align="center">
<a href="https://github.com/eddyizm/tempo/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
<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>
</p>
<!-- <p align="center">
<!--
<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>
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.cappielloantonio.tempo"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
</p> -->
-->
**Tempus** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
@@ -33,13 +43,10 @@ Please note the two variants in the release assets include release/debug and 32/
`app-tempus` <- The github release with all the android auto/chromecast features
`app-degoogled*` <- The f-droid release that goes without any of the google stuff. It was last released at 3.8.1 from the original repo. Since I don't have access to that original repo, I am releasing the apk's here on github.
`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)
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
## Usage
[Documentation](USAGE.md) (work in progress)
@@ -104,6 +111,11 @@ Currently there are no tests but I would love to start on some unit tests.
Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
## Support
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
## License
Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.

View File

@@ -6,6 +6,7 @@
- [Getting Started](#getting-started)
- [Server Configuration](#server-configuration)
- [Main Features](#main-features)
- [Navigation](#navigation)
- [Playback Controls](#playback-controls)
- [Favorites](#favorites)
@@ -27,6 +28,8 @@ This app works with any service that implements the Subsonic API, including:
- [LMS - Lightweight Music Server](https://github.com/epoupon/lms) - *personal fave and my backend*
- [Navidrome](https://www.navidrome.org/)
- [Gonic](https://github.com/sentriz/gonic)
- [Ampache](https://github.com/ampache/ampache)
- [NextCloud Music](https://apps.nextcloud.com/apps/music)
@@ -78,9 +81,23 @@ On the main player control screen, tapping on the artwork will reveal a small co
1. Downloads the track (there is a notification if the android screen but not a pop toast currently )
2. Adds track to playlist - pops up playlist dialog.
3. Adds tracks to the queue via instant mix function
* TBD: what is the _instant mix function_?
* Uses [getSimilarSongs](https://opensubsonic.netlify.app/docs/endpoints/getsimilarsongs/) of OpenSubsonic API.
Which tracks to be mixed depends on the server implementation. For example, Navidrome gets 15 similar artists from LastFM, then 20 top songs from each.
4. Saves play queue (if the feature is enabled in the settings)
* if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right)
### Podcasts
If your server supports it - add a podcast rss feed
<p align="left">
<img src="mockup/usage/add_podcast_feed.png" width=317>
</p>
### Radio Stations
If your server supports it - add a internet radio station feed
<p align="left">
<img src="mockup/usage/add_radio_station.png" width=326>
</p>
## Navigation
@@ -163,4 +180,4 @@ For additional help:
---
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*

View File

@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
versionCode 1
versionName '4.0.6'
versionCode 6
versionName '4.2.4'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {
@@ -35,7 +35,12 @@ android {
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
includeInApk = false
// Disables dependency metadata when building Android App Bundles (for Google Play)
includeInBundle = false
}
flavorDimensions += "default"
@@ -105,12 +110,12 @@ dependencies {
implementation 'com.github.bumptech.glide:annotations:4.16.0'
// Media3
implementation 'androidx.media3:media3-session:1.5.1'
implementation 'androidx.media3:media3-common:1.5.1'
implementation 'androidx.media3:media3-exoplayer:1.5.1'
implementation 'androidx.media3:media3-ui:1.5.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
tempusImplementation 'androidx.media3:media3-cast:1.5.1'
implementation 'androidx.media3:media3-session:1.8.0'
implementation 'androidx.media3:media3-common:1.8.0'
implementation 'androidx.media3:media3-exoplayer:1.8.0'
implementation 'androidx.media3:media3-ui:1.8.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
tempusImplementation 'androidx.media3:media3-cast:1.8.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'

View File

@@ -5,18 +5,20 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
@@ -43,6 +45,7 @@ class MediaService : MediaLibraryService() {
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var shuffleCommands: List<CommandButton>
private lateinit var repeatCommands: List<CommandButton>
private lateinit var networkCallback: CustomNetworkCallback
lateinit var equalizerManager: EqualizerManager
private var customLayout = ImmutableList.of<CommandButton>()
@@ -79,6 +82,39 @@ class MediaService : MediaLibraryService() {
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
"android.media3.session.demo.REPEAT_ALL"
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
}
fun updateMediaItems() {
Log.d("MediaService", "update items");
val n = player.mediaItemCount
val k = player.currentMediaItemIndex
val current = player.currentPosition
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
player.clearMediaItems()
player.setMediaItems(items, k, current)
}
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
var wasWifi = false
init {
val manager = getSystemService(ConnectivityManager::class.java)
val network = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null)
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifi != wasWifi) {
wasWifi = isWifi
widgetUpdateHandler.post(Runnable {
updateMediaItems()
})
}
}
}
override fun onCreate() {
@@ -90,6 +126,7 @@ class MediaService : MediaLibraryService() {
restorePlayerFromQueue()
initializePlayerListener()
initializeEqualizerManager()
initializeNetworkListener()
setPlayer(player)
}
@@ -99,6 +136,7 @@ class MediaService : MediaLibraryService() {
}
override fun onDestroy() {
releaseNetworkCallback()
equalizerManager.release()
stopWidgetUpdates()
releasePlayer()
@@ -246,16 +284,7 @@ class MediaService : MediaLibraryService() {
private fun initializeEqualizerManager() {
equalizerManager = EqualizerManager()
val audioSessionId = player.audioSessionId
if (equalizerManager.attachToSession(audioSessionId)) {
val enabled = Preferences.isEqualizerEnabled()
equalizerManager.setEnabled(enabled)
val bands = equalizerManager.getNumberOfBands()
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
}
}
attachEqualizerIfPossible(audioSessionId)
}
private fun initializeMediaLibrarySession() {
@@ -275,6 +304,12 @@ class MediaService : MediaLibraryService() {
}
}
private fun initializeNetworkListener() {
networkCallback = CustomNetworkCallback()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
updateMediaItems()
}
private fun restorePlayerFromQueue() {
if (player.mediaItemCount > 0) return
@@ -383,6 +418,10 @@ class MediaService : MediaLibraryService() {
customLayout = librarySessionCallback.buildCustomLayout(player)
mediaLibrarySession.setCustomLayout(customLayout)
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
attachEqualizerIfPossible(audioSessionId)
}
})
if (player.isPlaying) {
scheduleWidgetUpdates()
@@ -398,6 +437,10 @@ class MediaService : MediaLibraryService() {
mediaLibrarySession.release()
}
private fun releaseNetworkCallback() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
}
@SuppressLint("PrivateResource")
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
@@ -494,6 +537,21 @@ class MediaService : MediaLibraryService() {
widgetUpdateScheduled = false
}
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
if (audioSessionId == 0 || audioSessionId == -1) return false
val attached = equalizerManager.attachToSession(audioSessionId)
if (attached) {
val enabled = Preferences.isEqualizerEnabled()
equalizerManager.setEnabled(enabled)
val bands = equalizerManager.getNumberOfBands()
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
}
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
}
return attached
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)

View File

@@ -3,8 +3,8 @@ package com.cappielloantonio.tempo.github;
import com.cappielloantonio.tempo.github.api.release.ReleaseClient;
public class Github {
private static final String OWNER = "CappielloAntonio";
private static final String REPO = "Tempo";
private static final String OWNER = "eddyizm";
private static final String REPO = "Tempus";
private ReleaseClient releaseClient;
public ReleaseClient getReleaseClient() {

View File

@@ -7,10 +7,11 @@ public class UpdateUtil {
public static boolean showUpdateDialog(LatestRelease release) {
if (release.getTagName() == null) return false;
String remoteTag = release.getTagName().replaceAll("^\\D+", "");
try {
String[] local = BuildConfig.VERSION_NAME.split("\\.");
String[] remote = release.getTagName().split("\\.");
String[] remote = remoteTag.split("\\.");
for (int i = 0; i < local.length; i++) {
int localPart = Integer.parseInt(local[i]);

View File

@@ -13,9 +13,11 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import retrofit2.Call;
import retrofit2.Callback;
@@ -312,24 +314,42 @@ public class ArtistRepository {
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getTopSongs(artist.getName(), count)
.getArtist(artist.getId())
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getTopSongs() != null && response.body().getSubsonicResponse().getTopSongs().getSongs() != null) {
List<Child> songs = response.body().getSubsonicResponse().getTopSongs().getSongs();
if (response.isSuccessful() && response.body() != null &&
response.body().getSubsonicResponse().getArtist() != null &&
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
if (songs != null && !songs.isEmpty()) {
Collections.shuffle(songs);
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
Log.d("ArtistRepository", "Got albums directly: " + albums.size());
if (albums.isEmpty()) {
Log.d("ArtistRepository", "No albums found in artist response");
return;
}
randomSongs.setValue(songs);
Collections.shuffle(albums);
int[] counts = albums.stream().mapToInt(AlbumID3::getSongCount).toArray();
Arrays.parallelPrefix(counts, Integer::sum);
int albumLimit = 0;
int multiplier = 4; // get more than the limit so we can shuffle them
while (albumLimit < albums.size() && counts[albumLimit] < count * multiplier)
albumLimit++;
Log.d("ArtistRepository", String.format("Retaining %d/%d albums", albumLimit, albums.size()));
fetchAllAlbumSongsWithCallback(albums.stream().limit(albumLimit).collect(Collectors.toList()), songs -> {
Collections.shuffle(songs);
randomSongs.setValue(songs.stream().limit(count).collect(Collectors.toList()));
});
} else {
Log.d("ArtistRepository", "Failed to get artist info");
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d("ArtistRepository", "Error getting artist info: " + t.getMessage());
}
});

View File

@@ -121,6 +121,15 @@ public class QueueRepository {
}
}
private boolean isMediaInQueue(List<Queue> queue, Child media) {
if (queue == null || media == null) return false;
return queue.stream().anyMatch(queueItem ->
queueItem != null && media.getId() != null &&
queueItem.getId().equals(media.getId())
);
}
public void insertAll(List<Child> toAdd, boolean reset, int afterIndex) {
try {
List<Queue> media = new ArrayList<>();
@@ -134,8 +143,14 @@ public class QueueRepository {
media = getMediaThreadSafe.getMedia();
}
for (int i = 0; i < toAdd.size(); i++) {
Queue queueItem = new Queue(toAdd.get(i));
List<Child> filteredToAdd = toAdd;
final List<Queue> finalMedia = media;
filteredToAdd = toAdd.stream()
.filter(child -> !isMediaInQueue(finalMedia, child))
.collect(Collectors.toList());
for (int i = 0; i < filteredToAdd.size(); i++) {
Queue queueItem = new Queue(filteredToAdd.get(i));
media.add(afterIndex + i, queueItem);
}

View File

@@ -100,6 +100,33 @@ public class SongRepository {
return randomSongsSample;
}
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(number, fromYear, toYear, genre)
.enqueue(new Callback<ApiResponse>() {
@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 && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs());
}
randomSongsSample.setValue(songs);
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return randomSongsSample;
}
public void scrobble(String id, boolean submission) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()

View File

@@ -7,7 +7,7 @@ import java.util.UUID;
public class SubsonicPreferences {
private String serverUrl;
private String username;
private String clientName = "Tempo";
private String clientName = "Tempus";
private SubsonicAuthentication authentication;
public String getServerUrl() {

View File

@@ -34,6 +34,11 @@ public class AlbumSongListClient {
return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear);
}
public Call<ApiResponse> getRandomSongs(int size, Integer fromYear, Integer toYear, String genre) {
Log.d(TAG, "getRandomSongs()");
return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear, genre);
}
public Call<ApiResponse> getSongsByGenre(String genre, int count, int offset) {
Log.d(TAG, "getSongsByGenre()");
return albumSongListService.getSongsByGenre(subsonic.getParams(), genre, count, offset);

View File

@@ -19,6 +19,9 @@ public interface AlbumSongListService {
@GET("getRandomSongs")
Call<ApiResponse> getRandomSongs(@QueryMap Map<String, String> params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear);
@GET("getRandomSongs")
Call<ApiResponse> getRandomSongs(@QueryMap Map<String, String> params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear, @Query("genre") String genre);
@GET("getSongsByGenre")
Call<ApiResponse> getSongsByGenre(@QueryMap Map<String, String> params, @Query("genre") String genre, @Query("count") int count, @Query("offset") int offset);

View File

@@ -438,7 +438,7 @@ public class MainActivity extends BaseActivity {
}
private void checkTempoUpdate() {
if (BuildConfig.FLAVOR.equals("tempo") && Preferences.showTempoUpdateDialog()) {
if (BuildConfig.FLAVOR.equals("tempus") && Preferences.isGithubUpdateEnabled() && Preferences.showTempusUpdateDialog()) {
mainViewModel.checkTempoUpdate().observe(this, latestRelease -> {
if (latestRelease != null && UpdateUtil.showUpdateDialog(latestRelease)) {
GithubTempoUpdateDialog dialog = new GithubTempoUpdateDialog(latestRelease);

View File

@@ -151,6 +151,9 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
case Constants.ARTIST_ORDER_BY_RANDOM:
Collections.shuffle(artists);
break;
case Constants.ARTIST_ORDER_BY_ALBUM_COUNT:
artists.sort(Comparator.comparing(ArtistID3::getAlbumCount).reversed());
break;
}
notifyDataSetChanged();

View File

@@ -73,6 +73,11 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
this.item = item;
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> {
onLongClick();
return true;
});
}
public void onClick() {
@@ -82,6 +87,13 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
click.onMediaClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
click.onMediaLongClick(bundle);
return true;
}
}
private void startAnimation(ViewHolder holder) {

View File

@@ -55,7 +55,7 @@ public class GithubTempoUpdateDialog extends DialogFragment {
});
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> {
Preferences.setTempoUpdateReminder();
Preferences.setTempusUpdateReminder();
Objects.requireNonNull(getDialog()).dismiss();
});

View File

@@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
@@ -114,7 +115,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
artistAdapter = new ArtistCatalogueAdapter(this);
artistAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
bind.artistCatalogueRecyclerView.setAdapter(artistAdapter);
artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> artistAdapter.setItems(artistList));
artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> {
artistAdapter.setItems(artistList);
artistAdapter.sort(Preferences.getArtistSortOrder());
});
bind.artistCatalogueRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
@@ -192,6 +196,9 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
} else if (menuItem.getItemId() == R.id.menu_artist_sort_random) {
artistAdapter.sort(Constants.ARTIST_ORDER_BY_RANDOM);
return true;
} else if (menuItem.getItemId() == R.id.menu_artist_sort_album_count) {
artistAdapter.sort(Constants.ARTIST_ORDER_BY_ALBUM_COUNT);
return true;
}
return false;

View File

@@ -188,8 +188,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
} else {
if (bind != null)
bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
songHorizontalAdapter.setItems(songs);
reapplyPlayback();
}

View File

@@ -117,14 +117,12 @@ public class DownloadFragment extends Fragment implements ClickCallback {
if (songs.isEmpty()) {
if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.VISIBLE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE);
bind.downloadDownloadedSector.setVisibility(View.GONE);
bind.downloadedGroupByImageView.setVisibility(View.GONE);
}
} else {
if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.GONE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE);
bind.downloadDownloadedSector.setVisibility(View.VISIBLE);
bind.downloadedGroupByImageView.setVisibility(View.VISIBLE);

View File

@@ -3,7 +3,9 @@ package com.cappielloantonio.tempo.ui.fragment
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.BroadcastReceiver
import android.os.Bundle
import android.os.IBinder
import android.view.Gravity
@@ -12,6 +14,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import com.cappielloantonio.tempo.R
@@ -28,6 +31,17 @@ class EqualizerFragment : Fragment() {
private lateinit var safeSpace: Space
private val bandSeekBars = mutableListOf<SeekBar>()
private var receiverRegistered = false
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
@OptIn(UnstableApi::class)
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == MediaService.ACTION_EQUALIZER_UPDATED) {
initUI()
restoreEqualizerPreferences()
}
}
}
private val connection = object : ServiceConnection {
@OptIn(UnstableApi::class)
override fun onServiceConnected(className: ComponentName, service: IBinder) {
@@ -49,12 +63,29 @@ class EqualizerFragment : Fragment() {
intent.action = MediaService.ACTION_BIND_EQUALIZER
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
if (!receiverRegistered) {
ContextCompat.registerReceiver(
requireContext(),
equalizerUpdatedReceiver,
IntentFilter(MediaService.ACTION_EQUALIZER_UPDATED),
ContextCompat.RECEIVER_NOT_EXPORTED
)
receiverRegistered = true
}
}
override fun onStop() {
super.onStop()
requireActivity().unbindService(connection)
equalizerManager = null
if (receiverRegistered) {
try {
requireContext().unregisterReceiver(equalizerUpdatedReceiver)
} catch (_: Exception) {
// ignore if not registered
}
receiverRegistered = false
}
}
override fun onCreateView(
@@ -234,4 +265,4 @@ class EqualizerFragment : Fragment() {
}
private fun Int.dpToPx(context: Context): Int =
(this * context.resources.displayMetrics.density).toInt()
(this * context.resources.displayMetrics.density).toInt()

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -72,6 +73,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
super.onResume();
setMediaBrowserListenableFuture();
updateNowPlayingItem();
try {
long position = mediaBrowserListenableFuture.get().getCurrentMediaItemIndex();
bind.playerQueueRecyclerView.scrollToPosition((int) position);
} catch (Exception e) {
Log.e("PlayerQueueFragment", "Failed to get mediaBrowserListenableFuture in onResume", e);
}
}
@Override

View File

@@ -29,6 +29,7 @@ import androidx.navigation.NavOptions;
import androidx.navigation.fragment.NavHostFragment;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreference;
@@ -77,6 +78,13 @@ public class SettingsFragment extends PreferenceFragmentCompat {
result -> {}
);
if (!BuildConfig.FLAVOR.equals("tempus")) {
PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key");
if (githubUpdateCategory != null) {
getPreferenceScreen().removePreference(githubUpdateCategory);
}
}
directoryPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {

View File

@@ -189,7 +189,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
bind.songListShuffleImageView.setOnClickListener(v -> {
Collections.shuffle(songs);
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(25, songs.size())), 0);
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(500, songs.size())), 0);
activity.setBottomSheetInPeek(true);
});
}

View File

@@ -89,6 +89,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
ArtistRepository artistRepository = new ArtistRepository();
artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
// navidrome may return null for this
if (songs == null)
return;
MusicUtil.ratingFilter(songs);
if (!songs.isEmpty()) {

View File

@@ -17,6 +17,7 @@ import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.subsonic.models.Share;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.dialog.ShareUpdateDialog;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.UIUtil;
@@ -24,6 +25,8 @@ import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.cappielloantonio.tempo.viewmodel.ShareBottomSheetViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.List;
@UnstableApi
public class ShareBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
@@ -50,8 +53,15 @@ public class ShareBottomSheetDialog extends BottomSheetDialogFragment implements
private void init(View view) {
ImageView shareCover = view.findViewById(R.id.share_cover_image_view);
String coverArtId = null;
List<Child> entries = shareBottomSheetViewModel.getShare().getEntries();
if (entries != null && !entries.isEmpty()) {
coverArtId = entries.get(0).getCoverArtId();
}
CustomGlideRequest.Builder
.from(requireContext(), shareBottomSheetViewModel.getShare().getEntries().get(0).getCoverArtId(), CustomGlideRequest.ResourceType.Unknown)
.from(requireContext(), coverArtId, CustomGlideRequest.ResourceType.Unknown)
.build()
.into(shareCover);

View File

@@ -40,6 +40,7 @@ object Constants {
const val ARTIST_STARRED = "ARTIST_STARRED"
const val ARTIST_ORDER_BY_NAME = "ARTIST_ORDER_BY_NAME"
const val ARTIST_ORDER_BY_RANDOM = "ARTIST_ORDER_BY_RANDOM"
const val ARTIST_ORDER_BY_ALBUM_COUNT = "ARTIST_ORDER_BY_ALBUM_COUNT"
const val ARTIST_ORDER_BY_MOST_RECENTLY_STARRED = "ARTIST_ORDER_BY_MOST_RECENTLY_STARRED"
const val ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED = "ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED"

View File

@@ -115,6 +115,29 @@ public class MappingUtil {
.build();
}
public static MediaItem mapMediaItem(MediaItem old) {
String mediaId = null;
if (old.requestMetadata.extras != null)
mediaId = old.requestMetadata.extras.getString("id");
if (mediaId != null && DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(mediaId)) {
return old;
}
Uri uri = old.requestMetadata.mediaUri == null ? null : MusicUtil.updateStreamUri(old.requestMetadata.mediaUri);
return new MediaItem.Builder()
.setMediaId(old.mediaId)
.setMediaMetadata(old.mediaMetadata)
.setRequestMetadata(
new MediaItem.RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(old.requestMetadata.extras)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build();
}
public static List<MediaItem> mapDownloads(List<Child> items) {
ArrayList<MediaItem> downloads = new ArrayList<>();

View File

@@ -21,11 +21,16 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class MusicUtil {
private static final String TAG = "MusicUtil";
private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+");
public static Uri getStreamUri(String id) {
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
@@ -61,6 +66,24 @@ public class MusicUtil {
return Uri.parse(uri.toString());
}
public static Uri updateStreamUri(Uri uri) {
String s = uri.toString();
Matcher m1 = BITRATE_PATTERN.matcher(s);
s = m1.replaceAll("");
Matcher m2 = FORMAT_PATTERN.matcher(s);
s = m2.replaceAll("");
s = s.replace("&estimateContentLength=true", "");
if (!Preferences.isServerPrioritized())
s += "&maxBitRate=" + getBitratePreference();
if (!Preferences.isServerPrioritized())
s += "&format=" + getTranscodingFormatPreference();
if (Preferences.askForEstimateContentLength())
s += "&estimateContentLength=true";
return Uri.parse(s);
}
public static Uri getDownloadUri(String id) {
StringBuilder uri = new StringBuilder();

View File

@@ -70,6 +70,7 @@ object Preferences {
private const val SONG_RATING_PER_ITEM = "song_rating_per_item"
private const val RATING_PER_ITEM = "rating_per_item"
private const val NEXT_UPDATE_CHECK = "next_update_check"
private const val GITHUB_UPDATE_CHECK = "github_update_check"
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"
@@ -79,6 +80,7 @@ object Preferences {
private const val ALBUM_DETAIL = "album_detail"
private const val ALBUM_SORT_ORDER = "album_sort_order"
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
@JvmStatic
fun getServer(): String? {
@@ -573,15 +575,21 @@ object Preferences {
return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false)
}
@JvmStatic
fun showTempoUpdateDialog(): Boolean {
fun isGithubUpdateEnabled(): Boolean {
return App.getInstance().preferences.getBoolean(GITHUB_UPDATE_CHECK, true)
}
@JvmStatic
fun showTempusUpdateDialog(): Boolean {
return App.getInstance().preferences.getLong(
NEXT_UPDATE_CHECK, 0
) + 86400000 < System.currentTimeMillis()
}
@JvmStatic
fun setTempoUpdateReminder() {
fun setTempusUpdateReminder() {
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
}
@@ -656,4 +664,14 @@ object Preferences {
fun setAlbumSortOrder(sortOrder: String) {
App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply()
}
@JvmStatic
fun getArtistSortOrder(): String {
val sort_by_album_count = App.getInstance().preferences.getBoolean(ARTIST_SORT_BY_ALBUM_COUNT, false)
Log.d("Preferences", "getSortOrder")
if (sort_by_album_count)
return Constants.ARTIST_ORDER_BY_ALBUM_COUNT
else
return Constants.ARTIST_ORDER_BY_NAME
}
}

View File

@@ -105,7 +105,11 @@ public class UIUtil {
}
public static String getReadableDate(Date date) {
if (date == null) {
return App.getContext().getString(R.string.share_no_expiration);
}
SimpleDateFormat formatter = new SimpleDateFormat("dd MMM, yyyy", Locale.getDefault());
return formatter.format(date);
}
}

View File

@@ -37,7 +37,7 @@ public class SongListPageViewModel extends AndroidViewModel {
public int year = 0;
public int maxNumberByYear = 500;
public int maxNumberByGenre = 100;
public int maxNumberByGenre = 500;
public SongListPageViewModel(@NonNull Application application) {
super(application);
@@ -51,7 +51,7 @@ public class SongListPageViewModel extends AndroidViewModel {
switch (title) {
case Constants.MEDIA_BY_GENRE:
songList = songRepository.getSongsByGenre(genre.getGenre(), 0);
songList = songRepository.getRandomSampleWithGenre(maxNumberByGenre, 0, 3000, genre.getGenre());
break;
case Constants.MEDIA_BY_ARTIST:
songList = artistRepository.getTopSongs(artist.getName(), 50);

View File

@@ -14,22 +14,21 @@
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_album_page_nested_scroll_view"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/album_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:paddingTop="8dp">
android:paddingTop="8dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/album_cover_image_view"
@@ -252,53 +251,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_page_button_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp" />
<LinearLayout
android:id="@+id/similar_album_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="32dp"
android:paddingEnd="20dp"
android:text="@string/album_page_extra_info_button" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/similar_albums_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/toolbar_fragment"
@@ -26,6 +27,7 @@
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom"
android:visibility="gone">
<ImageView
@@ -57,92 +59,78 @@
android:text="@string/download_info_empty_subtitle" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_download_nested_scroll_view"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_downloaded_sector"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_downloaded_sector"
<TextView
android:id="@+id/downloaded_text_view_refreshable"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/download_title_section"
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/shuffle_downloaded_text_view_clickable"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="@dimen/global_padding_bottom"
android:visibility="gone"
tools:visibility="visible">
android:text="@string/download_shuffle_all_subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable" />
<TextView
android:id="@+id/downloaded_text_view_refreshable"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/download_title_section"
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/downloaded_refresh_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/ic_refresh"
android:contentDescription="@string/download_refresh_button_content_description"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<TextView
android:id="@+id/shuffle_downloaded_text_view_clickable"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/download_shuffle_all_subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
<ImageView
android:id="@+id/downloaded_go_back_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:layout_marginHorizontal="12dp"
android:background="@drawable/ic_arrow_back"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<ImageView
android:id="@+id/downloaded_refresh_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/ic_refresh"
android:contentDescription="@string/download_refresh_button_content_description"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<ImageView
android:id="@+id/downloaded_group_by_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:background="@drawable/ic_filter_list"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/downloaded_go_back_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginHorizontal="12dp"
android:layout_gravity="center"
android:background="@drawable/ic_arrow_back"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<ImageView
android:id="@+id/downloaded_group_by_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:background="@drawable/ic_filter_list"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloaded_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="12dp"
android:paddingBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/shuffle_downloaded_text_view_clickable" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloaded_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingHorizontal="12dp"
android:paddingTop="12dp"
android:paddingBottom="@dimen/global_padding_bottom" />
</LinearLayout>

View File

@@ -20,6 +20,20 @@
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom">
<TextView
android:id="@+id/podcast_channels_pre_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_subtitle_new_podcast_channel"
android:textAllCaps="true"
android:textColor="?attr/colorPrimary"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/home_podcast_channels_sector"
android:layout_width="match_parent"
@@ -29,17 +43,6 @@
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/podcast_channels_pre_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_subtitle_new_podcast_channel"
android:textAllCaps="true" />
<!-- Label and button -->
<LinearLayout
android:layout_width="match_parent"
@@ -169,4 +172,4 @@
android:gravity="center"
android:text="@string/podcast_info_empty_button" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,44 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<TextView
android:id="@+id/internet_radio_station_pre_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_subtitle_new_internet_radio_station"
android:textAllCaps="true"
android:textColor="?attr/colorPrimary"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/internet_radio_station_title_text_view"
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/home_title_internet_radio_station"
app:layout_constraintTop_toBottomOf="@id/internet_radio_station_pre_text_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.core.widget.NestedScrollView
android:id="@+id/home_radio_station_sector"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@id/internet_radio_station_title_text_view">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom">
<TextView
android:id="@+id/internet_radio_station_pre_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_subtitle_new_internet_radio_station"
android:textAllCaps="true" />
<TextView
android:id="@+id/internet_radio_station_title_text_view"
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/home_title_internet_radio_station" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/internet_radio_station_recycler_view"
android:layout_width="match_parent"
@@ -61,7 +71,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@id/internet_radio_station_title_text_view">
<ImageView
android:id="@+id/empty_description_image_view"
@@ -105,7 +115,4 @@
android:gravity="center"
android:text="@string/radio_station_info_empty_button" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -27,7 +27,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/playlist_cover_image_view_top_left"

View File

@@ -7,7 +7,10 @@
<ImageView
android:id="@+id/discover_song_cover_image_view"
android:layout_width="match_parent"
android:layout_height="196dp"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:pivotX="50%"
android:pivotY="50%"
android:background="?attr/colorSurfaceContainerHighest"
android:foreground="@drawable/gradient_discover_background_image" />

View File

@@ -6,4 +6,7 @@
<item
android:id="@+id/menu_artist_sort_random"
android:title="@string/menu_sort_random" />
<item
android:id="@+id/menu_artist_sort_album_count"
android:title="@string/menu_sort_album_count" />
</menu>

View File

@@ -413,7 +413,9 @@
<string name="share_bottom_sheet_delete">Eliminar compartición</string>
<string name="share_bottom_sheet_update">Actualizar compartición</string>
<string name="share_subtitle_item">Fecha de caducidad: %1$s</string>
<string name="share_no_expiration">Nunca</string>
<string name="share_unsupported_error">El uso compartido no está soportado o no está habilitado</string>
<string name="asset_link_debug_toast">Enlace de recurso: %1$s</string>
<string name="share_update_dialog_hint_description">Descripción</string>
<string name="share_update_dialog_hint_expiration_date">Fecha de caducidad</string>
<string name="song_bottom_sheet_add_to_queue">Añadir a la cola</string>
@@ -486,4 +488,21 @@
<string name="settings_sync_starred_artists_for_offline_use_summary">Si está habilitada, los artistas destacados se descargarán para uso sin conexión.</string>
<string name="widget_time_elapsed_placeholder">0:00</string>
<string name="exo_controls_heart_off_description">Eliminar de favoritos</string>
<string name="asset_link_chip_text">%1$s • %2$s</string>
<string name="asset_link_copied_toast">Copiado %1$s al portapapeles</string>
<string name="settings_album_detail">Mostrar los detalles del álbum</string>
<string name="settings_album_detail_summary">Si está habilitada, muestra los detalles del álbum, como el género, el número de pistas, etc. en la página de álbum</string>
<string name="asset_link_clipboard_label">Enlace de recurso de Tempus</string>
<string name="asset_link_label_song">UID de la pista</string>
<string name="asset_link_label_album">UID del álbum</string>
<string name="asset_link_label_artist">UID del artista</string>
<string name="asset_link_label_playlist">UID de la lista de reproducción</string>
<string name="asset_link_label_genre">UID del género</string>
<string name="asset_link_label_year">UID del año</string>
<string name="asset_link_label_unknown">UID del recurso</string>
<string name="asset_link_error_unsupported">Enlace de recurso no válido</string>
<string name="asset_link_error_song">No se ha podido abrir la pista</string>
<string name="asset_link_error_album">No se ha podido abrir el álbum</string>
<string name="asset_link_error_artist">No se ha podido abrir el artista</string>
<string name="asset_link_error_playlist">No se ha podido abrir la lista de reproducción</string>
</resources>

View File

@@ -321,6 +321,9 @@
<string name="settings_github_summary">Śledź tworzenie aplikacji</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">Aktualizacje</string>
<string name="settings_github_update_title">Sprawdzaj dostępność nowych aktualizacji na githubie</string>
<string name="settings_github_update_summary">Jeżeli używana jest wersja z githuba, domyślnie aplikacja będzie sprawdzać czy są dostępne nowe wydania apk. Kliknij przełącznik aby, wyłączyć automatyczne sprawdzanie</string>
<string name="settings_support_summary">Dołącz do dyskusji i wsparcia społeczności</string>
<string name="settings_support_title">Wsparcie użytkowników</string>
<string name="settings_scan_result">Skanowanie: naliczono %1$d utworów</string>
@@ -400,6 +403,7 @@
<string name="share_bottom_sheet_delete">Usuń udostępnianie</string>
<string name="share_bottom_sheet_update">Zaktualizuj udostępnianie</string>
<string name="share_subtitle_item">Data wygaśnięcia: %1$s</string>
<string name="share_no_expiration">Nigdy</string>
<string name="share_unsupported_error">Udostępnianie nie jest wspierane lub włączone</string>
<string name="asset_link_clipboard_label">Link zasobu Tempus</string>
<string name="asset_link_label_song">UID piosenki</string>
@@ -516,4 +520,6 @@
<string name="settings_app_equalizer_summary">Otwórz wbudowany korektor dźwięku</string>
<string name="settings_album_detail">Pokaż szczegóły albumu</string>
<string name="settings_album_detail_summary">Jeżeli włączone, pokaż szczegóły albumu takie jak gatunek, ilość piosenek itp. na stronie albumu</string>
<string name="settings_artist_sort_by_album_count">Sortuj wykonawców po ilości albumów</string>
<string name="settings_artist_sort_by_album_count_summary">Jeżeli włączone, sortuje wykonawców po ilości albumów. Jeżeli wyłączone, sortuje albumy po nazwach.</string>
</resources>

View File

@@ -270,7 +270,13 @@
<string name="server_unreachable_dialog_title">Сервер недоступен</string>
<string name="settings_about_summary">Tempus — это легкий музыкальный клиент с открытым исходным кодом для Subsonic, разработанный и созданный специально для Android.</string>
<string name="settings_about_title">О нас</string>
<string name="settings_album_detail">Показать детали альбома</string>
<string name="settings_album_detail_summary">Если включено, отображать информацию об альбоме, например жанр, количество песен и т. д., на странице альбома.</string>
<string name="settings_allow_playlist_duplicates">Разрешить добавление дубликатов в плейлист</string>
<string name="settings_allow_playlist_duplicates_summary">Если включено, дубликаты не будут проверяться при добавлении в плейлист..</string>
<string name="settings_always_on_display">Всегда на дисплее</string>
<string name="settings_artist_sort_by_album_count">Сортировать исполнителей по количеству альбомов</string>
<string name="settings_artist_sort_by_album_count_summary">Если включено, сортировать исполнителей по количеству альбомов. Если отключено, сортировать по имени.</string>
<string name="settings_audio_transcode_download_format">Формат перекодирования</string>
<string name="settings_audio_transcode_download_priority_summary">Если этот параметр включен, Tempus не будет принудительно загружать трек с настройками перекодирования, указанными ниже.</string>
<string name="settings_audio_transcode_download_priority_title">Установите приоритет настроек сервера, используемых для потоковой передачи при загрузке</string>
@@ -319,8 +325,12 @@
<string name="settings_queue_syncing_countdown">Таймер синхронизации</string>
<string name="settings_queue_syncing_summary">Если этот параметр включен, пользователь будет иметь возможность сохранять свою очередь воспроизведения и загружать состояние при открытии приложения.</string>
<string name="settings_queue_syncing_title">Синхронизировать очередь воспроизведения для этого пользователя</string>
<string name="settings_show_mini_shuffle_button">Показать кнопку Shuffle</string>
<string name="settings_show_mini_shuffle_button_summary">Если включено, показывать кнопку перемешивания, убрать сердечко в мини-плеере</string>
<string name="settings_radio">Показать радио</string>
<string name="settings_radio_summary">Если включено, показывать раздел радио. Перезапустите приложение, чтобы оно вступило в силу.</string>
<string name="settings_auto_download_lyrics">Автоматическая загрузка текстов песен</string>
<string name="settings_auto_download_lyrics_summary">Автоматически сохранять тексты песен, когда они доступны, чтобы их можно было просматривать в автономном режиме.</string>
<string name="settings_replay_gain">Установите режим усиления воспроизведения</string>
<string name="settings_rounded_corner">Закругленные углы</string>
<string name="settings_rounded_corner_size">Размер углов</string>
@@ -346,6 +356,9 @@
<string name="settings_sync_starred_albums_for_offline_use_title">Синхронизировать помеченные альбомы для использования в автономном режиме.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Если этот параметр включен, помеченные треки будут загружены для использования в автономном режиме.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Синхронизировать помеченные треки для использования в автономном режиме.</string>
<string name="settings_sync_starred_artists_for_offline_use_title">Синхронизировать избранных исполнителей для использования офлайн</string>
<string name="settings_support_summary">Присоединяйтесь к обсуждениям в сообществе и оказывайте поддержку</string>
<string name="settings_support_title">Поддержка пользователей</string>
<string name="settings_theme">Тема</string>
<string name="settings_title_data">Данные</string>
<string name="settings_title_general">Общий</string>

View File

@@ -200,6 +200,7 @@
<string name="menu_sort_artist">Artist</string>
<string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Random</string>
<string name="menu_sort_album_count">Album Count</string>
<string name="menu_sort_recently_added">Recently added</string>
<string name="menu_sort_recently_played">Recently played</string>
<string name="menu_sort_most_played">Most played</string>
@@ -330,6 +331,9 @@
<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_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>
<string name="settings_support_summary">Join community discussions and support</string>
<string name="settings_support_title">User support</string>
<string name="settings_scan_result">Scanning: counting %1$d tracks</string>
@@ -378,7 +382,7 @@
<string name="settings_summary_syncing">Returns the state of the play queue for this user. This includes the tracks in the play queue, the currently playing track, and the position within this track. The server must support this feature.\n*This setting is not 100% working on all servers/devices.</string>
<string name="settings_summary_streaming_cache_size">%1$s \nCurrently in use: %2$s MiB</string>
<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 donwloads\" is set to \"Direct download\" 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_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>
@@ -409,6 +413,7 @@
<string name="share_bottom_sheet_delete">Delete share</string>
<string name="share_bottom_sheet_update">Update share</string>
<string name="share_subtitle_item">Expiration date: %1$s</string>
<string name="share_no_expiration">Never</string>
<string name="share_unsupported_error">Sharing is not supported or not enabled</string>
<string name="asset_link_clipboard_label">Tempus asset link</string>
<string name="asset_link_label_song">Song UID</string>
@@ -526,4 +531,6 @@
<string name="settings_album_detail">Show album detail</string>
<string name="settings_album_detail_summary">If enabled, show the album details like genre, song count etc. on the album page</string>
<string name="settings_artist_sort_by_album_count">Sort artists by album count</string>
<string name="settings_artist_sort_by_album_count_summary">If enabled, sort the artists by album count. Sort by name if disabled.</string>
</resources>

View File

@@ -116,6 +116,12 @@
android:summary="@string/settings_album_detail_summary"
android:key="album_detail" />
<SwitchPreference
android:title="@string/settings_artist_sort_by_album_count"
android:defaultValue="false"
android:summary="@string/settings_artist_sort_by_album_count_summary"
android:key="artist_sort_by_album_count" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_playlist">
@@ -394,6 +400,18 @@
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory
android:key="settings_github_update_category_key"
app:title="@string/settings_github_update">
<Preference
app:selectable="false"
app:summary="@string/settings_github_update_summary" />
<SwitchPreference
android:title="@string/settings_github_update_title"
android:defaultValue="true"
android:key="github_update_check" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_about_title">
<Preference
app:selectable="false"

View File

@@ -4,10 +4,14 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Binder
import android.os.IBinder
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
@@ -43,6 +47,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private lateinit var castPlayer: CastPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var librarySessionCallback: MediaLibrarySessionCallback
private lateinit var networkCallback: CustomNetworkCallback
lateinit var equalizerManager: EqualizerManager
inner class LocalBinder : Binder() {
@@ -55,6 +60,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
companion object {
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
}
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
private var widgetUpdateScheduled = false
@@ -69,6 +75,38 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
}
}
fun updateMediaItems() {
Log.d("MediaService", "update items");
val n = player.mediaItemCount
val k = player.currentMediaItemIndex
val current = player.currentPosition
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
player.clearMediaItems()
player.setMediaItems(items, k, current)
}
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
var wasWifi = false
init {
val manager = getSystemService(ConnectivityManager::class.java)
val network = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null)
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifi != wasWifi) {
wasWifi = isWifi
widgetUpdateHandler.post(Runnable {
updateMediaItems()
})
}
}
}
override fun onCreate() {
super.onCreate()
@@ -79,6 +117,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
initializePlayerListener()
initializeCastPlayer()
initializeEqualizerManager()
initializeNetworkListener()
setPlayer(
null,
@@ -99,6 +138,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
}
override fun onDestroy() {
releaseNetworkCallback()
equalizerManager.release()
stopWidgetUpdates()
releasePlayer()
@@ -121,16 +161,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private fun initializeEqualizerManager() {
equalizerManager = EqualizerManager()
val audioSessionId = player.audioSessionId
if (equalizerManager.attachToSession(audioSessionId)) {
val enabled = Preferences.isEqualizerEnabled()
equalizerManager.setEnabled(enabled)
val bands = equalizerManager.getNumberOfBands()
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
}
}
attachEqualizerIfPossible(audioSessionId)
}
private fun initializePlayer() {
@@ -178,6 +209,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
.build()
}
private fun initializeNetworkListener() {
networkCallback = CustomNetworkCallback()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
updateMediaItems()
}
private fun restorePlayerFromQueue() {
if (player.mediaItemCount > 0) return
@@ -288,6 +325,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onRepeatModeChanged(repeatMode: Int) {
Preferences.setRepeatMode(repeatMode)
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
attachEqualizerIfPossible(audioSessionId)
}
})
if (player.isPlaying) {
scheduleWidgetUpdates()
@@ -374,6 +415,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
automotiveRepository.deleteMetadata()
}
private fun releaseNetworkCallback() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
override fun onCastSessionAvailable() {
@@ -401,6 +446,22 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.playWhenReady = isPlaying
player.prepare()
}
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
if (audioSessionId == 0 || audioSessionId == -1) return false
val attached = equalizerManager.attachToSession(audioSessionId)
if (attached) {
val enabled = Preferences.isEqualizerEnabled()
equalizerManager.setEnabled(enabled)
val bands = equalizerManager.getNumberOfBands()
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
}
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
}
return attached
}
}
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L

View File

@@ -0,0 +1,2 @@
chore: updated tempo references to tempus
fix: Crash on share no expiration date or field returned from api

View File

@@ -0,0 +1,10 @@
Update Spanish (es-ES) translation
Shuffle for artists without using `getTopSongs`
Update USAGE.md with instant mix detils
Sort artists by album count
Fix downloaded tab performance
Remove NestedScrollViews for fragment_album_page
Playlist page should not snap
Do not override getItemViewType and getItemId
Update media3 dependencies
Update MediaItems after network change

View File

@@ -0,0 +1,2 @@
reverts change causing album disc/track list to get out of order
Add listener to enable equalizer when audioSessionId

View File

@@ -0,0 +1,5 @@
Equalizer fix in main build variant
fix Images not filling holder in discovery
Make artist and album clickable on home discovery
Implement scroll to currently playing feature
Shuffling genres now queuing 500 tracks vs 25

View File

@@ -0,0 +1,5 @@
Update russian strings.xml by @Sevinfolds in https://github.com/eddyizm/tempus/pull/249
Disallow duplicate songs in queue by @eddyizm in https://github.com/eddyizm/tempus/pull/252
Fixed crash when viewing share by @drakeerv in https://github.com/eddyizm/tempus/pull/255
Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/257
Add podcast channel visible when empty podcasts by @eddyizm in https://github.com/eddyizm/tempus/pull/260

View File

@@ -8,11 +8,12 @@ Features
- Streaming and Offline Mode: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers.
- Playlist Management: Create, edit, and manage playlists to curate your perfect music collection.
- Gapless Playback: Experience uninterrupted playback with gapless listening mode.
- Chromecast Support: Stream your music to Chromecast devices. The support is currently in a rudimentary state.
- Scrobbling Integration: Optionally integrate Tempus with Last.fm to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- 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.
- 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.
- Multiple Libraries: Tempus handles multi-library setups gracefully. They are displayed as Library folders.
- Equalizer: Option to use in app equalizer.
- Widget: New widget to keeping the basic controls on your screen at all times.
- Available in 11 languages: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish
*Chromecast/Android Auto Not Supported: These features require google libraries and are only available in the github apk.*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 958.83 227.02">
<defs>
<style>
.cls-1 {
fill: #f24b6a;
}
</style>
</defs>
<g id="Livello_1-2" data-name="Livello 1">
<g>
<g>
<path d="m127.84,23.28v27.94h-47.22v129.87h-33.19V51.22H0v-27.94h127.84Z"/>
<path d="m207.46,146.83c-.79,6.92-4.39,13.96-10.81,21.09-9.99,11.35-23.98,17.02-41.97,17.02-14.85,0-27.94-4.78-39.29-14.35-11.35-9.56-17.02-25.12-17.02-46.68,0-20.2,5.12-35.69,15.36-46.47,10.24-10.78,23.54-16.17,39.88-16.17,9.71,0,18.45,1.82,26.23,5.46,7.78,3.64,14.2,9.39,19.27,17.24,4.57,6.92,7.53,14.95,8.89,24.09.78,5.35,1.11,13.06.96,23.13h-79.87c.43,11.71,4.1,19.91,11.03,24.62,4.21,2.93,9.28,4.39,15.2,4.39,6.28,0,11.38-1.78,15.31-5.35,2.14-1.93,4.03-4.6,5.67-8.03h31.16Zm-30.19-35.76c-.5-8.06-2.94-14.19-7.33-18.36-4.39-4.18-9.83-6.26-16.33-6.26-7.07,0-12.55,2.21-16.43,6.64-3.89,4.43-6.34,10.42-7.33,17.99h47.43Z"/>
<path d="m289.12,96.51c-2.57-5.64-7.6-8.46-15.1-8.46-8.71,0-14.56,2.82-17.56,8.46-1.64,3.21-2.46,8-2.46,14.35v70.23h-30.94v-116.49h29.66v17.02c3.78-6.07,7.35-10.39,10.71-12.96,5.92-4.57,13.6-6.85,23.02-6.85,8.92,0,16.13,1.96,21.63,5.89,4.42,3.64,7.78,8.32,10.06,14.03,4-6.85,8.96-11.88,14.88-15.1,6.28-3.21,13.28-4.82,20.98-4.82,5.14,0,10.21,1,15.2,3,5,2,9.53,5.5,13.6,10.49,3.28,4.07,5.5,9.07,6.64,14.99.71,3.93,1.07,9.67,1.07,17.24l-.21,73.55h-31.26v-74.3c0-4.42-.71-8.06-2.14-10.92-2.71-5.42-7.71-8.14-14.99-8.14-8.42,0-14.24,3.5-17.45,10.49-1.64,3.71-2.46,8.17-2.46,13.38v69.49h-30.73v-69.49c0-6.92-.71-11.95-2.14-15.1Z"/>
<path d="m504.99,76.92c9.42,10.06,14.13,24.84,14.13,44.33,0,20.56-4.62,36.22-13.86,47-9.24,10.78-21.15,16.17-35.71,16.17-9.28,0-16.99-2.32-23.13-6.96-3.36-2.57-6.64-6.32-9.85-11.24v60.81h-30.19V64.39h29.23v17.24c3.28-5.07,6.78-9.06,10.49-11.99,6.78-5.21,14.85-7.82,24.2-7.82,13.63,0,25.2,5.03,34.69,15.1Zm-17.34,45.82c0-8.99-2.05-16.95-6.16-23.88-4.11-6.92-10.76-10.39-19.97-10.39-11.06,0-18.67,5.25-22.8,15.74-2.14,5.57-3.21,12.63-3.21,21.2,0,13.56,3.6,23.09,10.81,28.59,4.28,3.21,9.35,4.82,15.2,4.82,8.49,0,14.97-3.28,19.43-9.85,4.46-6.57,6.69-15.31,6.69-26.23Z"/>
<path d="m634.28,79.17c9.85,12.35,14.78,26.95,14.78,43.79s-4.93,31.78-14.78,43.95c-9.85,12.17-24.8,18.25-44.86,18.25s-35.01-6.08-44.86-18.25c-9.85-12.17-14.77-26.82-14.77-43.95s4.92-31.44,14.77-43.79c9.85-12.35,24.8-18.52,44.86-18.52s35.01,6.17,44.86,18.52Zm-44.97,7.28c-8.92,0-15.79,3.16-20.61,9.48-4.82,6.32-7.23,15.33-7.23,27.03s2.41,20.74,7.23,27.09c4.82,6.35,11.69,9.53,20.61,9.53s15.77-3.18,20.56-9.53c4.78-6.35,7.17-15.38,7.17-27.09s-2.39-20.72-7.17-27.03c-4.78-6.32-11.63-9.48-20.56-9.48Z"/>
</g>
<g>
<rect class="cls-1" x="827.72" width="11.92" height="227.02"/>
<rect class="cls-1" x="803.88" y="17.88" width="11.92" height="167.43"/>
<rect class="cls-1" x="780.05" y="35.76" width="11.92" height="53.64"/>
<rect class="cls-1" x="708.53" y="35.76" width="11.92" height="53.64"/>
<rect class="cls-1" x="946.92" y="35.76" width="11.92" height="53.64"/>
<rect class="cls-1" x="756.21" y="17.88" width="11.92" height="89.39"/>
<rect class="cls-1" x="899.24" y="17.88" width="11.92" height="89.39"/>
<rect class="cls-1" x="732.37" width="11.92" height="125.15"/>
<rect class="cls-1" x="923.08" width="11.92" height="125.15"/>
<rect class="cls-1" x="875.4" y="35.76" width="11.92" height="53.64"/>
<rect class="cls-1" x="851.56" y="17.88" width="11.92" height="167.43"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB