Compare commits
34 Commits
v4.11.0
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a179db6323 | ||
|
|
1beeab28a6 | ||
|
|
ad6a569961 | ||
|
|
0f5a8f6b97 | ||
|
|
3cd1bdf229 | ||
|
|
d7389db265 | ||
|
|
b3c93b3885 | ||
|
|
25864accc9 | ||
|
|
f734ced2cb | ||
|
|
4a3d2305c0 | ||
|
|
8db6797eaa | ||
|
|
cb4c19757d | ||
|
|
b6e75afe12 | ||
|
|
b621be06df | ||
|
|
7a17e91690 | ||
|
|
1036829186 | ||
|
|
becfc1d589 | ||
|
|
44bf346332 | ||
|
|
896e5fb3bd | ||
|
|
3086a8b9f9 | ||
|
|
10c2172be0 | ||
|
|
918bf6928e | ||
|
|
c9cf86acb5 | ||
|
|
0487f3bb9b | ||
|
|
c7f2524085 | ||
|
|
88c2129cd4 | ||
|
|
aa5d0f92db | ||
|
|
3ba2255205 | ||
|
|
145bb82eb0 | ||
|
|
932d1aaa8c | ||
|
|
4f8212d491 | ||
|
|
b403d69982 | ||
|
|
a49f2b97a2 | ||
|
|
c44e60c0e5 |
55
CHANGELOG.md
@@ -1,5 +1,60 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.13.0](https://github.com/eddyizm/tempo/releases/tag/v4.13.0) (2026-03-25)
|
||||||
|
* chore(i18n): Improve Russian translation by @NikkoFox in https://github.com/eddyizm/tempus/pull/503
|
||||||
|
* feat: tile size manager by @MaFo-28 in https://github.com/eddyizm/tempus/pull/440
|
||||||
|
* chore(i18n): Translated to zh_TW by @olivertzeng in https://github.com/eddyizm/tempus/pull/494
|
||||||
|
* fix: Show full album name when displaying details by @jaime-grj in https://github.com/eddyizm/tempus/pull/508
|
||||||
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/509
|
||||||
|
* fix: Relocate "Offline mode" text by @jaime-grj in https://github.com/eddyizm/tempus/pull/510
|
||||||
|
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/516
|
||||||
|
* refactor: navigation and bottom sheet by @tvillega in https://github.com/eddyizm/tempus/pull/491
|
||||||
|
* feat: Logo refresh by @eddyizm in https://github.com/eddyizm/tempus/pull/498
|
||||||
|
* feat: Add 'genres' page/function to Android Auto by @Jorilx in https://github.com/eddyizm/tempus/pull/505
|
||||||
|
* feat: Added all-songs feature by @unknown0816 in https://github.com/eddyizm/tempus/pull/517
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @NikkoFox made their first contribution in https://github.com/eddyizm/tempus/pull/503
|
||||||
|
* @olivertzeng made their first contribution in https://github.com/eddyizm/tempus/pull/494
|
||||||
|
* @Jorilx made their first contribution in https://github.com/eddyizm/tempus/pull/505
|
||||||
|
* @unknown0816 made their first contribution in https://github.com/eddyizm/tempus/pull/517
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.6...v4.13.0
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.12.6](https://github.com/eddyizm/tempo/releases/tag/v4.12.6) (2026-03-06)
|
||||||
|
* doc: update USAGE with android auto configuration by @MaFo-28 in https://github.com/eddyizm/tempus/pull/481
|
||||||
|
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/483
|
||||||
|
* fix: remove material you dynamic theming by @tvillega in https://github.com/eddyizm/tempus/pull/484
|
||||||
|
* fix: collapse sheet on navitation change by @tvillega in https://github.com/eddyizm/tempus/pull/482
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.4...v4.12.5
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.12.4](https://github.com/eddyizm/tempo/releases/tag/v4.12.4) (2026-03-01)
|
||||||
|
* feat: advertise existing long press to refresh per section on library page by @tvillega in https://github.com/eddyizm/tempus/pull/467
|
||||||
|
* fix: playlist filter returns properly filtered list and reset correctly by @eddyizm in https://github.com/eddyizm/tempus/pull/476
|
||||||
|
* feat: toggle player bitrate visibility on touch by @tvillega in https://github.com/eddyizm/tempus/pull/466
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.0...v4.12.3
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.12.0](https://github.com/eddyizm/tempo/releases/tag/v4.12.0) (2026-02-28)
|
||||||
|
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/441
|
||||||
|
* feat: radio logos support for AndroidAuto by @dmachard in https://github.com/eddyizm/tempus/pull/435
|
||||||
|
* feat: Port remove song of playlist from tempus ng by @tvillega in https://github.com/eddyizm/tempus/pull/457
|
||||||
|
* fix: artist sort by name case sensitive by @tvillega in https://github.com/eddyizm/tempus/pull/462
|
||||||
|
* feat: added slide out enhanced navigation for tab mode and optionally portrait mode by @tvillega in https://github.com/eddyizm/tempus/pull/450
|
||||||
|
* feat: Android Auto: improve media service browsing by @MaFo-28 in https://github.com/eddyizm/tempus/pull/437
|
||||||
|
* feat: Support specifying a client certificate for mTLS auth by @tinsukE in https://github.com/eddyizm/tempus/pull/458
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @MaFo-28 made their first contribution in https://github.com/eddyizm/tempus/pull/437
|
||||||
|
* @tinsukE made their first contribution in https://github.com/eddyizm/tempus/pull/458
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.11.0...v4.12.0
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
## [4.11.0](https://github.com/eddyizm/tempo/releases/tag/v4.11.0) (2026-02-15)
|
## [4.11.0](https://github.com/eddyizm/tempo/releases/tag/v4.11.0) (2026-02-15)
|
||||||
* fix: added dynamic application id from gradle variant by @eddyizm in https://github.com/eddyizm/tempus/pull/425
|
* fix: added dynamic application id from gradle variant by @eddyizm in https://github.com/eddyizm/tempus/pull/425
|
||||||
|
|||||||
20
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Tempus" title="Tempus" src="mockup/svg/tempus_horizontal_logo.png" width="250">
|
<img alt="Tempus" title="Tempus" src="mockup/svg/tempus-horizontal-banner.png" width="250">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -84,13 +84,13 @@ Please note the two variants in the release assets include release/debug and 32/
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_light.png" width=200>
|
<img src="mockup/1_light_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_light.png" width=200>
|
<img src="mockup/2_light_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_light.png" width=200>
|
<img src="mockup/3_light_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_light.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_light.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_light.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_light.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_light.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_light.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_light.png" width=200>
|
<!-- <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_light.png" width=200> -->
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -100,13 +100,13 @@ Please note the two variants in the release assets include release/debug and 32/
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_dark.png" width=200>
|
<img src="mockup/1_dark_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_dark.png" width=200>
|
<img src="mockup/2_dark_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_dark.png" width=200>
|
<img src="mockup/3_dark_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_dark.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_dark.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_dark.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_dark.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_dark.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_dark.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_dark.png" width=200>
|
<!-- <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_dark.png" width=200> -->
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -136,4 +136,4 @@ Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel fr
|
|||||||
## Credits
|
## Credits
|
||||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
|
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
|
||||||
|
|
||||||
[Opensvg.org](https://opensvg.org) for the new turntable logo.
|
[SeattleGuy](https://github.com/SeattleGuy) for the new logo design.
|
||||||
|
|||||||
58
USAGE.md
@@ -158,7 +158,8 @@ If your server supports it - add a internet radio station feed
|
|||||||
|
|
||||||
## Android Auto
|
## Android Auto
|
||||||
|
|
||||||
### Enabling on your head unit
|
**Enabling on your head unit**
|
||||||
|
|
||||||
To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options".
|
To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options".
|
||||||
1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission.
|
1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission.
|
||||||
<p align="left">
|
<p align="left">
|
||||||
@@ -177,6 +178,61 @@ To allow the Tempus app on your car's head unit, "Unknown sources" needs to be e
|
|||||||
<img width="270" height="600" alt="3" src="https://github.com/user-attachments/assets/37db88e9-1b76-417f-9c47-da9f3a750fff" />
|
<img width="270" height="600" alt="3" src="https://github.com/user-attachments/assets/37db88e9-1b76-417f-9c47-da9f3a750fff" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
**Interface Configuration**
|
||||||
|
|
||||||
|
The Android Auto interface can be configured by user to best suit their preferences.
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/aa_preferences.png" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/aa_functions.png" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
4 tabs can be configured with the following functions:
|
||||||
|
- Do not display : This tab is not used
|
||||||
|
- Home : Displays all functions not used in other tabs
|
||||||
|
- Recent : The 15 most recently listened-to albums
|
||||||
|
- Albums : Albums sorted by name
|
||||||
|
- Artists : Albums sorted by artist
|
||||||
|
- Playlists
|
||||||
|
- Podcast : The 100 podcasts recently added
|
||||||
|
- Radio
|
||||||
|
- Folder : Navigation through music directories
|
||||||
|
- Albums most played : The 15 most played albums
|
||||||
|
- Albums added : The 15 recently added albums
|
||||||
|
- Star tracks
|
||||||
|
- Star albums
|
||||||
|
- Star artists
|
||||||
|
- Random : 100 random songs
|
||||||
|
- Genres : 500 songs of the chosen genre OR 100 random songs if "shuffle genre songs" is selected
|
||||||
|
|
||||||
|
If all tabs are set to "Do not display", then "Home" tab will be created with all functions inside.
|
||||||
|
|
||||||
|
If "Home" is selected after another tab, it becomes "More"
|
||||||
|
|
||||||
|
In addition, you can choose to display the following functions as thumbnails or lists:
|
||||||
|
- Home
|
||||||
|
- Albums (Last played, Most played, Recently added, Artists, Star tracks, Star albums, Star artists, Random)
|
||||||
|
- Playlists
|
||||||
|
- Radio
|
||||||
|
- Podcast
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/aa_thumbnails.jpg" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/aa_list.jpg" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The A-Z button allows you to jump to items starting with the chosen letter.
|
||||||
|
|
||||||
|
Search button returns albums or artists, even if they are not displayed by the selected function.
|
||||||
|
|
||||||
|
Results of the A-Z jump or search will always be displayed as a list.
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/aa_AZ.jpg" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/aa_search.jpg" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Display of albums and artists is limited to 500. For large libraries, it's preferable to use star albums or star artists.
|
||||||
|
|
||||||
### Server Settings
|
### Server Settings
|
||||||
**IN PROGRESS**
|
**IN PROGRESS**
|
||||||
|
|||||||
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
markdown: GFM
|
||||||
@@ -10,8 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 20
|
versionCode 24
|
||||||
versionName '4.11.0'
|
versionName '4.13.0'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
@@ -101,6 +101,7 @@ dependencies {
|
|||||||
implementation 'androidx.room:room-runtime:2.6.1'
|
implementation 'androidx.room:room-runtime:2.6.1'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0"
|
||||||
|
|
||||||
// Android Material
|
// Android Material
|
||||||
implementation 'com.google.android.material:material:1.10.0'
|
implementation 'com.google.android.material:material:1.10.0'
|
||||||
|
|||||||
1164
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
Normal file
|
Before Width: | Height: | Size: 20 KiB |
10
app/src/degoogled/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF36C12C"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
||||||
@@ -1,54 +1,78 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="108">
|
||||||
<group android:scaleX="0.49"
|
|
||||||
android:scaleY="0.49"
|
|
||||||
android:translateX="130.56"
|
|
||||||
android:translateY="130.56">
|
|
||||||
|
|
||||||
<path
|
<group
|
||||||
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
|
android:scaleX="0.13"
|
||||||
android:fillColor="#8CC152"/> <path
|
android:scaleY="0.13"
|
||||||
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
|
android:translateX="21.5"
|
||||||
android:fillColor="#62A43B"/> <path
|
android:translateY="21.5">
|
||||||
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
|
|
||||||
android:fillColor="#8CC152"/> <path
|
<path
|
||||||
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
<path
|
<gradient
|
||||||
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
|
android:startX="122.34"
|
||||||
android:fillColor="#E6E9ED"/>
|
android:startY="23.55"
|
||||||
<path
|
android:endX="377.69"
|
||||||
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
|
android:endY="465.83"
|
||||||
android:fillColor="#E6E9ED"/>
|
android:type="linear">
|
||||||
<path
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
android:fillColor="#E6E9ED"/>
|
</gradient>
|
||||||
<path
|
</aapt:attr>
|
||||||
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
|
</path>
|
||||||
android:fillColor="#CCD1D9"/>
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
android:fillColor="#434A54"/>
|
<aapt:attr name="android:fillColor">
|
||||||
<path
|
<gradient
|
||||||
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
|
android:startX="116.21"
|
||||||
android:fillColor="#656D78"/>
|
android:startY="67.61"
|
||||||
<path
|
android:endX="403.29"
|
||||||
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
|
android:endY="429.34"
|
||||||
android:fillColor="#656D78"/>
|
android:type="linear">
|
||||||
<path
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
android:fillColor="#FFCE54"/>
|
</gradient>
|
||||||
<path
|
</aapt:attr>
|
||||||
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
|
</path>
|
||||||
android:fillColor="#F6BB42"/>
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
android:fillColor="#434A54"/>
|
<aapt:attr name="android:fillColor">
|
||||||
<path
|
<gradient
|
||||||
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
|
android:startX="420.63"
|
||||||
android:fillColor="#434A54"/>
|
android:startY="403.74"
|
||||||
</group>
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#66FFFFFF"
|
||||||
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,53 +1,77 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="108">
|
||||||
<group android:scaleX="0.55"
|
|
||||||
android:scaleY="0.55"
|
<group
|
||||||
android:translateX="150.56"
|
android:scaleX="0.13"
|
||||||
android:translateY="150.56">
|
android:scaleY="0.13"
|
||||||
|
android:translateX="21.5"
|
||||||
|
android:translateY="21.5">
|
||||||
<path
|
<path
|
||||||
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
android:fillColor="#8CC152"/> <path
|
<aapt:attr name="android:fillColor">
|
||||||
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
|
<gradient
|
||||||
android:fillColor="#62A43B"/> <path
|
android:startX="122.34"
|
||||||
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
|
android:startY="23.55"
|
||||||
android:fillColor="#8CC152"/> <path
|
android:endX="377.69"
|
||||||
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
|
android:endY="465.83"
|
||||||
android:fillColor="#E6E9ED"/>
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="116.21"
|
||||||
|
android:startY="67.61"
|
||||||
|
android:endX="403.29"
|
||||||
|
android:endY="429.34"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
|
android:fillColor="#66FFFFFF"
|
||||||
android:fillColor="#CCD1D9"/>
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
<path
|
</group>
|
||||||
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
|
|
||||||
android:fillColor="#434A54"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
|
|
||||||
android:fillColor="#656D78"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
|
|
||||||
android:fillColor="#656D78"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
|
|
||||||
android:fillColor="#FFCE54"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
|
|
||||||
android:fillColor="#F6BB42"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
|
|
||||||
android:fillColor="#434A54"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
|
|
||||||
android:fillColor="#434A54"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
</vector>
|
||||||
77
app/src/degoogled/res/drawable/ic_toolbar_tempo.xml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="522">
|
||||||
|
<group
|
||||||
|
android:scaleX="1.0"
|
||||||
|
android:scaleY="1.0"
|
||||||
|
android:translateX="14.0"
|
||||||
|
android:translateY="14.0">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="122.34"
|
||||||
|
android:startY="23.55"
|
||||||
|
android:endX="377.69"
|
||||||
|
android:endY="465.83"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="116.21"
|
||||||
|
android:startY="67.61"
|
||||||
|
android:endX="403.29"
|
||||||
|
android:endY="429.34"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#66FFFFFF"
|
||||||
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
78
app/src/degoogled/res/drawable/logo.xml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:scaleX="0.16"
|
||||||
|
android:scaleY="0.16"
|
||||||
|
android:translateX="14.0"
|
||||||
|
android:translateY="14.0">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="122.34"
|
||||||
|
android:startY="23.55"
|
||||||
|
android:endX="377.69"
|
||||||
|
android:endY="465.83"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="116.21"
|
||||||
|
android:startY="67.61"
|
||||||
|
android:endX="403.29"
|
||||||
|
android:endY="429.34"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#66FFFFFF"
|
||||||
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#626A75</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -11,6 +11,7 @@ import com.cappielloantonio.tempo.github.Github;
|
|||||||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||||
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
||||||
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
|
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
|
||||||
|
import com.cappielloantonio.tempo.util.ClientCertManager;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
@@ -31,6 +32,8 @@ public class App extends Application {
|
|||||||
instance = new App();
|
instance = new App();
|
||||||
context = getApplicationContext();
|
context = getApplicationContext();
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
ClientCertManager.setupSslSocketFactory(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static App getInstance() {
|
public static App getInstance() {
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Database(
|
@Database(
|
||||||
version = 13,
|
version = 14,
|
||||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
||||||
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
autoMigrations = {
|
||||||
|
@AutoMigration(from = 10, to = 11),
|
||||||
|
@AutoMigration(from = 11, to = 12),
|
||||||
|
@AutoMigration(from = 13, to = 14),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@TypeConverters({DateConverters.class})
|
@TypeConverters({DateConverters.class})
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public interface PlaylistDao {
|
|||||||
@Query("SELECT * FROM playlist")
|
@Query("SELECT * FROM playlist")
|
||||||
LiveData<List<Playlist>> getAll();
|
LiveData<List<Playlist>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlist")
|
||||||
|
List<Playlist> getAllSync();
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
void insert(Playlist playlist);
|
void insert(Playlist playlist);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.annotation.Nullable
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
@@ -35,5 +34,8 @@ data class Server(
|
|||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
|
|
||||||
@ColumnInfo(name = "low_security", defaultValue = "false")
|
@ColumnInfo(name = "low_security", defaultValue = "false")
|
||||||
val isLowSecurity: Boolean
|
val isLowSecurity: Boolean,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "client_cert")
|
||||||
|
val clientCert: String?,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -195,11 +195,20 @@ class SessionMediaItem() {
|
|||||||
title = internetRadioStation.name
|
title = internetRadioStation.name
|
||||||
streamUrl = internetRadioStation.streamUrl
|
streamUrl = internetRadioStation.streamUrl
|
||||||
type = Constants.MEDIA_TYPE_RADIO
|
type = Constants.MEDIA_TYPE_RADIO
|
||||||
|
|
||||||
|
val homePageUrl = internetRadioStation.homePageUrl
|
||||||
|
if (homePageUrl != null && homePageUrl.isNotEmpty() && MusicUtil.isImageUrl(homePageUrl)) {
|
||||||
|
val encodedUrl = android.util.Base64.encodeToString(
|
||||||
|
homePageUrl.toByteArray(java.nio.charset.StandardCharsets.UTF_8),
|
||||||
|
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
coverArtId = "ir_$encodedUrl"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMediaItem(): MediaItem {
|
fun getMediaItem(): MediaItem {
|
||||||
val uri: Uri = getStreamUri()
|
val uri: Uri = getStreamUri()
|
||||||
val artworkUri = AlbumArtContentProvider.contentUri(coverArtId)
|
val artworkUri = if (coverArtId != null) AlbumArtContentProvider.contentUri(coverArtId!!) else null
|
||||||
|
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString("id", id)
|
bundle.putString("id", id)
|
||||||
@@ -229,7 +238,7 @@ class SessionMediaItem() {
|
|||||||
bundle.putLong("starred", starred?.time ?: 0)
|
bundle.putLong("starred", starred?.time ?: 0)
|
||||||
bundle.putString("albumId", albumId)
|
bundle.putString("albumId", albumId)
|
||||||
bundle.putString("artistId", artistId)
|
bundle.putString("artistId", artistId)
|
||||||
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC)
|
bundle.putString("type", type)
|
||||||
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
|
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
|
||||||
bundle.putInt("originalWidth", originalWidth ?: 0)
|
bundle.putInt("originalWidth", originalWidth ?: 0)
|
||||||
bundle.putInt("originalHeight", originalHeight ?: 0)
|
bundle.putInt("originalHeight", originalHeight ?: 0)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.cappielloantonio.tempo.navigation;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
public class NavigationController {
|
||||||
|
|
||||||
|
NavigationHelper helper;
|
||||||
|
|
||||||
|
public NavigationController(@NonNull NavigationHelper helper) {
|
||||||
|
this.helper = helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWithBottomSheetBehavior(BottomSheetBehavior<View> bottomSheetBehavior,
|
||||||
|
NavController navController) {
|
||||||
|
helper.syncWithBottomSheetBehavior(bottomSheetBehavior, navController);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNavbarVisibility(boolean visibility) {
|
||||||
|
helper.setBottomNavigationBarVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDrawerLock(boolean visibility) {
|
||||||
|
helper.setNavigationDrawerLock(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigationDrawerLocked() {
|
||||||
|
return helper.isNavigationDrawerLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleDrawerLockOnOrientation(AppCompatActivity activity) {
|
||||||
|
helper.toggleNavigationDrawerLockOnOrientationChange(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSystemBarsVisibility(AppCompatActivity activity, boolean visibility) {
|
||||||
|
helper.setSystemBarsVisibility(activity, visibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.cappielloantonio.tempo.navigation;
|
||||||
|
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat;
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavDestination;
|
||||||
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.navigation.ui.NavigationUI;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
import com.google.android.material.navigation.NavigationView;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
|
public class NavigationHelper {
|
||||||
|
/* UI components */
|
||||||
|
private BottomNavigationView bottomNavigationView;
|
||||||
|
private FrameLayout bottomNavigationViewFrame;
|
||||||
|
private DrawerLayout drawerLayout;
|
||||||
|
|
||||||
|
/* Navigation components */
|
||||||
|
private NavigationView navigationView;
|
||||||
|
private NavHostFragment navHostFragment;
|
||||||
|
|
||||||
|
/* States that need to be remembered */
|
||||||
|
// -- //
|
||||||
|
|
||||||
|
/* Private constructor */
|
||||||
|
public NavigationHelper(@NonNull BottomNavigationView bottomNavigationView,
|
||||||
|
@NonNull FrameLayout bottomNavigationViewFrame,
|
||||||
|
@NonNull DrawerLayout drawerLayout,
|
||||||
|
@NonNull NavigationView navigationView,
|
||||||
|
@NonNull NavHostFragment navHostFragment) {
|
||||||
|
this.bottomNavigationView = bottomNavigationView;
|
||||||
|
this.bottomNavigationViewFrame = bottomNavigationViewFrame;
|
||||||
|
this.drawerLayout = drawerLayout;
|
||||||
|
this.navigationView = navigationView;
|
||||||
|
this.navHostFragment = navHostFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWithBottomSheetBehavior(@NonNull BottomSheetBehavior<View> bottomSheetBehavior,
|
||||||
|
@NonNull NavController navController) {
|
||||||
|
navController.addOnDestinationChangedListener(
|
||||||
|
(controller, destination, arguments) -> {
|
||||||
|
// React to the user clicking one of these on bottom-navbar/drawer
|
||||||
|
boolean isTarget = isTargetDestination(destination);
|
||||||
|
int currentState = bottomSheetBehavior.getState();
|
||||||
|
|
||||||
|
if (isTarget && currentState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NavigationUI.setupWithNavController(bottomNavigationView, navController);
|
||||||
|
NavigationUI.setupWithNavController(navigationView, navController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract(pure = true)
|
||||||
|
private static boolean isTargetDestination(NavDestination destination) {
|
||||||
|
int destId = destination.getId();
|
||||||
|
return destId == R.id.homeFragment ||
|
||||||
|
destId == R.id.libraryFragment ||
|
||||||
|
destId == R.id.downloadFragment ||
|
||||||
|
destId == R.id.albumCatalogueFragment ||
|
||||||
|
destId == R.id.artistCatalogueFragment ||
|
||||||
|
destId == R.id.genreCatalogueFragment ||
|
||||||
|
destId == R.id.playlistCatalogueFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Clean public methods
|
||||||
|
Removes the need to invoke the activity on the fragment
|
||||||
|
*/
|
||||||
|
|
||||||
|
public void setBottomNavigationBarVisibility(boolean visible) {
|
||||||
|
int visibility = visible
|
||||||
|
? View.VISIBLE
|
||||||
|
: View.GONE;
|
||||||
|
bottomNavigationView.setVisibility(visibility);
|
||||||
|
bottomNavigationViewFrame.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNavigationDrawerLock(boolean locked) {
|
||||||
|
int mode = locked
|
||||||
|
? DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||||
|
: DrawerLayout.LOCK_MODE_UNLOCKED;
|
||||||
|
drawerLayout.setDrawerLockMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigationDrawerLocked() {
|
||||||
|
return drawerLayout.getDrawerLockMode(navigationView) != DrawerLayout.LOCK_MODE_UNLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public void toggleNavigationDrawerLockOnOrientationChange(
|
||||||
|
AppCompatActivity activity) {
|
||||||
|
|
||||||
|
int orientation = activity.getResources().getConfiguration().orientation;
|
||||||
|
boolean isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||||
|
|
||||||
|
if (Preferences.getEnableDrawerOnPortrait()) {
|
||||||
|
setNavigationDrawerLock(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNavigationDrawerLock(!isLandscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
All of these are the "backward compatible" changes that don't break the assumption
|
||||||
|
that everything was defined on the activity and is gobally available
|
||||||
|
*/
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public BottomNavigationView getBottomNavigationView() {
|
||||||
|
return bottomNavigationView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public FrameLayout getBottomNavigationViewFrame() {
|
||||||
|
return bottomNavigationViewFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public DrawerLayout getDrawerLayout() {
|
||||||
|
return drawerLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Auxiliar functions, could be moved somewhere else
|
||||||
|
*/
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public void setSystemBarsVisibility(AppCompatActivity activity, boolean visibility) {
|
||||||
|
WindowInsetsControllerCompat insetsController;
|
||||||
|
Window window = activity.getWindow();
|
||||||
|
View decorView = window.getDecorView();
|
||||||
|
insetsController = new WindowInsetsControllerCompat(window, decorView);
|
||||||
|
|
||||||
|
if (visibility) {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, true);
|
||||||
|
insetsController.show(WindowInsetsCompat.Type.navigationBars());
|
||||||
|
insetsController.show(WindowInsetsCompat.Type.statusBars());
|
||||||
|
insetsController.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT);
|
||||||
|
} else {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false);
|
||||||
|
insetsController.hide(WindowInsetsCompat.Type.navigationBars());
|
||||||
|
insetsController.hide(WindowInsetsCompat.Type.statusBars());
|
||||||
|
insetsController.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.content.UriMatcher;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -53,7 +54,15 @@ public class AlbumArtContentProvider extends ContentProvider {
|
|||||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
String albumId = uri.getLastPathSegment();
|
String albumId = uri.getLastPathSegment();
|
||||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
|
Uri artworkUri;
|
||||||
|
|
||||||
|
if (albumId != null && albumId.startsWith("ir_")) {
|
||||||
|
String encodedUrl = albumId.substring("ir_".length());
|
||||||
|
String decodedUrl = new String(Base64.decode(encodedUrl, Base64.URL_SAFE | Base64.NO_WRAP));
|
||||||
|
artworkUri = Uri.parse(decodedUrl);
|
||||||
|
} else {
|
||||||
|
artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// use pipe to communicate between background thread and caller of openFile()
|
// use pipe to communicate between background thread and caller of openFile()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -36,6 +35,7 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Genre;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
@@ -69,6 +69,16 @@ public class AutomotiveRepository {
|
|||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
||||||
List<AlbumID3> albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums();
|
List<AlbumID3> albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums();
|
||||||
|
|
||||||
|
// add by MFO
|
||||||
|
// Hack for artist view
|
||||||
|
if("alphabeticalByArtist".equals(type))for(AlbumID3 album : albums){
|
||||||
|
String artistName = album.getArtist();
|
||||||
|
String albumName = album.getName();
|
||||||
|
album.setName(artistName);
|
||||||
|
album.setArtist(albumName);
|
||||||
|
}
|
||||||
|
// end add by MFO
|
||||||
|
|
||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (AlbumID3 album : albums) {
|
for (AlbumID3 album : albums) {
|
||||||
@@ -780,7 +790,7 @@ public class AutomotiveRepository {
|
|||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 20, 20, 20)
|
.search3(query, 20, 0, 20, 0, 20, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
@@ -943,6 +953,116 @@ public class AutomotiveRepository {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getGenres(String prefix) {
|
||||||
|
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
|
||||||
|
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getBrowsingClient()
|
||||||
|
.getGenres()
|
||||||
|
.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().getGenres() != null && response.body().getSubsonicResponse().getGenres().getGenres() != null) {
|
||||||
|
List<Genre> genres = response.body().getSubsonicResponse().getGenres().getGenres();
|
||||||
|
|
||||||
|
// Sort genres alphabetically by name
|
||||||
|
genres.sort((g1, g2) -> {
|
||||||
|
String name1 = g1.getGenre() != null ? g1.getGenre() : "";
|
||||||
|
String name2 = g2.getGenre() != null ? g2.getGenre() : "";
|
||||||
|
return name1.compareToIgnoreCase(name2);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Genre genre : genres) {
|
||||||
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
|
.setTitle(genre.getGenre())
|
||||||
|
.setIsBrowsable(true)
|
||||||
|
.setIsPlayable(false)
|
||||||
|
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
MediaItem mediaItem = new MediaItem.Builder()
|
||||||
|
.setMediaId(prefix + genre.getGenre())
|
||||||
|
.setMediaMetadata(mediaMetadata)
|
||||||
|
.setUri("")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mediaItems.add(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
|
||||||
|
|
||||||
|
listenableFuture.set(libraryResult);
|
||||||
|
} else {
|
||||||
|
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
listenableFuture.setException(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return listenableFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getSongsByGenre(String genre, int count, boolean shuffle) {
|
||||||
|
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
|
||||||
|
|
||||||
|
Call<ApiResponse> call;
|
||||||
|
if (shuffle) {
|
||||||
|
call = App.getSubsonicClientInstance(false)
|
||||||
|
.getAlbumSongListClient()
|
||||||
|
.getRandomSongs(count, null, null, genre);
|
||||||
|
} else {
|
||||||
|
call = App.getSubsonicClientInstance(false)
|
||||||
|
.getAlbumSongListClient()
|
||||||
|
.getSongsByGenre(genre, count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
call.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
List<com.cappielloantonio.tempo.subsonic.models.Child> songs;
|
||||||
|
if (shuffle) {
|
||||||
|
songs = response.body().getSubsonicResponse().getRandomSongs() != null
|
||||||
|
? response.body().getSubsonicResponse().getRandomSongs().getSongs()
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
songs = response.body().getSubsonicResponse().getSongsByGenre() != null
|
||||||
|
? response.body().getSubsonicResponse().getSongsByGenre().getSongs()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songs != null) {
|
||||||
|
setChildrenMetadata(songs);
|
||||||
|
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(songs);
|
||||||
|
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
|
||||||
|
listenableFuture.set(libraryResult);
|
||||||
|
} else {
|
||||||
|
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
listenableFuture.setException(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return listenableFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getSongsByGenre(String genre, int count) {
|
||||||
|
return getSongsByGenre(genre, count, false);
|
||||||
|
}
|
||||||
|
|
||||||
private static class GetMediaItemThreadSafe implements Runnable {
|
private static class GetMediaItemThreadSafe implements Runnable {
|
||||||
private final SessionMediaItemDao sessionMediaItemDao;
|
private final SessionMediaItemDao sessionMediaItemDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package com.cappielloantonio.tempo.repository;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
@@ -23,8 +26,45 @@ import retrofit2.Callback;
|
|||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class PlaylistRepository {
|
public class PlaylistRepository {
|
||||||
|
private static final MutableLiveData<Boolean> playlistUpdateTrigger = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public LiveData<Boolean> getPlaylistUpdateTrigger() {
|
||||||
|
return playlistUpdateTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyPlaylistChanged() {
|
||||||
|
playlistUpdateTrigger.postValue(true);
|
||||||
|
refreshAllPlaylists();
|
||||||
|
}
|
||||||
|
|
||||||
@androidx.media3.common.util.UnstableApi
|
@androidx.media3.common.util.UnstableApi
|
||||||
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
||||||
|
private static final MutableLiveData<List<Playlist>> allPlaylistsLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public LiveData<List<Playlist>> getAllPlaylists(LifecycleOwner owner) {
|
||||||
|
refreshAllPlaylists();
|
||||||
|
return allPlaylistsLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshAllPlaylists() {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylists()
|
||||||
|
.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().getPlaylists() != null) {
|
||||||
|
List<Playlist> playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||||
|
allPlaylistsLiveData.postValue(playlists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
||||||
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
@@ -104,9 +144,16 @@ public class PlaylistRepository {
|
|||||||
return playlistLiveData;
|
return playlistLiveData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
public interface AddToPlaylistCallback {
|
||||||
|
void onSuccess();
|
||||||
|
void onFailure();
|
||||||
|
void onAllSkipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic, AddToPlaylistCallback callback) {
|
||||||
|
android.util.Log.d("PlaylistRepository", "addSongToPlaylist: id=" + playlistId + ", songs=" + songsId);
|
||||||
if (songsId.isEmpty()) {
|
if (songsId.isEmpty()) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
if (callback != null) callback.onAllSkipped();
|
||||||
} else{
|
} else{
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
@@ -114,17 +161,45 @@ public class PlaylistRepository {
|
|||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
|
if (callback != null) callback.onSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
|
if (callback != null) callback.onFailure();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeSongFromPlaylist(String playlistId, int index, AddToPlaylistCallback callback) {
|
||||||
|
ArrayList<Integer> indexes = new ArrayList<>();
|
||||||
|
indexes.add(index);
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.updatePlaylist(playlistId, null, true, null, indexes)
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
|
if (callback != null) {
|
||||||
|
if (response.isSuccessful()) callback.onSuccess();
|
||||||
|
else callback.onFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
if (callback != null) callback.onFailure();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||||
|
addSongToPlaylist(playlistId, songsId, playlistVisibilityIsPublic, null);
|
||||||
|
}
|
||||||
|
|
||||||
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
@@ -132,7 +207,7 @@ public class PlaylistRepository {
|
|||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -145,20 +220,45 @@ public class PlaylistRepository {
|
|||||||
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.deletePlaylist(playlistId)
|
.updatePlaylist(playlistId, name, true, null, null)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
createPlaylist(null, name, songsId);
|
if (response.isSuccessful()) {
|
||||||
|
// After renaming, we need to handle the song list update.
|
||||||
|
// Subsonic doesn't have a "replace all songs" in updatePlaylist.
|
||||||
|
// So we might still need to recreate if the songs changed significantly,
|
||||||
|
// but if we just renamed, we should update the local pinned database.
|
||||||
|
updateLocalPinnedPlaylistName(playlistId, name);
|
||||||
|
notifyPlaylistChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If songsId is provided, we might want to re-sync them.
|
||||||
|
// For now, let's at least fix the name duplication issue.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
private void updateLocalPinnedPlaylistName(String id, String newName) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Playlist> pinned = playlistDao.getAllSync();
|
||||||
|
if (pinned != null) {
|
||||||
|
for (Playlist p : pinned) {
|
||||||
|
if (p.getId().equals(id)) {
|
||||||
|
p.setName(newName);
|
||||||
|
playlistDao.insert(p); // Replace strategy will update it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
public void deletePlaylist(String playlistId) {
|
public void deletePlaylist(String playlistId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
@@ -166,7 +266,7 @@ public class PlaylistRepository {
|
|||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -194,6 +294,49 @@ public class PlaylistRepository {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
|
public void updatePinnedPlaylists() {
|
||||||
|
updatePinnedPlaylists(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
|
public void updatePinnedPlaylists(List<String> forceIds) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Playlist> pinned = playlistDao.getAllSync();
|
||||||
|
if (pinned != null && !pinned.isEmpty()) {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylists()
|
||||||
|
.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().getPlaylists() != null) {
|
||||||
|
List<Playlist> remotes = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||||
|
new Thread(() -> {
|
||||||
|
for (Playlist p : pinned) {
|
||||||
|
for (Playlist r : remotes) {
|
||||||
|
if (p.getId().equals(r.getId())) {
|
||||||
|
p.setName(r.getName());
|
||||||
|
p.setSongCount(r.getSongCount());
|
||||||
|
p.setDuration(r.getDuration());
|
||||||
|
p.setCoverArtId(r.getCoverArtId());
|
||||||
|
playlistDao.insert(p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
private static class InsertThreadSafe implements Runnable {
|
private static class InsertThreadSafe implements Runnable {
|
||||||
private final PlaylistDao playlistDao;
|
private final PlaylistDao playlistDao;
|
||||||
private final Playlist playlist;
|
private final Playlist playlist;
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||||
@@ -11,13 +16,18 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.ui.fragment.SearchFragment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -31,7 +41,7 @@ public class SearchingRepository {
|
|||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 20, 20, 20)
|
.search3(query, 20, 0, 20, 0, 20, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
@@ -49,12 +59,63 @@ public class SearchingRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<SearchResult3> search3(String query) {
|
@UnstableApi
|
||||||
|
public MutableLiveData<SearchResult3> search3(SearchFragment sf, String query) {
|
||||||
MutableLiveData<SearchResult3> result = new MutableLiveData<>();
|
MutableLiveData<SearchResult3> result = new MutableLiveData<>();
|
||||||
|
|
||||||
|
Executors.newSingleThreadExecutor().execute(() -> {
|
||||||
|
List<Child> allSongs = new ArrayList<>();
|
||||||
|
int offset = 0;
|
||||||
|
int limit = 1000;
|
||||||
|
boolean hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
try {
|
||||||
|
Response<ApiResponse> response = App.getSubsonicClientInstance(false)
|
||||||
|
.getSearchingClient()
|
||||||
|
.search3(query, limit, offset, 0, 0, 0, 0)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
SearchResult3 tmp = response.body().getSubsonicResponse().getSearchResult3();
|
||||||
|
if (tmp != null && tmp.getSongs() != null && !tmp.getSongs().isEmpty()) {
|
||||||
|
List<Child> fetchedSongs = tmp.getSongs();
|
||||||
|
allSongs.addAll(fetchedSongs);
|
||||||
|
|
||||||
|
offset += fetchedSongs.size();
|
||||||
|
hasMore = fetchedSongs.size() == limit;
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlaylistWithSongs pws = new PlaylistWithSongs("allsongs", allSongs);
|
||||||
|
pws.setName(sf.getView().getContext().getString(R.string.search_all_songs, String.valueOf(allSongs.size())));
|
||||||
|
pws.setSongCount(allSongs.size());
|
||||||
|
List<Playlist> lpws = new ArrayList<>();
|
||||||
|
lpws.add(pws);
|
||||||
|
long duration = 0;
|
||||||
|
for (Child song: allSongs) {
|
||||||
|
if (song != null && song.getDuration() != null) {
|
||||||
|
duration += song.getDuration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pws.setDuration(duration);
|
||||||
|
|
||||||
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
|
sf.updateUI(lpws);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 20, 20, 20)
|
.search3(query, 20, 0, 20, 0, 20, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
@@ -77,7 +138,7 @@ public class SearchingRepository {
|
|||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 5, 5, 5)
|
.search3(query, 5, 0, 5, 0, 5, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.subsonic
|
|||||||
import com.cappielloantonio.tempo.App
|
import com.cappielloantonio.tempo.App
|
||||||
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
||||||
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
||||||
|
import com.cappielloantonio.tempo.util.ClientCertManager
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -13,7 +14,7 @@ import java.util.Date
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class RetrofitClient(subsonic: Subsonic) {
|
class RetrofitClient(subsonic: Subsonic) {
|
||||||
var retrofit: Retrofit
|
val retrofit: Retrofit
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
@@ -50,6 +51,7 @@ class RetrofitClient(subsonic: Subsonic) {
|
|||||||
.addInterceptor(cacheUtil.offlineInterceptor)
|
.addInterceptor(cacheUtil.offlineInterceptor)
|
||||||
// .addNetworkInterceptor(cacheUtil.onlineInterceptor)
|
// .addNetworkInterceptor(cacheUtil.onlineInterceptor)
|
||||||
.cache(getCache())
|
.cache(getCache())
|
||||||
|
.setupSsl()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,4 +65,11 @@ class RetrofitClient(subsonic: Subsonic) {
|
|||||||
val cacheSize = 10 * 1024 * 1024
|
val cacheSize = 10 * 1024 * 1024
|
||||||
return Cache(App.getContext().cacheDir, cacheSize.toLong())
|
return Cache(App.getContext().cacheDir, cacheSize.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun OkHttpClient.Builder.setupSsl(): OkHttpClient.Builder {
|
||||||
|
ClientCertManager.sslSocketFactory?.let { sslSocketFactory ->
|
||||||
|
sslSocketFactory(sslSocketFactory, ClientCertManager.trustManager)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,8 @@ public class SearchingClient {
|
|||||||
return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount);
|
return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Call<ApiResponse> search3(String query, int songCount, int albumCount, int artistCount) {
|
public Call<ApiResponse> search3(String query, int songCount, int songOffset, int albumCount, int albumOffset, int artistCount, int artistOffset) {
|
||||||
Log.d(TAG, "search3()");
|
Log.d(TAG, "search3()");
|
||||||
return searchingService.search3(subsonic.getParams(), query, songCount, albumCount, artistCount);
|
return searchingService.search3(subsonic.getParams(), query, songCount, songOffset, albumCount, albumOffset, artistCount, artistOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ public interface SearchingService {
|
|||||||
Call<ApiResponse> search2(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
|
Call<ApiResponse> search2(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
|
||||||
|
|
||||||
@GET("search3")
|
@GET("search3")
|
||||||
Call<ApiResponse> search3(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
|
Call<ApiResponse> search3(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("songOffset") int songOffset, @Query("albumCount") int albumCount, @Query("albumOffset") int albumOffset, @Query("artistCount") int artistCount, @Query("artistOffset") int artistOffset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.subsonic.models
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -10,5 +11,6 @@ class InternetRadioStation(
|
|||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var streamUrl: String? = null,
|
var streamUrl: String? = null,
|
||||||
|
@SerializedName("homePageUrl", alternate = ["homepageUrl"])
|
||||||
var homePageUrl: String? = null,
|
var homePageUrl: String? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -2,31 +2,28 @@ package com.cappielloantonio.tempo.ui.activity;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.navigation.ui.NavigationUI;
|
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.BuildConfig;
|
import com.cappielloantonio.tempo.BuildConfig;
|
||||||
@@ -34,8 +31,12 @@ import com.cappielloantonio.tempo.R;
|
|||||||
import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver;
|
import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver;
|
||||||
import com.cappielloantonio.tempo.databinding.ActivityMainBinding;
|
import com.cappielloantonio.tempo.databinding.ActivityMainBinding;
|
||||||
import com.cappielloantonio.tempo.github.utils.UpdateUtil;
|
import com.cappielloantonio.tempo.github.utils.UpdateUtil;
|
||||||
|
import com.cappielloantonio.tempo.navigation.NavigationController;
|
||||||
|
import com.cappielloantonio.tempo.navigation.NavigationHelper;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.ui.activity.base.BaseActivity;
|
import com.cappielloantonio.tempo.ui.activity.base.BaseActivity;
|
||||||
|
import com.cappielloantonio.tempo.ui.controller.BottomSheetController;
|
||||||
|
import com.cappielloantonio.tempo.ui.controller.BottomSheetHelper;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
||||||
@@ -48,6 +49,7 @@ import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
|||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
import com.google.android.material.color.DynamicColors;
|
import com.google.android.material.color.DynamicColors;
|
||||||
|
import com.google.android.material.navigation.NavigationView;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -63,15 +65,24 @@ public class MainActivity extends BaseActivity {
|
|||||||
private FragmentManager fragmentManager;
|
private FragmentManager fragmentManager;
|
||||||
private NavHostFragment navHostFragment;
|
private NavHostFragment navHostFragment;
|
||||||
private BottomNavigationView bottomNavigationView;
|
private BottomNavigationView bottomNavigationView;
|
||||||
|
private FrameLayout bottomNavigationViewFrame;
|
||||||
|
private DrawerLayout drawerLayout;
|
||||||
|
private NavigationView navigationView;
|
||||||
public NavController navController;
|
public NavController navController;
|
||||||
private BottomSheetBehavior bottomSheetBehavior;
|
private NavigationController navigationController;
|
||||||
private boolean isLandscape = false;
|
private BottomSheetController bottomSheetController;
|
||||||
|
public BottomSheetBehavior bottomSheetBehavior;
|
||||||
|
public boolean isLandscape = false;
|
||||||
private AssetLinkNavigator assetLinkNavigator;
|
private AssetLinkNavigator assetLinkNavigator;
|
||||||
private AssetLinkUtil.AssetLink pendingAssetLink;
|
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||||
|
|
||||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||||
private Intent pendingDownloadPlaybackIntent;
|
private Intent pendingDownloadPlaybackIntent;
|
||||||
|
|
||||||
|
public ActivityMainBinding getBinding() {
|
||||||
|
return bind;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
SplashScreen.installSplashScreen(this);
|
SplashScreen.installSplashScreen(this);
|
||||||
@@ -111,6 +122,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
pingServer();
|
pingServer();
|
||||||
|
toggleNavigationDrawerLockOnOrientationChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -137,7 +149,6 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
fragmentManager = getSupportFragmentManager();
|
|
||||||
|
|
||||||
initBottomSheet();
|
initBottomSheet();
|
||||||
initNavigation();
|
initNavigation();
|
||||||
@@ -148,59 +159,78 @@ public class MainActivity extends BaseActivity {
|
|||||||
goToLogin();
|
goToLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set bottom navigation height
|
toggleNavigationDrawerLockOnOrientationChange();
|
||||||
if (isLandscape) {
|
|
||||||
ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams();
|
|
||||||
Rect windowRect = new Rect();
|
|
||||||
bottomNavigationView.getWindowVisibleDisplayFrame(windowRect);
|
|
||||||
layoutParams.width = windowRect.height();
|
|
||||||
bottomNavigationView.setLayoutParams(layoutParams);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOTTOM SHEET/NAVIGATION
|
private void initNavigation() {
|
||||||
private void initBottomSheet() {
|
// We link the nav_graph.xml with our navigationController
|
||||||
bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet));
|
NavHostFragment navHostFragment = (NavHostFragment) this
|
||||||
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
|
.getSupportFragmentManager()
|
||||||
fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit();
|
.findFragmentById(R.id.nav_host_fragment);
|
||||||
|
navController = Objects.requireNonNull(navHostFragment).getNavController();
|
||||||
|
/*
|
||||||
|
navController is currently global since some legacy code still invokes it directly
|
||||||
|
the MainActivity methods that use it must be converted to NavigationHelper methods
|
||||||
|
*/
|
||||||
|
|
||||||
checkBottomSheetAfterStateChanged();
|
// Helper
|
||||||
|
NavigationHelper navigationHelper =
|
||||||
|
new NavigationHelper(
|
||||||
|
findViewById(R.id.bottom_navigation),
|
||||||
|
findViewById(R.id.bottom_navigation_frame),
|
||||||
|
findViewById(R.id.drawer_layout),
|
||||||
|
findViewById(R.id.nav_view),
|
||||||
|
navHostFragment
|
||||||
|
);
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
navigationController = new NavigationController(navigationHelper);
|
||||||
|
navigationController.syncWithBottomSheetBehavior(bottomSheetBehavior, navController);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initBottomSheet() {
|
||||||
|
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||||
|
View bottomSheetView = findViewById(R.id.player_bottom_sheet);
|
||||||
|
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
|
||||||
|
/*
|
||||||
|
bottomSheetBehavior is currently global since some legacy code still invokes it directly
|
||||||
|
the MainActivity methods that use it must be converted to BottomSheetHelper methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
BottomSheetHelper bottomSheetHelper =
|
||||||
|
new BottomSheetHelper(
|
||||||
|
bottomSheetBehavior,
|
||||||
|
bottomSheetView,
|
||||||
|
fragmentManager
|
||||||
|
);
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
bottomSheetController = new BottomSheetController(bottomSheetHelper);
|
||||||
|
bottomSheetController.addCallback(bottomSheetCallback);
|
||||||
|
bottomSheetController.replaceFragment(R.id.player_bottom_sheet);
|
||||||
|
bottomSheetController.checkAfterStateChanged(mainViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomSheetInPeek(Boolean isVisible) {
|
public void setBottomSheetInPeek(Boolean isVisible) {
|
||||||
if (isVisible) {
|
bottomSheetController.setStateInPeek(isVisible);
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
} else {
|
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomSheetVisibility(boolean visibility) {
|
public void setBottomSheetVisibility(boolean visibility) {
|
||||||
if (visibility) {
|
bottomSheetController.setVisibility(visibility);
|
||||||
findViewById(R.id.player_bottom_sheet).setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
findViewById(R.id.player_bottom_sheet).setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkBottomSheetAfterStateChanged() {
|
|
||||||
final Handler handler = new Handler();
|
|
||||||
final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
|
||||||
handler.postDelayed(runnable, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void collapseBottomSheetDelayed() {
|
public void collapseBottomSheetDelayed() {
|
||||||
final Handler handler = new Handler();
|
bottomSheetController.collapseDelayed();
|
||||||
final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
handler.postDelayed(runnable, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandBottomSheet() {
|
public void expandBottomSheet() {
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
bottomSheetController.expand();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomSheetDraggableState(Boolean isDraggable) {
|
public void setBottomSheetDraggableState(Boolean isDraggable) {
|
||||||
bottomSheetBehavior.setDraggable(isDraggable);
|
bottomSheetController.setDraggable(isDraggable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
|
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
|
||||||
@@ -213,7 +243,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case BottomSheetBehavior.STATE_HIDDEN:
|
case BottomSheetBehavior.STATE_HIDDEN:
|
||||||
resetMusicSession();
|
resetMusicSession(); // I can't put the callback inside BottomSheetHelper because of this line
|
||||||
break;
|
break;
|
||||||
case BottomSheetBehavior.STATE_COLLAPSED:
|
case BottomSheetBehavior.STATE_COLLAPSED:
|
||||||
if (playerBottomSheetFragment != null)
|
if (playerBottomSheetFragment != null)
|
||||||
@@ -237,12 +267,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private void animateBottomSheet(float slideOffset) {
|
private void animateBottomSheet(float slideOffset) {
|
||||||
PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
|
bottomSheetController.animate(slideOffset);
|
||||||
if (playerBottomSheetFragment != null) {
|
|
||||||
float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
|
|
||||||
playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset);
|
|
||||||
playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void animateBottomNavigation(float slideOffset, int navigationHeight) {
|
private void animateBottomNavigation(float slideOffset, int navigationHeight) {
|
||||||
@@ -257,36 +282,57 @@ public class MainActivity extends BaseActivity {
|
|||||||
bind.bottomNavigation.setTranslationY(slideY);
|
bind.bottomNavigation.setTranslationY(slideY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initNavigation() {
|
public void setBottomNavigationBarVisibility(boolean visibility) {
|
||||||
bottomNavigationView = findViewById(R.id.bottom_navigation);
|
navigationController.setNavbarVisibility(visibility);
|
||||||
navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment);
|
|
||||||
navController = Objects.requireNonNull(navHostFragment).getNavController();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet è aperto,
|
|
||||||
* lo chiudo
|
|
||||||
*/
|
|
||||||
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
|
|
||||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && (
|
|
||||||
destination.getId() == R.id.homeFragment ||
|
|
||||||
destination.getId() == R.id.libraryFragment ||
|
|
||||||
destination.getId() == R.id.downloadFragment)
|
|
||||||
) {
|
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
NavigationUI.setupWithNavController(bottomNavigationView, navController);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomNavigationBarVisibility(boolean visibility) {
|
public void toggleBottomNavigationBarVisibilityOnOrientationChange() {
|
||||||
if (visibility) {
|
float displayDensity = getResources().getDisplayMetrics().density;
|
||||||
bottomNavigationView.setVisibility(View.VISIBLE);
|
// Ignore orientation change, bottom navbar always hidden
|
||||||
|
if (Preferences.getHideBottomNavbarOnPortrait()) {
|
||||||
|
navigationController.setNavbarVisibility(false);
|
||||||
|
bottomSheetController.setPeekHeight(56, displayDensity);
|
||||||
|
navigationController.setSystemBarsVisibility(this, !isLandscape);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLandscape) {
|
||||||
|
// Show app navbar + show system bars
|
||||||
|
bottomSheetController.setPeekHeight(136, displayDensity);
|
||||||
|
navigationController.setNavbarVisibility(true);
|
||||||
|
navigationController.setSystemBarsVisibility(this, true);
|
||||||
} else {
|
} else {
|
||||||
bottomNavigationView.setVisibility(View.GONE);
|
// Hide app navbar + hide system bars
|
||||||
|
bottomSheetController.setPeekHeight(56, displayDensity);
|
||||||
|
navigationController.setNavbarVisibility(false);
|
||||||
|
navigationController.setSystemBarsVisibility(this, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setNavigationDrawerLock(boolean locked) {
|
||||||
|
navigationController.setDrawerLock(locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigationDrawerLocked() {
|
||||||
|
return navigationController.isNavigationDrawerLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleNavigationDrawerLockOnOrientationChange() {
|
||||||
|
navigationController.toggleDrawerLockOnOrientation(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSystemBarsVisibility(boolean visibility) {
|
||||||
|
navigationController.setSystemBarsVisibility(this, visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
There are only 4 init functions that must exist up to here
|
||||||
|
1. init()
|
||||||
|
2. initNavigation()
|
||||||
|
3. initBottomSheet()
|
||||||
|
4. bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() { ... }
|
||||||
|
*/
|
||||||
|
|
||||||
private void initService() {
|
private void initService() {
|
||||||
MediaManager.check(getMediaBrowserListenableFuture());
|
MediaManager.check(getMediaBrowserListenableFuture());
|
||||||
|
|
||||||
@@ -321,7 +367,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void goToHome() {
|
private void goToHome() {
|
||||||
bottomNavigationView.setVisibility(View.VISIBLE);
|
setBottomNavigationBarVisibility(true);
|
||||||
|
|
||||||
if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) {
|
if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) {
|
||||||
navController.navigate(R.id.action_landingFragment_to_homeFragment);
|
navController.navigate(R.id.action_landingFragment_to_homeFragment);
|
||||||
@@ -368,6 +414,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
Preferences.setServer(null);
|
Preferences.setServer(null);
|
||||||
Preferences.setLocalAddress(null);
|
Preferences.setLocalAddress(null);
|
||||||
Preferences.setUser(null);
|
Preferences.setUser(null);
|
||||||
|
Preferences.setClientCert(null);
|
||||||
|
|
||||||
// TODO Enter all settings to be reset
|
// TODO Enter all settings to be reset
|
||||||
Preferences.setOpenSubsonic(false);
|
Preferences.setOpenSubsonic(false);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,6 +23,8 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
|
|||||||
|
|
||||||
private List<AlbumID3> albums;
|
private List<AlbumID3> albums;
|
||||||
|
|
||||||
|
private int sizePx = 400;
|
||||||
|
|
||||||
public AlbumAdapter(ClickCallback click) {
|
public AlbumAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.albums = Collections.emptyList();
|
this.albums = Collections.emptyList();
|
||||||
@@ -31,11 +34,20 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemLibraryAlbumBinding view = ItemLibraryAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemLibraryAlbumBinding view = ItemLibraryAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize(parent.getContext());
|
||||||
|
sizePx = TileSizeManager.getInstance().getTileSizePx(parent.getContext());
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.albumCoverImageView.getLayoutParams();
|
||||||
|
lp.width = sizePx;
|
||||||
|
lp.height = sizePx;
|
||||||
|
holder.item.albumCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
AlbumID3 album = albums.get(position);
|
AlbumID3 album = albums.get(position);
|
||||||
|
|
||||||
holder.item.albumNameLabel.setText(album.getName());
|
holder.item.albumNameLabel.setText(album.getName());
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,6 +25,7 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
|
|||||||
private final boolean mix;
|
private final boolean mix;
|
||||||
private final boolean bestOf;
|
private final boolean bestOf;
|
||||||
|
|
||||||
|
private int sizePx = 400;
|
||||||
private List<ArtistID3> artists;
|
private List<ArtistID3> artists;
|
||||||
|
|
||||||
public ArtistAdapter(ClickCallback click, Boolean mix, Boolean bestOf) {
|
public ArtistAdapter(ClickCallback click, Boolean mix, Boolean bestOf) {
|
||||||
@@ -37,11 +39,20 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemLibraryArtistBinding view = ItemLibraryArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemLibraryArtistBinding view = ItemLibraryArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize(parent.getContext());
|
||||||
|
sizePx = TileSizeManager.getInstance().getTileSizePx(parent.getContext());
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.artistCoverImageView.getLayoutParams();
|
||||||
|
lp.width = sizePx;
|
||||||
|
lp.height = sizePx;
|
||||||
|
holder.item.artistCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
ArtistID3 artist = artists.get(position);
|
ArtistID3 artist = artists.get(position);
|
||||||
|
|
||||||
holder.item.artistNameLabel.setText(artist.getName());
|
holder.item.artistNameLabel.setText(artist.getName());
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
|
|||||||
public void sort(String order) {
|
public void sort(String order) {
|
||||||
switch (order) {
|
switch (order) {
|
||||||
case Constants.ARTIST_ORDER_BY_NAME:
|
case Constants.ARTIST_ORDER_BY_NAME:
|
||||||
artists.sort(Comparator.comparing(ArtistID3::getName));
|
artists.sort(Comparator.comparing(ArtistID3::getName,String.CASE_INSENSITIVE_ORDER));
|
||||||
break;
|
break;
|
||||||
case Constants.ARTIST_ORDER_BY_RANDOM:
|
case Constants.ARTIST_ORDER_BY_RANDOM:
|
||||||
Collections.shuffle(artists);
|
Collections.shuffle(artists);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.SimilarArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.SimilarArtistID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,6 +23,8 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
|
|||||||
|
|
||||||
private List<SimilarArtistID3> artists;
|
private List<SimilarArtistID3> artists;
|
||||||
|
|
||||||
|
private int sizePx = 400;
|
||||||
|
|
||||||
public ArtistSimilarAdapter(ClickCallback click) {
|
public ArtistSimilarAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.artists = Collections.emptyList();
|
this.artists = Collections.emptyList();
|
||||||
@@ -31,11 +34,20 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemLibrarySimilarArtistBinding view = ItemLibrarySimilarArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemLibrarySimilarArtistBinding view = ItemLibrarySimilarArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize(parent.getContext());
|
||||||
|
sizePx = TileSizeManager.getInstance().getTileSizePx(parent.getContext());
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.similarArtistCoverImageView.getLayoutParams();
|
||||||
|
lp.width = sizePx;
|
||||||
|
lp.height = sizePx;
|
||||||
|
holder.item.similarArtistCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
SimilarArtistID3 artist = artists.get(position);
|
SimilarArtistID3 artist = artists.get(position);
|
||||||
|
|
||||||
holder.item.artistNameLabel.setText(artist.getName());
|
holder.item.artistNameLabel.setText(artist.getName());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,6 +25,9 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
|
|||||||
|
|
||||||
private List<Child> songs;
|
private List<Child> songs;
|
||||||
|
|
||||||
|
private int widthPx = 800;
|
||||||
|
private int heightPx = 400;
|
||||||
|
|
||||||
public DiscoverSongAdapter(ClickCallback click) {
|
public DiscoverSongAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.songs = Collections.emptyList();
|
this.songs = Collections.emptyList();
|
||||||
@@ -32,11 +37,21 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemHomeDiscoverSongBinding view = ItemHomeDiscoverSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemHomeDiscoverSongBinding view = ItemHomeDiscoverSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateDiscoverSize(parent.getContext());
|
||||||
|
widthPx = TileSizeManager.getInstance().getDiscoverWidthPx(parent.getContext());;
|
||||||
|
heightPx = TileSizeManager.getInstance().getDiscoverHeightPx(parent.getContext());;
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.discoverSongCoverImageView.getLayoutParams();
|
||||||
|
lp.width = widthPx;
|
||||||
|
lp.height = heightPx;
|
||||||
|
holder.item.discoverSongCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
Child song = songs.get(position);
|
Child song = songs.get(position);
|
||||||
|
|
||||||
holder.item.titleDiscoverSongLabel.setText(song.getTitle());
|
holder.item.titleDiscoverSongLabel.setText(song.getTitle());
|
||||||
|
|||||||
@@ -42,8 +42,13 @@ public class InternetRadioStationAdapter extends RecyclerView.Adapter<InternetRa
|
|||||||
holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName());
|
holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName());
|
||||||
holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl());
|
holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl());
|
||||||
|
|
||||||
|
String imageId = internetRadioStation.getHomePageUrl();
|
||||||
|
if (imageId == null || imageId.isEmpty()) {
|
||||||
|
imageId = internetRadioStation.getStreamUrl();
|
||||||
|
}
|
||||||
|
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(holder.itemView.getContext(), internetRadioStation.getStreamUrl(), CustomGlideRequest.ResourceType.Radio)
|
.from(holder.itemView.getContext(), imageId, CustomGlideRequest.ResourceType.Radio)
|
||||||
.build()
|
.build()
|
||||||
.into(holder.item.internetRadioStationCoverImageView);
|
.into(holder.item.internetRadioStationCoverImageView);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
|
|||||||
|
|
||||||
FilterResults results = new FilterResults();
|
FilterResults results = new FilterResults();
|
||||||
results.values = filteredList;
|
results.values = filteredList;
|
||||||
|
results.count = filteredList.size();
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,9 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
|
|||||||
@Override
|
@Override
|
||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
playlists.clear();
|
playlists.clear();
|
||||||
if (results.count > 0) playlists.addAll((List) results.values);
|
if (results.values != null) {
|
||||||
|
playlists.addAll((List<Playlist>) results.values);
|
||||||
|
}
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
private boolean onLongClick() {
|
private boolean onLongClick() {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
||||||
|
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
|
||||||
|
|
||||||
click.onMediaLongClick(bundle);
|
click.onMediaLongClick(bundle);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.controller;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
public class BottomSheetController {
|
||||||
|
|
||||||
|
BottomSheetHelper helper;
|
||||||
|
|
||||||
|
public BottomSheetController(@NonNull BottomSheetHelper bottomSheetPlayerHelper) {
|
||||||
|
this.helper = bottomSheetPlayerHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expand() {
|
||||||
|
helper.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
helper.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStateInPeek(boolean isVisible) {
|
||||||
|
helper.setStateInPeek(isVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisibility(boolean visibility) {
|
||||||
|
helper.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallback(BottomSheetBehavior.BottomSheetCallback callback) {
|
||||||
|
helper.addCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceFragment(int playerBottomSheet) {
|
||||||
|
helper.replaceFragment(playerBottomSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAfterStateChanged(MainViewModel mainViewModel) {
|
||||||
|
helper.checkAfterStateChanged(mainViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collapseDelayed() {
|
||||||
|
helper.collapseDelayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDraggable(Boolean isDraggable) {
|
||||||
|
helper.setDraggable(isDraggable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
return helper.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void animate(float slideOffset) {
|
||||||
|
helper.animate(slideOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPeekHeight(int peekHeight, float displayDensity) {
|
||||||
|
helper.setPeekHeight(peekHeight, displayDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.controller;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
public class BottomSheetHelper {
|
||||||
|
|
||||||
|
BottomSheetBehavior<View> bottomSheetBehavior;
|
||||||
|
View bottomSheetView;
|
||||||
|
FragmentManager fragmentManager; // Of the entire activity
|
||||||
|
PlayerBottomSheetFragment playerBottomSheetFragment;
|
||||||
|
|
||||||
|
public void setState(int state) {
|
||||||
|
bottomSheetBehavior.setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BottomSheetHelper(@NonNull BottomSheetBehavior<View> bottomSheetBehavior,
|
||||||
|
@NonNull View bottomSheetView,
|
||||||
|
@NonNull FragmentManager fragmentManager) {
|
||||||
|
this.bottomSheetBehavior = bottomSheetBehavior;
|
||||||
|
this.bottomSheetView = bottomSheetView;
|
||||||
|
this.fragmentManager = fragmentManager;
|
||||||
|
this.playerBottomSheetFragment = new PlayerBottomSheetFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallback(BottomSheetBehavior.BottomSheetCallback callback) {
|
||||||
|
bottomSheetBehavior.addBottomSheetCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStateInPeek(boolean isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
} else {
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisibility(boolean visibility) {
|
||||||
|
if (visibility) {
|
||||||
|
bottomSheetView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
bottomSheetView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceFragment(int playerBottomSheet) {
|
||||||
|
fragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(
|
||||||
|
playerBottomSheet,
|
||||||
|
playerBottomSheetFragment,
|
||||||
|
"PlayerBottomSheet")
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAfterStateChanged(MainViewModel mainViewModel) {
|
||||||
|
final Handler handler = new Handler();
|
||||||
|
final Runnable runnable = () -> setStateInPeek(mainViewModel.isQueueLoaded());
|
||||||
|
handler.postDelayed(runnable, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collapseDelayed() {
|
||||||
|
final Handler handler = new Handler();
|
||||||
|
final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
handler.postDelayed(runnable, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDraggable(Boolean isDraggable) {
|
||||||
|
bottomSheetBehavior.setDraggable((isDraggable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
return bottomSheetBehavior.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void animate(float slideOffset) {
|
||||||
|
if (playerBottomSheetFragment != null) {
|
||||||
|
float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
|
||||||
|
playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset);
|
||||||
|
playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPeekHeight(int peekHeight, float displayDensity) {
|
||||||
|
int newPeekPx = (int) (peekHeight * displayDensity);
|
||||||
|
bottomSheetBehavior.setPeekHeight(newPeekPx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ package com.cappielloantonio.tempo.ui.dialog;
|
|||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.security.KeyChain;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -32,11 +32,21 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
private String server;
|
private String server;
|
||||||
private String localAddress;
|
private String localAddress;
|
||||||
private boolean lowSecurity = false;
|
private boolean lowSecurity = false;
|
||||||
|
private String clientCertAlias;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
bind = DialogServerSignupBinding.inflate(getLayoutInflater());
|
bind = DialogServerSignupBinding.inflate(getLayoutInflater());
|
||||||
|
bind.clientCertTextView.setOnClickListener(v -> {
|
||||||
|
if (TextUtils.isEmpty(bind.clientCertTextView.getText())) {
|
||||||
|
KeyChain.choosePrivateKeyAlias(requireActivity(), alias -> {
|
||||||
|
bind.clientCertTextView.setText(alias);
|
||||||
|
}, null, null, null, null);
|
||||||
|
} else {
|
||||||
|
bind.clientCertTextView.setText(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
|
loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
|
||||||
|
|
||||||
@@ -74,6 +84,7 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress());
|
bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress());
|
||||||
bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress());
|
bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress());
|
||||||
bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity());
|
bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity());
|
||||||
|
bind.clientCertTextView.setText(loginViewModel.getServerToEdit().getClientCert());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loginViewModel.setServerToEdit(null);
|
loginViewModel.setServerToEdit(null);
|
||||||
@@ -106,6 +117,7 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null;
|
server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null;
|
||||||
localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null;
|
localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null;
|
||||||
lowSecurity = bind.lowSecurityCheckbox.isChecked();
|
lowSecurity = bind.lowSecurityCheckbox.isChecked();
|
||||||
|
clientCertAlias = bind.clientCertTextView.getText() != null && !bind.clientCertTextView.getText().toString().trim().isBlank() ? bind.clientCertTextView.getText().toString().trim() : null;
|
||||||
|
|
||||||
if (TextUtils.isEmpty(serverName)) {
|
if (TextUtils.isEmpty(serverName)) {
|
||||||
bind.serverNameTextView.setError(getString(R.string.error_required));
|
bind.serverNameTextView.setError(getString(R.string.error_required));
|
||||||
@@ -137,6 +149,6 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
|
|
||||||
private void saveServerPreference() {
|
private void saveServerPreference() {
|
||||||
String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString();
|
String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString();
|
||||||
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity));
|
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity, this.clientCertAlias));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -48,9 +49,9 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentAlbumCatalogueBinding bind;
|
private FragmentAlbumCatalogueBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
||||||
|
|
||||||
private AlbumCatalogueAdapter albumAdapter;
|
|
||||||
private int spanCount = 2;
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
private AlbumCatalogueAdapter albumAdapter;
|
||||||
private String currentSortOrder;
|
private String currentSortOrder;
|
||||||
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
|
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
|
||||||
|
|
||||||
@@ -92,9 +93,9 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
|
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
|
|
||||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
TileSizeManager.getInstance().calculateTileSize( requireContext() );
|
||||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
spanCount = TileSizeManager.getInstance().getTileSpanCount( requireContext() );
|
||||||
}
|
tileSpacing = TileSizeManager.getInstance().getTileSpacing( requireContext() );
|
||||||
|
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initAlbumCatalogueView();
|
initAlbumCatalogueView();
|
||||||
@@ -140,7 +141,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initAlbumCatalogueView() {
|
private void initAlbumCatalogueView() {
|
||||||
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
|
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
||||||
|
|||||||
@@ -261,8 +261,10 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind.albumOtherInfoButton.setOnClickListener(v -> {
|
bind.albumOtherInfoButton.setOnClickListener(v -> {
|
||||||
if (bind.albumDetailView.getVisibility() == View.GONE) {
|
if (bind.albumDetailView.getVisibility() == View.GONE) {
|
||||||
bind.albumDetailView.setVisibility(View.VISIBLE);
|
bind.albumDetailView.setVisibility(View.VISIBLE);
|
||||||
|
bind.albumNameLabel.setMaxLines(Integer.MAX_VALUE);
|
||||||
} else if (bind.albumDetailView.getVisibility() == View.VISIBLE) {
|
} else if (bind.albumDetailView.getVisibility() == View.VISIBLE) {
|
||||||
bind.albumDetailView.setVisibility(View.GONE);
|
bind.albumDetailView.setVisibility(View.GONE);
|
||||||
|
bind.albumNameLabel.setMaxLines(2);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
|
||||||
@@ -51,7 +52,9 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
private ArtistCatalogueViewModel artistCatalogueViewModel;
|
private ArtistCatalogueViewModel artistCatalogueViewModel;
|
||||||
|
|
||||||
private ArtistCatalogueAdapter artistAdapter;
|
private ArtistCatalogueAdapter artistAdapter;
|
||||||
|
|
||||||
private int spanCount = 2;
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@@ -68,9 +71,9 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
|
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
|
|
||||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
TileSizeManager.getInstance().calculateTileSize( requireContext() );
|
||||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
spanCount = TileSizeManager.getInstance().getTileSpanCount( requireContext() );
|
||||||
}
|
tileSpacing = TileSizeManager.getInstance().getTileSpacing( requireContext() );
|
||||||
|
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initArtistCatalogueView();
|
initArtistCatalogueView();
|
||||||
@@ -115,7 +118,7 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initArtistCatalogueView() {
|
private void initArtistCatalogueView() {
|
||||||
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.artistCatalogueRecyclerView.setHasFixedSize(true);
|
bind.artistCatalogueRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
artistAdapter = new ArtistCatalogueAdapter(this);
|
artistAdapter = new ArtistCatalogueAdapter(this);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
|||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
@@ -65,6 +66,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
private int spanCount = 2;
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
@@ -75,9 +77,9 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
||||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
TileSizeManager.getInstance().calculateTileSize( requireContext() );
|
||||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
spanCount = TileSizeManager.getInstance().getTileSpanCount( requireContext() );
|
||||||
}
|
tileSpacing = TileSizeManager.getInstance().getTileSpacing( requireContext() );
|
||||||
|
|
||||||
init(view);
|
init(view);
|
||||||
initAppBar();
|
initAppBar();
|
||||||
@@ -285,7 +287,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private void initAlbumsView() {
|
private void initAlbumsView() {
|
||||||
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.albumsRecyclerView.setHasFixedSize(true);
|
bind.albumsRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
|
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
|
||||||
@@ -304,7 +306,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private void initSimilarArtistsView() {
|
private void initSimilarArtistsView() {
|
||||||
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.similarArtistsRecyclerView.setHasFixedSize(true);
|
bind.similarArtistsRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);
|
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||||
activity.setBottomSheetVisibility(true);
|
activity.setBottomSheetVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,26 @@ import com.cappielloantonio.tempo.R
|
|||||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||||
import com.cappielloantonio.tempo.service.BaseMediaService
|
import com.cappielloantonio.tempo.service.BaseMediaService
|
||||||
import com.cappielloantonio.tempo.service.MediaService
|
import com.cappielloantonio.tempo.service.MediaService
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||||
import com.cappielloantonio.tempo.util.Preferences
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
|
|
||||||
class EqualizerFragment : Fragment() {
|
class EqualizerFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var activity: MainActivity
|
||||||
private var equalizerManager: EqualizerManager? = null
|
private var equalizerManager: EqualizerManager? = null
|
||||||
private lateinit var eqBandsContainer: LinearLayout
|
private lateinit var eqBandsContainer: LinearLayout
|
||||||
private lateinit var eqSwitch: Switch
|
private lateinit var eqSwitch: Switch
|
||||||
private lateinit var resetButton: Button
|
private lateinit var resetButton: Button
|
||||||
private lateinit var safeSpace: Space
|
private lateinit var safeSpace: Space
|
||||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||||
|
|
||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
activity = requireActivity() as MainActivity
|
||||||
|
}
|
||||||
|
|
||||||
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
@@ -73,6 +81,8 @@ class EqualizerFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
}
|
}
|
||||||
|
val showBottomBar = !Preferences.getHideBottomNavbarOnPortrait()
|
||||||
|
activity.setBottomNavigationBarVisibility(showBottomBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
@@ -43,7 +44,9 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
private GenreCatalogueViewModel genreCatalogueViewModel;
|
private GenreCatalogueViewModel genreCatalogueViewModel;
|
||||||
|
|
||||||
private GenreCatalogueAdapter genreCatalogueAdapter;
|
private GenreCatalogueAdapter genreCatalogueAdapter;
|
||||||
|
|
||||||
private int spanCount = 2;
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@@ -59,9 +62,9 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
|
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
|
||||||
|
|
||||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
TileSizeManager.getInstance().calculateGenreSize( requireContext() );
|
||||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
spanCount = TileSizeManager.getInstance().getGenreSpanCount( requireContext() );
|
||||||
}
|
tileSpacing = TileSizeManager.getInstance().getGenreSpacing( requireContext() );
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initAppBar();
|
initAppBar();
|
||||||
@@ -105,7 +108,7 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initGenreCatalogueView() {
|
private void initGenreCatalogueView() {
|
||||||
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 16, false));
|
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.genreCatalogueRecyclerView.setHasFixedSize(true);
|
bind.genreCatalogueRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
genreCatalogueAdapter = new GenreCatalogueAdapter(this);
|
genreCatalogueAdapter = new GenreCatalogueAdapter(this);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class HomeFragment extends Fragment {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||||
activity.setBottomSheetVisibility(true);
|
activity.setBottomSheetVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
|||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
@@ -682,11 +683,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
private void initDiscoverSongSlideView() {
|
private void initDiscoverSongSlideView() {
|
||||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||||
|
|
||||||
bind.discoverSongViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
|
bind.discoverSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||||
|
bind.discoverSongRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
discoverSongAdapter = new DiscoverSongAdapter(this);
|
discoverSongAdapter = new DiscoverSongAdapter(this);
|
||||||
bind.discoverSongViewPager.setAdapter(discoverSongAdapter);
|
bind.discoverSongRecyclerView.setAdapter(discoverSongAdapter);
|
||||||
bind.discoverSongViewPager.setOffscreenPageLimit(1);
|
|
||||||
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
||||||
MusicUtil.ratingFilter(songs);
|
MusicUtil.ratingFilter(songs);
|
||||||
|
|
||||||
@@ -699,8 +701,6 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
discoverSongAdapter.setItems(songs);
|
discoverSongAdapter.setItems(songs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setSlideViewOffset(bind.discoverSongViewPager, 20, 16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSimilarSongView() {
|
private void initSimilarSongView() {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
@@ -16,8 +18,11 @@ import androidx.media3.session.SessionToken;
|
|||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding;
|
import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding;
|
||||||
@@ -43,6 +48,7 @@ import java.util.Objects;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class LibraryFragment extends Fragment implements ClickCallback {
|
public class LibraryFragment extends Fragment implements ClickCallback {
|
||||||
private static final String TAG = "LibraryFragment";
|
private static final String TAG = "LibraryFragment";
|
||||||
|
private static final String TOAST_MSG = "Long press to refresh" ;
|
||||||
|
|
||||||
private FragmentLibraryBinding bind;
|
private FragmentLibraryBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
@@ -81,13 +87,14 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
initArtistView();
|
initArtistView();
|
||||||
initGenreView();
|
initGenreView();
|
||||||
initPlaylistView();
|
initPlaylistView();
|
||||||
|
initSwipeToRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -112,22 +119,41 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle);
|
activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Album
|
||||||
bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshAlbumSample(getViewLifecycleOwner());
|
libraryViewModel.refreshAlbumSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.albumCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Artist
|
||||||
bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshArtistSample(getViewLifecycleOwner());
|
libraryViewModel.refreshArtistSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.artistCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Genre
|
||||||
bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshGenreSample(getViewLifecycleOwner());
|
libraryViewModel.refreshGenreSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.genreCatalogueSampleTextViewRefreshable.setOnClickListener(v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Playlist
|
||||||
bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner());
|
libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.playlistCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initAppBar() {
|
private void initAppBar() {
|
||||||
@@ -304,4 +330,20 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
private void initializeMediaBrowser() {
|
private void initializeMediaBrowser() {
|
||||||
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void initSwipeToRefresh() {
|
||||||
|
bind.swipeLibraryToRefresh.setOnRefreshListener(() -> {
|
||||||
|
pullToRefresh();
|
||||||
|
bind.swipeLibraryToRefresh.setRefreshing(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pullToRefresh() {
|
||||||
|
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
|
||||||
|
libraryViewModel.refreshAlbumSample(lifecycleOwner);
|
||||||
|
libraryViewModel.refreshGenreSample(lifecycleOwner);
|
||||||
|
libraryViewModel.refreshArtistSample(lifecycleOwner);
|
||||||
|
libraryViewModel.refreshPlaylistSample(lifecycleOwner);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
|||||||
@Override
|
@Override
|
||||||
public void onServerClick(Bundle bundle) {
|
public void onServerClick(Bundle bundle) {
|
||||||
Server server = bundle.getParcelable("server_object");
|
Server server = bundle.getParcelable("server_object");
|
||||||
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity());
|
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity(), server.getClientCert());
|
||||||
|
|
||||||
SystemRepository systemRepository = new SystemRepository();
|
SystemRepository systemRepository = new SystemRepository();
|
||||||
systemRepository.checkUserCredential(new SystemCallback() {
|
systemRepository.checkUserCredential(new SystemCallback() {
|
||||||
@@ -142,13 +142,14 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
|||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity) {
|
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity, String clientCert) {
|
||||||
Preferences.setServerId(serverId);
|
Preferences.setServerId(serverId);
|
||||||
Preferences.setServer(server);
|
Preferences.setServer(server);
|
||||||
Preferences.setLocalAddress(localAddress);
|
Preferences.setLocalAddress(localAddress);
|
||||||
Preferences.setUser(user);
|
Preferences.setUser(user);
|
||||||
Preferences.setPassword(password);
|
Preferences.setPassword(password);
|
||||||
Preferences.setLowSecurity(isLowSecurity);
|
Preferences.setLowSecurity(isLowSecurity);
|
||||||
|
Preferences.setClientCert(clientCert);
|
||||||
|
|
||||||
App.getSubsonicClientInstance(true);
|
App.getSubsonicClientInstance(true);
|
||||||
}
|
}
|
||||||
@@ -161,6 +162,7 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
|||||||
Preferences.setToken(null);
|
Preferences.setToken(null);
|
||||||
Preferences.setSalt(null);
|
Preferences.setSalt(null);
|
||||||
Preferences.setLowSecurity(false);
|
Preferences.setLowSecurity(false);
|
||||||
|
Preferences.setClientCert(null);
|
||||||
|
|
||||||
App.getSubsonicClientInstance(true);
|
App.getSubsonicClientInstance(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import android.os.Bundle;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -33,6 +35,10 @@ import androidx.media3.session.SessionToken;
|
|||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavOptions;
|
import androidx.navigation.NavOptions;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.transition.ChangeBounds;
|
||||||
|
import androidx.transition.Slide;
|
||||||
|
import androidx.transition.TransitionManager;
|
||||||
|
import androidx.transition.TransitionSet;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
@@ -56,7 +62,6 @@ import com.google.android.material.elevation.SurfaceColors;
|
|||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -314,7 +319,7 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
if (!samplingRate.trim().isEmpty()) items.add(samplingRate);
|
if (!samplingRate.trim().isEmpty()) items.add(samplingRate);
|
||||||
String mediaQuality = TextUtils.join(" • ", items);
|
String mediaQuality = TextUtils.join(" • ", items);
|
||||||
|
|
||||||
playerMediaBitrate.setVisibility(View.VISIBLE);
|
playerMediaBitrate.setVisibility(Preferences.getBitrateVisible() ? View.VISIBLE : View.GONE);
|
||||||
playerMediaBitrate.setText(isLocal ? mediaQuality : mediaQuality);
|
playerMediaBitrate.setText(isLocal ? mediaQuality : mediaQuality);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +340,25 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata);
|
TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata);
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
playerMediaExtension.setOnClickListener( v -> toggleBitrateVisibility() );
|
||||||
|
playerMediaBitrate.setOnClickListener(v -> toggleBitrateVisibility() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void toggleBitrateVisibility() {
|
||||||
|
ViewGroup parent = (ViewGroup) playerMediaBitrate.getParent();
|
||||||
|
|
||||||
|
TransitionSet transition = new TransitionSet()
|
||||||
|
.addTransition(new Slide(Gravity.START))
|
||||||
|
.addTransition(new ChangeBounds())
|
||||||
|
.setDuration(500)
|
||||||
|
.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||||
|
TransitionManager.beginDelayedTransition(parent, transition);
|
||||||
|
|
||||||
|
playerMediaBitrate.setVisibility(Preferences.getBitrateVisible() ? View.GONE : View.VISIBLE);
|
||||||
|
Preferences.setBitrateVisible(!Preferences.getBitrateVisible());
|
||||||
|
}
|
||||||
|
|
||||||
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
||||||
if (assetLinkChipGroup == null) return;
|
if (assetLinkChipGroup == null) return;
|
||||||
String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC;
|
String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC;
|
||||||
|
|||||||
@@ -216,8 +216,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
||||||
Collections.shuffle(songs);
|
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> shuffledSongs = new java.util.ArrayList<>(songs);
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
java.util.Collections.shuffle(shuffledSongs);
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, shuffledSongs, 0);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,32 +228,33 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
private void initBackCover() {
|
private void initBackCover() {
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
||||||
if (bind != null && songs != null && !songs.isEmpty()) {
|
if (bind != null && songs != null && !songs.isEmpty()) {
|
||||||
Collections.shuffle(songs);
|
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> randomSongs = new java.util.ArrayList<>(songs);
|
||||||
|
java.util.Collections.shuffle(randomSongs);
|
||||||
|
|
||||||
// Pic top-left
|
// Pic top-left
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), !songs.isEmpty() ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), !randomSongs.isEmpty() ? randomSongs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
||||||
.into(bind.playlistCoverImageViewTopLeft);
|
.into(bind.playlistCoverImageViewTopLeft);
|
||||||
|
|
||||||
// Pic top-right
|
// Pic top-right
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 1 ? songs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 1 ? randomSongs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
||||||
.into(bind.playlistCoverImageViewTopRight);
|
.into(bind.playlistCoverImageViewTopRight);
|
||||||
|
|
||||||
// Pic bottom-left
|
// Pic bottom-left
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 2 ? songs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 2 ? randomSongs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
||||||
.into(bind.playlistCoverImageViewBottomLeft);
|
.into(bind.playlistCoverImageViewBottomLeft);
|
||||||
|
|
||||||
// Pic bottom-right
|
// Pic bottom-right
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 3 ? songs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 3 ? randomSongs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
||||||
.into(bind.playlistCoverImageViewBottomRight);
|
.into(bind.playlistCoverImageViewBottomRight);
|
||||||
@@ -271,6 +273,11 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
songHorizontalAdapter.setItems(songs);
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
if (songs != null) {
|
||||||
|
bind.playlistSongCountLabel.setText(getString(R.string.playlist_song_count, songs.size()));
|
||||||
|
long totalDuration = songs.stream().mapToLong(s -> s.getDuration() != null ? s.getDuration() : 0).sum();
|
||||||
|
bind.playlistDurationLabel.setText(getString(R.string.playlist_duration, MusicUtil.getReadableDurationString(totalDuration, false)));
|
||||||
|
}
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -291,6 +298,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaLongClick(Bundle bundle) {
|
public void onMediaLongClick(Bundle bundle) {
|
||||||
|
bundle.putString(Constants.PLAYLIST_ID, playlistPageViewModel.getPlaylist().getId());
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,16 +26,20 @@ import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper;
|
|||||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
|
import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class SearchFragment extends Fragment implements ClickCallback {
|
public class SearchFragment extends Fragment implements ClickCallback {
|
||||||
@@ -49,6 +53,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
private ArtistAdapter artistAdapter;
|
private ArtistAdapter artistAdapter;
|
||||||
private AlbumAdapter albumAdapter;
|
private AlbumAdapter albumAdapter;
|
||||||
private SongHorizontalAdapter songHorizontalAdapter;
|
private SongHorizontalAdapter songHorizontalAdapter;
|
||||||
|
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@@ -126,6 +131,12 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
||||||
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
|
|
||||||
|
bind.allsongsview.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
|
bind.allsongsview.setHasFixedSize(true);
|
||||||
|
|
||||||
|
playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this);
|
||||||
|
bind.allsongsview.setAdapter(playlistHorizontalAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSearchView() {
|
private void initSearchView() {
|
||||||
@@ -216,13 +227,23 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
public void search(String query) {
|
public void search(String query) {
|
||||||
searchViewModel.setQuery(query);
|
searchViewModel.setQuery(query);
|
||||||
|
bind.allSongs.setText(this.getView().getContext().getString(R.string.search_all_songs_loading));
|
||||||
|
playlistHorizontalAdapter.setItems(Collections.emptyList());
|
||||||
bind.searchBar.setText(query);
|
bind.searchBar.setText(query);
|
||||||
bind.searchView.hide();
|
bind.searchView.hide();
|
||||||
performSearch(query);
|
performSearch(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateUI(List<Playlist> allSongs) {
|
||||||
|
if (!allSongs.isEmpty()) {
|
||||||
|
playlistHorizontalAdapter.setItems(allSongs);
|
||||||
|
} else {
|
||||||
|
playlistHorizontalAdapter.setItems(Collections.emptyList());
|
||||||
|
}
|
||||||
|
bind.allSongs.setText(this.getView().getContext().getString(R.string.search_all_songs_play,String.valueOf(allSongs.getFirst().getName())));
|
||||||
|
}
|
||||||
private void performSearch(String query) {
|
private void performSearch(String query) {
|
||||||
searchViewModel.search3(query).observe(getViewLifecycleOwner(), result -> {
|
searchViewModel.search3(this, query).observe(getViewLifecycleOwner(), result -> {
|
||||||
if (bind != null) {
|
if (bind != null) {
|
||||||
if (result.getArtists() != null) {
|
if (result.getArtists() != null) {
|
||||||
bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE);
|
bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
@@ -281,6 +302,19 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistClick(Bundle bundle) {
|
||||||
|
PlaylistWithSongs playlistWithSongs = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
||||||
|
if (playlistWithSongs != null) {
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, playlistWithSongs.getEntries(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistLongClick(Bundle bundle) {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.playlistBottomSheetDialog, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAlbumClick(Bundle bundle) {
|
public void onAlbumClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle);
|
||||||
|
|||||||
@@ -0,0 +1,603 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.media.audiofx.AudioEffect;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.text.InputFilter;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.core.os.LocaleListCompat;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavOptions;
|
||||||
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.preference.EditTextPreference;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceCategory;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.SwitchPreference;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.BuildConfig;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.ScanCallback;
|
||||||
|
import com.cappielloantonio.tempo.service.EqualizerManager;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
||||||
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public class SettingsContainerFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
private static final String TAG = "SettingsFragment";
|
||||||
|
private MainActivity activity;
|
||||||
|
|
||||||
|
private SettingViewModel settingViewModel;
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||||
|
|
||||||
|
private MediaService.LocalBinder mediaServiceBinder;
|
||||||
|
private boolean isServiceBound = false;
|
||||||
|
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
equalizerResultLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
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 -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||||
|
Intent data = result.getData();
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
);
|
||||||
|
|
||||||
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
activity = (MainActivity) getActivity();
|
||||||
|
|
||||||
|
View view = super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class);
|
||||||
|
|
||||||
|
if (view != null) {
|
||||||
|
getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
activity.setBottomNavigationBarVisibility(false);
|
||||||
|
activity.setBottomSheetVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
checkSystemEqualizer();
|
||||||
|
checkCacheStorage();
|
||||||
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
|
||||||
|
setStreamingCacheSize();
|
||||||
|
setAppLanguage();
|
||||||
|
setVersion();
|
||||||
|
setNetorkPingTimeoutBase();
|
||||||
|
|
||||||
|
actionLogout();
|
||||||
|
actionScan();
|
||||||
|
actionSyncStarredAlbums();
|
||||||
|
actionSyncStarredTracks();
|
||||||
|
actionSyncStarredArtists();
|
||||||
|
actionChangeStreamingCacheStorage();
|
||||||
|
actionChangeDownloadStorage();
|
||||||
|
actionSetDownloadDirectory();
|
||||||
|
actionDeleteDownloadStorage();
|
||||||
|
actionKeepScreenOn();
|
||||||
|
actionAutoDownloadLyrics();
|
||||||
|
actionMiniPlayerHeart();
|
||||||
|
|
||||||
|
bindMediaService();
|
||||||
|
actionAppEqualizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
setPreferencesFromResource(R.xml.global_preferences, rootKey);
|
||||||
|
ListPreference themePreference = findPreference(Preferences.THEME);
|
||||||
|
if (themePreference != null) {
|
||||||
|
themePreference.setOnPreferenceChangeListener(
|
||||||
|
(preference, newValue) -> {
|
||||||
|
String themeOption = (String) newValue;
|
||||||
|
ThemeHelper.applyTheme(themeOption);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkSystemEqualizer() {
|
||||||
|
Preference equalizer = findPreference("system_equalizer");
|
||||||
|
|
||||||
|
if (equalizer == null) return;
|
||||||
|
|
||||||
|
Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
|
||||||
|
|
||||||
|
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||||
|
equalizer.setOnPreferenceClickListener(preference -> {
|
||||||
|
equalizerResultLauncher.launch(intent);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
equalizer.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkCacheStorage() {
|
||||||
|
Preference storage = findPreference("streaming_cache_storage");
|
||||||
|
|
||||||
|
if (storage == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
} else {
|
||||||
|
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkStorage() {
|
||||||
|
Preference storage = findPreference("download_storage");
|
||||||
|
|
||||||
|
if (storage == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
} else {
|
||||||
|
int pref = Preferences.getDownloadStoragePreference();
|
||||||
|
if (pref == 0) {
|
||||||
|
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||||
|
} else if (pref == 1) {
|
||||||
|
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
} else {
|
||||||
|
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDownloadDirectory() {
|
||||||
|
Preference storage = findPreference("download_storage");
|
||||||
|
Preference directory = findPreference("set_download_directory");
|
||||||
|
|
||||||
|
if (directory == null) return;
|
||||||
|
|
||||||
|
String current = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (current != null) {
|
||||||
|
if (storage != null) storage.setVisible(false);
|
||||||
|
directory.setVisible(true);
|
||||||
|
directory.setIcon(R.drawable.ic_close);
|
||||||
|
directory.setTitle(R.string.settings_clear_download_folder);
|
||||||
|
directory.setSummary(current);
|
||||||
|
} else {
|
||||||
|
if (storage != null) storage.setVisible(true);
|
||||||
|
if (Preferences.getDownloadStoragePreference() == 2) {
|
||||||
|
directory.setVisible(true);
|
||||||
|
directory.setIcon(R.drawable.ic_folder);
|
||||||
|
directory.setTitle(R.string.settings_set_download_folder);
|
||||||
|
directory.setSummary(R.string.settings_choose_download_folder);
|
||||||
|
} else {
|
||||||
|
directory.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNetorkPingTimeoutBase() {
|
||||||
|
EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base");
|
||||||
|
|
||||||
|
if (networkPingTimeoutBase != null) {
|
||||||
|
networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance());
|
||||||
|
networkPingTimeoutBase.setOnBindEditTextListener(editText -> {
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> {
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (!Character.isDigit(source.charAt(i))) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
|
networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
String input = (String) newValue;
|
||||||
|
return input != null && !input.isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStreamingCacheSize() {
|
||||||
|
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||||
|
|
||||||
|
if (streamingCachePreference != null) {
|
||||||
|
streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider<ListPreference>() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public CharSequence provideSummary(@NonNull ListPreference preference) {
|
||||||
|
CharSequence entry = preference.getEntry();
|
||||||
|
|
||||||
|
if (entry == null) return null;
|
||||||
|
|
||||||
|
long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024);
|
||||||
|
|
||||||
|
return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAppLanguage() {
|
||||||
|
ListPreference localePref = (ListPreference) findPreference("language");
|
||||||
|
|
||||||
|
Map<String, String> locales = UIUtil.getLangPreferenceDropdownEntries(requireContext());
|
||||||
|
|
||||||
|
CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]);
|
||||||
|
CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]);
|
||||||
|
|
||||||
|
localePref.setEntries(entries);
|
||||||
|
localePref.setEntryValues(entryValues);
|
||||||
|
|
||||||
|
String value = localePref.getValue();
|
||||||
|
if ("default".equals(value)) {
|
||||||
|
localePref.setSummary(requireContext().getString(R.string.settings_system_language));
|
||||||
|
} else {
|
||||||
|
localePref.setSummary(Locale.forLanguageTag(value).getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
localePref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if ("default".equals(newValue)) {
|
||||||
|
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
|
||||||
|
preference.setSummary(requireContext().getString(R.string.settings_system_language));
|
||||||
|
} else {
|
||||||
|
LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue);
|
||||||
|
AppCompatDelegate.setApplicationLocales(appLocale);
|
||||||
|
preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVersion() {
|
||||||
|
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionLogout() {
|
||||||
|
findPreference("logout").setOnPreferenceClickListener(preference -> {
|
||||||
|
activity.quit();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionScan() {
|
||||||
|
findPreference("scan_library").setOnPreferenceClickListener(preference -> {
|
||||||
|
settingViewModel.launchScan(new ScanCallback() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception exception) {
|
||||||
|
findPreference("scan_library").setSummary(exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(boolean isScanning, long count) {
|
||||||
|
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||||
|
if (isScanning) getScanStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredTracks() {
|
||||||
|
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredSyncDialog dialog = new StarredSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredAlbums() {
|
||||||
|
findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredArtists() {
|
||||||
|
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionChangeStreamingCacheStorage() {
|
||||||
|
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
||||||
|
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPositiveClick() {
|
||||||
|
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNegativeClick() {
|
||||||
|
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionChangeDownloadStorage() {
|
||||||
|
findPreference("download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
|
DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPositiveClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNegativeClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNeutralClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSetDownloadDirectory() {
|
||||||
|
Preference pref = findPreference("set_download_directory");
|
||||||
|
if (pref != null) {
|
||||||
|
pref.setOnPreferenceClickListener(preference -> {
|
||||||
|
String current = Preferences.getDownloadDirectoryUri();
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
Preferences.setDownloadDirectoryUri(null);
|
||||||
|
Preferences.setDownloadStoragePreference(0);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show();
|
||||||
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
} else {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
directoryPickerLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionDeleteDownloadStorage() {
|
||||||
|
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
|
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionMiniPlayerHeart() {
|
||||||
|
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionAutoDownloadLyrics() {
|
||||||
|
SwitchPreference preference = findPreference("auto_download_lyrics");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getScanStatus() {
|
||||||
|
settingViewModel.getScanStatus(new ScanCallback() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception exception) {
|
||||||
|
findPreference("scan_library").setSummary(exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(boolean isScanning, long count) {
|
||||||
|
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||||
|
if (isScanning) getScanStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionKeepScreenOn() {
|
||||||
|
findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
} else {
|
||||||
|
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
mediaServiceBinder = (MediaService.LocalBinder) service;
|
||||||
|
isServiceBound = true;
|
||||||
|
checkEqualizerBands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
mediaServiceBinder = null;
|
||||||
|
isServiceBound = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void bindMediaService() {
|
||||||
|
Intent intent = new Intent(requireActivity(), MediaService.class);
|
||||||
|
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
|
||||||
|
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
isServiceBound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkEqualizerBands() {
|
||||||
|
if (mediaServiceBinder != null) {
|
||||||
|
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
|
||||||
|
short numBands = eqManager.getNumberOfBands();
|
||||||
|
Preference appEqualizer = findPreference("app_equalizer");
|
||||||
|
if (appEqualizer != null) {
|
||||||
|
appEqualizer.setVisible(numBands > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionAppEqualizer() {
|
||||||
|
Preference appEqualizer = findPreference("app_equalizer");
|
||||||
|
if (appEqualizer != null) {
|
||||||
|
appEqualizer.setOnPreferenceClickListener(preference -> {
|
||||||
|
NavController navController = NavHostFragment.findNavController(this);
|
||||||
|
NavOptions navOptions = new NavOptions.Builder()
|
||||||
|
.setLaunchSingleTop(true)
|
||||||
|
.setPopUpTo(R.id.equalizerFragment, true)
|
||||||
|
.build();
|
||||||
|
activity.setBottomNavigationBarVisibility(true);
|
||||||
|
activity.setBottomSheetVisibility(true);
|
||||||
|
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (isServiceBound) {
|
||||||
|
requireActivity().unbindService(serviceConnection);
|
||||||
|
isServiceBound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,128 +1,61 @@
|
|||||||
package com.cappielloantonio.tempo.ui.fragment;
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
import android.app.Activity;
|
import static com.google.android.material.internal.ViewUtils.hideKeyboard;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.media.audiofx.AudioEffect;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
|
||||||
import android.text.InputFilter;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
|
||||||
import androidx.core.os.LocaleListCompat;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.media3.common.util.UnstableApi;
|
|
||||||
import androidx.navigation.NavController;
|
|
||||||
import androidx.navigation.NavOptions;
|
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
|
||||||
import androidx.preference.EditTextPreference;
|
|
||||||
import androidx.preference.ListPreference;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
import androidx.preference.PreferenceCategory;
|
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
|
||||||
import androidx.preference.SwitchPreference;
|
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.BuildConfig;
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding;
|
||||||
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
|
||||||
import com.cappielloantonio.tempo.interfaces.ScanCallback;
|
|
||||||
import com.cappielloantonio.tempo.service.EqualizerManager;
|
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
|
||||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
public class SettingsFragment extends Fragment {
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
|
||||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
|
||||||
private static final String TAG = "SettingsFragment";
|
|
||||||
|
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SettingViewModel settingViewModel;
|
private FragmentSettingsBinding bind;
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
|
||||||
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
|
||||||
|
|
||||||
private MediaService.LocalBinder mediaServiceBinder;
|
|
||||||
private boolean isServiceBound = false;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
equalizerResultLauncher = registerForActivityResult(
|
activity = (MainActivity) getActivity();
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
|
||||||
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 -> {
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
||||||
Intent data = result.getData();
|
|
||||||
if (data != null) {
|
|
||||||
Uri uri = data.getData();
|
|
||||||
if (uri != null) {
|
|
||||||
requireContext().getContentResolver().takePersistableUriPermission(
|
|
||||||
uri,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
);
|
|
||||||
|
|
||||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
|
||||||
ExternalAudioReader.refreshCache();
|
|
||||||
Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show();
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
activity = (MainActivity) getActivity();
|
bind = FragmentSettingsBinding.inflate(inflater,container,false);
|
||||||
|
View view = bind.getRoot();
|
||||||
|
|
||||||
View view = super.onCreateView(inflater, container, savedInstanceState);
|
initAppBar();
|
||||||
settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class);
|
|
||||||
|
|
||||||
if (view != null) {
|
|
||||||
getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
// Add the PreferenceFragment only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
SettingsContainerFragment prefFragment = new SettingsContainerFragment();
|
||||||
|
|
||||||
|
// Use the child fragment manager so the PreferenceFragment is scoped to this fragment
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.settings_container, prefFragment)
|
||||||
|
.setReorderingAllowed(true) // optional but recommended
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -130,479 +63,25 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
activity.setBottomNavigationBarVisibility(false);
|
activity.setBottomNavigationBarVisibility(false);
|
||||||
activity.setBottomSheetVisibility(false);
|
activity.setBottomSheetVisibility(false);
|
||||||
}
|
activity.setNavigationDrawerLock(true);
|
||||||
|
activity.setSystemBarsVisibility(!activity.isLandscape);
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
checkSystemEqualizer();
|
|
||||||
checkCacheStorage();
|
|
||||||
checkStorage();
|
|
||||||
checkDownloadDirectory();
|
|
||||||
|
|
||||||
setStreamingCacheSize();
|
|
||||||
setAppLanguage();
|
|
||||||
setVersion();
|
|
||||||
setNetorkPingTimeoutBase();
|
|
||||||
|
|
||||||
actionLogout();
|
|
||||||
actionScan();
|
|
||||||
actionSyncStarredAlbums();
|
|
||||||
actionSyncStarredTracks();
|
|
||||||
actionSyncStarredArtists();
|
|
||||||
actionChangeStreamingCacheStorage();
|
|
||||||
actionChangeDownloadStorage();
|
|
||||||
actionSetDownloadDirectory();
|
|
||||||
actionDeleteDownloadStorage();
|
|
||||||
actionKeepScreenOn();
|
|
||||||
actionAutoDownloadLyrics();
|
|
||||||
actionMiniPlayerHeart();
|
|
||||||
|
|
||||||
bindMediaService();
|
|
||||||
actionAppEqualizer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
activity.setBottomSheetVisibility(true);
|
activity.setBottomSheetVisibility(true);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if (activity.isLandscape) {
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
activity.setNavigationDrawerLock(false);
|
||||||
setPreferencesFromResource(R.xml.global_preferences, rootKey);
|
} else if (Preferences.getEnableDrawerOnPortrait()) {
|
||||||
ListPreference themePreference = findPreference(Preferences.THEME);
|
activity.setNavigationDrawerLock(false);
|
||||||
if (themePreference != null) {
|
|
||||||
themePreference.setOnPreferenceChangeListener(
|
|
||||||
(preference, newValue) -> {
|
|
||||||
String themeOption = (String) newValue;
|
|
||||||
ThemeHelper.applyTheme(themeOption);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSystemEqualizer() {
|
private void initAppBar() {
|
||||||
Preference equalizer = findPreference("system_equalizer");
|
bind.settingsToolbar.setNavigationOnClickListener(v -> {
|
||||||
|
activity.navController.navigateUp();
|
||||||
if (equalizer == null) return;
|
|
||||||
|
|
||||||
Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
|
|
||||||
|
|
||||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
|
||||||
equalizer.setOnPreferenceClickListener(preference -> {
|
|
||||||
equalizerResultLauncher.launch(intent);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
equalizer.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkCacheStorage() {
|
|
||||||
Preference storage = findPreference("streaming_cache_storage");
|
|
||||||
|
|
||||||
if (storage == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
} else {
|
|
||||||
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkStorage() {
|
|
||||||
Preference storage = findPreference("download_storage");
|
|
||||||
|
|
||||||
if (storage == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
} else {
|
|
||||||
int pref = Preferences.getDownloadStoragePreference();
|
|
||||||
if (pref == 0) {
|
|
||||||
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
|
||||||
} else if (pref == 1) {
|
|
||||||
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
|
||||||
} else {
|
|
||||||
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkDownloadDirectory() {
|
|
||||||
Preference storage = findPreference("download_storage");
|
|
||||||
Preference directory = findPreference("set_download_directory");
|
|
||||||
|
|
||||||
if (directory == null) return;
|
|
||||||
|
|
||||||
String current = Preferences.getDownloadDirectoryUri();
|
|
||||||
if (current != null) {
|
|
||||||
if (storage != null) storage.setVisible(false);
|
|
||||||
directory.setVisible(true);
|
|
||||||
directory.setIcon(R.drawable.ic_close);
|
|
||||||
directory.setTitle(R.string.settings_clear_download_folder);
|
|
||||||
directory.setSummary(current);
|
|
||||||
} else {
|
|
||||||
if (storage != null) storage.setVisible(true);
|
|
||||||
if (Preferences.getDownloadStoragePreference() == 2) {
|
|
||||||
directory.setVisible(true);
|
|
||||||
directory.setIcon(R.drawable.ic_folder);
|
|
||||||
directory.setTitle(R.string.settings_set_download_folder);
|
|
||||||
directory.setSummary(R.string.settings_choose_download_folder);
|
|
||||||
} else {
|
|
||||||
directory.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setNetorkPingTimeoutBase() {
|
|
||||||
EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base");
|
|
||||||
|
|
||||||
if (networkPingTimeoutBase != null) {
|
|
||||||
networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance());
|
|
||||||
networkPingTimeoutBase.setOnBindEditTextListener(editText -> {
|
|
||||||
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
|
||||||
editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> {
|
|
||||||
for (int i = start; i < end; i++) {
|
|
||||||
if (!Character.isDigit(source.charAt(i))) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
String input = (String) newValue;
|
|
||||||
return input != null && !input.isEmpty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setStreamingCacheSize() {
|
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
|
||||||
|
|
||||||
if (streamingCachePreference != null) {
|
|
||||||
streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider<ListPreference>() {
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public CharSequence provideSummary(@NonNull ListPreference preference) {
|
|
||||||
CharSequence entry = preference.getEntry();
|
|
||||||
|
|
||||||
if (entry == null) return null;
|
|
||||||
|
|
||||||
long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024);
|
|
||||||
|
|
||||||
return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setAppLanguage() {
|
|
||||||
ListPreference localePref = (ListPreference) findPreference("language");
|
|
||||||
|
|
||||||
Map<String, String> locales = UIUtil.getLangPreferenceDropdownEntries(requireContext());
|
|
||||||
|
|
||||||
CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]);
|
|
||||||
CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]);
|
|
||||||
|
|
||||||
localePref.setEntries(entries);
|
|
||||||
localePref.setEntryValues(entryValues);
|
|
||||||
|
|
||||||
String value = localePref.getValue();
|
|
||||||
if ("default".equals(value)) {
|
|
||||||
localePref.setSummary(requireContext().getString(R.string.settings_system_language));
|
|
||||||
} else {
|
|
||||||
localePref.setSummary(Locale.forLanguageTag(value).getDisplayName());
|
|
||||||
}
|
|
||||||
|
|
||||||
localePref.setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if ("default".equals(newValue)) {
|
|
||||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
|
|
||||||
preference.setSummary(requireContext().getString(R.string.settings_system_language));
|
|
||||||
} else {
|
|
||||||
LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue);
|
|
||||||
AppCompatDelegate.setApplicationLocales(appLocale);
|
|
||||||
preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setVersion() {
|
|
||||||
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionLogout() {
|
|
||||||
findPreference("logout").setOnPreferenceClickListener(preference -> {
|
|
||||||
activity.quit();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionScan() {
|
|
||||||
findPreference("scan_library").setOnPreferenceClickListener(preference -> {
|
|
||||||
settingViewModel.launchScan(new ScanCallback() {
|
|
||||||
@Override
|
|
||||||
public void onError(Exception exception) {
|
|
||||||
findPreference("scan_library").setSummary(exception.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSuccess(boolean isScanning, long count) {
|
|
||||||
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
|
||||||
if (isScanning) getScanStatus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSyncStarredTracks() {
|
|
||||||
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
StarredSyncDialog dialog = new StarredSyncDialog(() -> {
|
|
||||||
((SwitchPreference)preference).setChecked(false);
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSyncStarredAlbums() {
|
|
||||||
findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> {
|
|
||||||
((SwitchPreference)preference).setChecked(false);
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSyncStarredArtists() {
|
|
||||||
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
|
|
||||||
((SwitchPreference)preference).setChecked(false);
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionChangeStreamingCacheStorage() {
|
|
||||||
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
|
||||||
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPositiveClick() {
|
|
||||||
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNegativeClick() {
|
|
||||||
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionChangeDownloadStorage() {
|
|
||||||
findPreference("download_storage").setOnPreferenceClickListener(preference -> {
|
|
||||||
DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPositiveClick() {
|
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNegativeClick() {
|
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNeutralClick() {
|
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSetDownloadDirectory() {
|
|
||||||
Preference pref = findPreference("set_download_directory");
|
|
||||||
if (pref != null) {
|
|
||||||
pref.setOnPreferenceClickListener(preference -> {
|
|
||||||
String current = Preferences.getDownloadDirectoryUri();
|
|
||||||
|
|
||||||
if (current != null) {
|
|
||||||
Preferences.setDownloadDirectoryUri(null);
|
|
||||||
Preferences.setDownloadStoragePreference(0);
|
|
||||||
ExternalAudioReader.refreshCache();
|
|
||||||
Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show();
|
|
||||||
checkStorage();
|
|
||||||
checkDownloadDirectory();
|
|
||||||
} else {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
|
||||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
|
||||||
directoryPickerLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionDeleteDownloadStorage() {
|
|
||||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
|
||||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionMiniPlayerHeart() {
|
|
||||||
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
|
|
||||||
if (preference == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
|
|
||||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionAutoDownloadLyrics() {
|
|
||||||
SwitchPreference preference = findPreference("auto_download_lyrics");
|
|
||||||
if (preference == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
|
||||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getScanStatus() {
|
|
||||||
settingViewModel.getScanStatus(new ScanCallback() {
|
|
||||||
@Override
|
|
||||||
public void onError(Exception exception) {
|
|
||||||
findPreference("scan_library").setSummary(exception.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSuccess(boolean isScanning, long count) {
|
|
||||||
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
|
||||||
if (isScanning) getScanStatus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionKeepScreenOn() {
|
|
||||||
findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
} else {
|
|
||||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
mediaServiceBinder = (MediaService.LocalBinder) service;
|
|
||||||
isServiceBound = true;
|
|
||||||
checkEqualizerBands();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
mediaServiceBinder = null;
|
|
||||||
isServiceBound = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void bindMediaService() {
|
|
||||||
Intent intent = new Intent(requireActivity(), MediaService.class);
|
|
||||||
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
|
|
||||||
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
|
||||||
isServiceBound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkEqualizerBands() {
|
|
||||||
if (mediaServiceBinder != null) {
|
|
||||||
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
|
|
||||||
short numBands = eqManager.getNumberOfBands();
|
|
||||||
Preference appEqualizer = findPreference("app_equalizer");
|
|
||||||
if (appEqualizer != null) {
|
|
||||||
appEqualizer.setVisible(numBands > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionAppEqualizer() {
|
|
||||||
Preference appEqualizer = findPreference("app_equalizer");
|
|
||||||
if (appEqualizer != null) {
|
|
||||||
appEqualizer.setOnPreferenceClickListener(preference -> {
|
|
||||||
NavController navController = NavHostFragment.findNavController(this);
|
|
||||||
NavOptions navOptions = new NavOptions.Builder()
|
|
||||||
.setLaunchSingleTop(true)
|
|
||||||
.setPopUpTo(R.id.equalizerFragment, true)
|
|
||||||
.build();
|
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
|
||||||
activity.setBottomSheetVisibility(true);
|
|
||||||
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (isServiceBound) {
|
|
||||||
requireActivity().unbindService(serviceConnection);
|
|
||||||
isServiceBound = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs;
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
public class PlaylistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||||
|
private PlaylistWithSongs playlist;
|
||||||
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private static final String TAG = "PlaylistBottomSheetDialog";
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.bottom_sheet_playlist_dialog, container, false);
|
||||||
|
|
||||||
|
playlist = requireArguments().getParcelable(Constants.PLAYLIST_OBJECT);
|
||||||
|
|
||||||
|
init(view);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
|
||||||
|
initializeMediaBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
releaseMediaBrowser();
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(View view) {
|
||||||
|
ImageView coverPlaylist = view.findViewById(R.id.playlist_cover_image_view);
|
||||||
|
|
||||||
|
CustomGlideRequest.Builder
|
||||||
|
.from(view.getContext(), playlist.getCoverArtId(), CustomGlideRequest.ResourceType.Playlist)
|
||||||
|
.build()
|
||||||
|
.into(coverPlaylist);
|
||||||
|
|
||||||
|
TextView titlePlaylist = view.findViewById(R.id.playlist_title_text_view);
|
||||||
|
titlePlaylist.setText(playlist.getName());
|
||||||
|
|
||||||
|
titlePlaylist.setSelected(true);
|
||||||
|
|
||||||
|
TextView countPlaylist = view.findViewById(R.id.playlist_count_text_view);
|
||||||
|
countPlaylist.setText(view.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false)));
|
||||||
|
|
||||||
|
TextView playNext = view.findViewById(R.id.play_next_text_view);
|
||||||
|
playNext.setOnClickListener(v -> {
|
||||||
|
MediaManager.enqueue(mediaBrowserListenableFuture, playlist.getEntries(), true);
|
||||||
|
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||||
|
dismissBottomSheet();
|
||||||
|
});
|
||||||
|
|
||||||
|
TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view);
|
||||||
|
addToQueue.setOnClickListener(v -> {
|
||||||
|
MediaManager.enqueue(mediaBrowserListenableFuture, playlist.getEntries(), false);
|
||||||
|
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||||
|
dismissBottomSheet();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissBottomSheet() {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeMediaBrowser() {
|
||||||
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseMediaBrowser() {
|
||||||
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -230,6 +230,34 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
|
|
||||||
updateDownloadButtons();
|
updateDownloadButtons();
|
||||||
|
|
||||||
|
String playlistId = requireArguments().getString(Constants.PLAYLIST_ID);
|
||||||
|
int itemPosition = requireArguments().getInt(Constants.ITEM_POSITION, -1);
|
||||||
|
|
||||||
|
TextView removeFromPlaylist = view.findViewById(R.id.remove_from_playlist_text_view);
|
||||||
|
if (playlistId != null && itemPosition != -1) {
|
||||||
|
removeFromPlaylist.setVisibility(View.VISIBLE);
|
||||||
|
removeFromPlaylist.setOnClickListener(v -> {
|
||||||
|
songBottomSheetViewModel.removeFromPlaylist(playlistId, itemPosition, new com.cappielloantonio.tempo.repository.PlaylistRepository.AddToPlaylistCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_success, Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure() {
|
||||||
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_failure, Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAllSkipped() {
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.cappielloantonio.tempo.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.security.KeyChain
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import okhttp3.internal.platform.Platform
|
||||||
|
import java.net.Socket
|
||||||
|
import java.security.KeyManagementException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.Principal
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
import javax.net.ssl.X509KeyManager
|
||||||
|
|
||||||
|
object ClientCertManager {
|
||||||
|
|
||||||
|
private const val TAG = "ClientCertManager"
|
||||||
|
|
||||||
|
val trustManager = Platform.get().platformTrustManager()
|
||||||
|
var sslSocketFactory: SSLSocketFactory? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setupSslSocketFactory(context: Context) {
|
||||||
|
sslSocketFactory = createSslSocketFactory(context)
|
||||||
|
sslSocketFactory?.let {
|
||||||
|
// HttpsURLConnection is used both by:
|
||||||
|
// - Glide: in IPv6StringLoader
|
||||||
|
// - ExoPlayer: in DefaultHttpDataSource
|
||||||
|
HttpsURLConnection.setDefaultSSLSocketFactory(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSslSocketFactory(context: Context): SSLSocketFactory? {
|
||||||
|
return try {
|
||||||
|
val clientKeyManager = object : X509KeyManager {
|
||||||
|
override fun getClientAliases(keyType: String?, issuers: Array<Principal>?) = null
|
||||||
|
|
||||||
|
override fun chooseClientAlias(
|
||||||
|
keyType: Array<String>?,
|
||||||
|
issuers: Array<Principal>?,
|
||||||
|
socket: Socket?
|
||||||
|
): String? {
|
||||||
|
val clientCert = Preferences.getClientCert() ?: return null
|
||||||
|
val server = Preferences.getServer() ?: return null
|
||||||
|
return if (server.toUri().host == socket?.inetAddress?.hostName) {
|
||||||
|
clientCert
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getServerAliases(keyType: String?, issuers: Array<Principal>?) = null
|
||||||
|
|
||||||
|
override fun chooseServerAlias(
|
||||||
|
keyType: String?,
|
||||||
|
issuers: Array<Principal>?,
|
||||||
|
socket: Socket?
|
||||||
|
) = null
|
||||||
|
|
||||||
|
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
|
||||||
|
val clientCert = Preferences.getClientCert()
|
||||||
|
return if (alias == clientCert && clientCert != null) {
|
||||||
|
KeyChain.getCertificateChain(
|
||||||
|
context,
|
||||||
|
clientCert
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPrivateKey(alias: String?): PrivateKey? {
|
||||||
|
val clientCert = Preferences.getClientCert()
|
||||||
|
return if (alias == clientCert && clientCert != null) {
|
||||||
|
KeyChain.getPrivateKey(
|
||||||
|
context,
|
||||||
|
clientCert
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
|
||||||
|
sslContext.socketFactory
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
Log.e(TAG, "Failed setting mTLS", e)
|
||||||
|
null
|
||||||
|
} catch (e: KeyManagementException) {
|
||||||
|
Log.e(TAG, "Failed setting mTLS", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ object Constants {
|
|||||||
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
||||||
const val GENRE_OBJECT = "GENRE_OBJECT"
|
const val GENRE_OBJECT = "GENRE_OBJECT"
|
||||||
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
||||||
|
const val PLAYLIST_ID = "PLAYLIST_ID"
|
||||||
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
||||||
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
||||||
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.ContentResolver;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
@@ -25,6 +26,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public class MappingUtil {
|
public class MappingUtil {
|
||||||
@@ -207,6 +209,16 @@ public class MappingUtil {
|
|||||||
|
|
||||||
public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) {
|
public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) {
|
||||||
Uri uri = Uri.parse(internetRadioStation.getStreamUrl());
|
Uri uri = Uri.parse(internetRadioStation.getStreamUrl());
|
||||||
|
Uri artworkUri = null;
|
||||||
|
String homePageUrl = internetRadioStation.getHomePageUrl();
|
||||||
|
String coverArtId = null;
|
||||||
|
|
||||||
|
if (homePageUrl != null && !homePageUrl.isEmpty() && MusicUtil.isImageUrl(homePageUrl)) {
|
||||||
|
String encodedUrl = Base64.encodeToString(homePageUrl.getBytes(StandardCharsets.UTF_8),
|
||||||
|
Base64.URL_SAFE | Base64.NO_WRAP);
|
||||||
|
coverArtId = "ir_" + encodedUrl;
|
||||||
|
artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
|
||||||
|
}
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString("id", internetRadioStation.getId());
|
bundle.putString("id", internetRadioStation.getId());
|
||||||
@@ -214,13 +226,17 @@ public class MappingUtil {
|
|||||||
bundle.putString("stationName", internetRadioStation.getName());
|
bundle.putString("stationName", internetRadioStation.getName());
|
||||||
bundle.putString("uri", uri.toString());
|
bundle.putString("uri", uri.toString());
|
||||||
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
||||||
|
bundle.putString("coverArtId", coverArtId);
|
||||||
|
if (homePageUrl != null) {
|
||||||
|
bundle.putString("homepageUrl", homePageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return new MediaItem.Builder()
|
return new MediaItem.Builder()
|
||||||
.setMediaId(internetRadioStation.getId())
|
.setMediaId(internetRadioStation.getId())
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
new MediaMetadata.Builder()
|
new MediaMetadata.Builder()
|
||||||
.setTitle(internetRadioStation.getName())
|
.setTitle(internetRadioStation.getName())
|
||||||
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
.setArtworkUri(artworkUri)
|
||||||
.setExtras(bundle)
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
|
|||||||
@@ -377,4 +377,15 @@ public class MusicUtil {
|
|||||||
|
|
||||||
toFilter.addAll(filtered);
|
toFilter.addAll(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isImageUrl(String url) {
|
||||||
|
if (url == null || url.isEmpty())
|
||||||
|
return false;
|
||||||
|
String path = url.toLowerCase().trim().split("\\?")[0];
|
||||||
|
|
||||||
|
return path.endsWith(".jpg") || path.endsWith(".jpeg") ||
|
||||||
|
path.endsWith(".png") || path.endsWith(".webp") ||
|
||||||
|
path.endsWith(".gif") || path.endsWith(".bmp") ||
|
||||||
|
path.endsWith(".svg");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ object Preferences {
|
|||||||
private const val TOKEN = "token"
|
private const val TOKEN = "token"
|
||||||
private const val SALT = "salt"
|
private const val SALT = "salt"
|
||||||
private const val LOW_SECURITY = "low_security"
|
private const val LOW_SECURITY = "low_security"
|
||||||
|
private const val CLIENT_CERT = "client_cert"
|
||||||
private const val BATTERY_OPTIMIZATION = "battery_optimization"
|
private const val BATTERY_OPTIMIZATION = "battery_optimization"
|
||||||
private const val SERVER_ID = "server_id"
|
private const val SERVER_ID = "server_id"
|
||||||
private const val OPEN_SUBSONIC = "open_subsonic"
|
private const val OPEN_SUBSONIC = "open_subsonic"
|
||||||
@@ -24,12 +25,15 @@ object Preferences {
|
|||||||
private const val IN_USE_SERVER_ADDRESS = "in_use_server_address"
|
private const val IN_USE_SERVER_ADDRESS = "in_use_server_address"
|
||||||
private const val NEXT_SERVER_SWITCH = "next_server_switch"
|
private const val NEXT_SERVER_SWITCH = "next_server_switch"
|
||||||
private const val PLAYBACK_SPEED = "playback_speed"
|
private const val PLAYBACK_SPEED = "playback_speed"
|
||||||
|
private const val BITRATE_VISIBLE = "bitrate_visible"
|
||||||
private const val SKIP_SILENCE = "skip_silence"
|
private const val SKIP_SILENCE = "skip_silence"
|
||||||
private const val SHUFFLE_MODE = "shuffle_mode"
|
private const val SHUFFLE_MODE = "shuffle_mode"
|
||||||
private const val REPEAT_MODE = "repeat_mode"
|
private const val REPEAT_MODE = "repeat_mode"
|
||||||
private const val IMAGE_CACHE_SIZE = "image_cache_size"
|
private const val IMAGE_CACHE_SIZE = "image_cache_size"
|
||||||
private const val STREAMING_CACHE_SIZE = "streaming_cache_size"
|
private const val STREAMING_CACHE_SIZE = "streaming_cache_size"
|
||||||
private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row"
|
private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row"
|
||||||
|
private const val ENABLE_DRAWER_ON_PORTRAIT = "enable_drawer_on_portrait"
|
||||||
|
private const val HIDE_BOTTOM_NAVBAR_ON_PORTRAIT = "hide_bottom_navbar_on_portrait"
|
||||||
private const val IMAGE_SIZE = "image_size"
|
private const val IMAGE_SIZE = "image_size"
|
||||||
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
|
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
|
||||||
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
|
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
|
||||||
@@ -88,8 +92,19 @@ object Preferences {
|
|||||||
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
||||||
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
|
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
|
||||||
|
|
||||||
|
private const val TILE_SIZE = "tile_size"
|
||||||
|
private const val AA_ALBUM_VIEW = "androidauto_album_view"
|
||||||
|
private const val AA_HOME_VIEW = "androidauto_home_view"
|
||||||
|
private const val AA_PLAYLIST_VIEW = "androidauto_playlist_view"
|
||||||
|
private const val AA_PODCAST_VIEW = "androidauto_podcast_view"
|
||||||
|
private const val AA_RADIO_VIEW = "androidauto_radio_view"
|
||||||
|
private const val AA_FIRST_TAB = "androidauto_first_tab"
|
||||||
|
private const val AA_SECOND_TAB = "androidauto_second_tab"
|
||||||
|
private const val AA_THIRD_TAB = "androidauto_third_tab"
|
||||||
|
private const val AA_FOURTH_TAB = "androidauto_fourth_tab"
|
||||||
|
private const val AA_SHUFFLE_GENRE_SONGS = "androidauto_shuffle_genre_songs"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServer(): String? {
|
fun getServer(): String? {
|
||||||
return App.getInstance().preferences.getString(SERVER, null)
|
return App.getInstance().preferences.getString(SERVER, null)
|
||||||
}
|
}
|
||||||
@@ -162,6 +177,16 @@ object Preferences {
|
|||||||
App.getInstance().preferences.edit().putBoolean(LOW_SECURITY, isLowSecurity).apply()
|
App.getInstance().preferences.edit().putBoolean(LOW_SECURITY, isLowSecurity).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getClientCert(): String? {
|
||||||
|
return App.getInstance().preferences.getString(CLIENT_CERT, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setClientCert(clientCert: String?) {
|
||||||
|
App.getInstance().preferences.edit().putString(CLIENT_CERT, clientCert).apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServerId(): String? {
|
fun getServerId(): String? {
|
||||||
return App.getInstance().preferences.getString(SERVER_ID, null)
|
return App.getInstance().preferences.getString(SERVER_ID, null)
|
||||||
@@ -270,6 +295,16 @@ object Preferences {
|
|||||||
App.getInstance().preferences.edit().putFloat(PLAYBACK_SPEED, playbackSpeed).apply()
|
App.getInstance().preferences.edit().putFloat(PLAYBACK_SPEED, playbackSpeed).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getBitrateVisible(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(BITRATE_VISIBLE, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setBitrateVisible(bitrateVisible: Boolean) {
|
||||||
|
App.getInstance().preferences.edit().putBoolean(BITRATE_VISIBLE, bitrateVisible).apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isSkipSilenceMode(): Boolean {
|
fun isSkipSilenceMode(): Boolean {
|
||||||
return App.getInstance().preferences.getBoolean(SKIP_SILENCE, false)
|
return App.getInstance().preferences.getBoolean(SKIP_SILENCE, false)
|
||||||
@@ -310,6 +345,16 @@ object Preferences {
|
|||||||
return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt()
|
return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getEnableDrawerOnPortrait(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(ENABLE_DRAWER_ON_PORTRAIT, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getHideBottomNavbarOnPortrait(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(HIDE_BOTTOM_NAVBAR_ON_PORTRAIT, false)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getImageSize(): Int {
|
fun getImageSize(): Int {
|
||||||
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()
|
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()
|
||||||
@@ -724,4 +769,64 @@ object Preferences {
|
|||||||
fun setArtistDisplayBiography(displayBiographyEnabled: Boolean) {
|
fun setArtistDisplayBiography(displayBiographyEnabled: Boolean) {
|
||||||
App.getInstance().preferences.edit().putBoolean(ARTIST_DISPLAY_BIOGRAPHY, displayBiographyEnabled).apply()
|
App.getInstance().preferences.edit().putBoolean(ARTIST_DISPLAY_BIOGRAPHY, displayBiographyEnabled).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getTileSize(): Int {
|
||||||
|
val parsed = App.getInstance().preferences.getString(TILE_SIZE, "2")?.toIntOrNull()
|
||||||
|
return parsed?.takeIf { it in 2..6 } ?: 2
|
||||||
|
}
|
||||||
|
fun isAndroidAutoAlbumViewEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(AA_ALBUM_VIEW, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAndroidAutoHomeViewEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(AA_HOME_VIEW, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAndroidAutoPlaylistViewEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(AA_PLAYLIST_VIEW, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAndroidAutoPodcastViewEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(AA_PODCAST_VIEW, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAndroidAutoRadioViewEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(AA_RADIO_VIEW, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getAndroidAutoFirstTab(): Int {
|
||||||
|
return App.getInstance().preferences.getString(AA_FIRST_TAB, "0")!!.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getAndroidAutoSecondTab(): Int {
|
||||||
|
return App.getInstance().preferences.getString(AA_SECOND_TAB, "1")!!.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getAndroidAutoThirdTab(): Int {
|
||||||
|
return App.getInstance().preferences.getString(AA_THIRD_TAB, "2")!!.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getAndroidAutoFourthTab(): Int {
|
||||||
|
return App.getInstance().preferences.getString(AA_FOURTH_TAB, "3")!!.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAndroidAutoShuffleGenreSongsEnabled(): Boolean {
|
||||||
|
return App.getInstance().preferences.getBoolean(AA_SHUFFLE_GENRE_SONGS, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setAndroidAutoShuffleGenreSongsEnabled(enabled: Boolean) {
|
||||||
|
App.getInstance().preferences.edit().putBoolean(AA_SHUFFLE_GENRE_SONGS, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
|
public class TileSizeManager {
|
||||||
|
|
||||||
|
private static TileSizeManager instance;
|
||||||
|
|
||||||
|
private int tileSizePx;
|
||||||
|
private int tileSpanCount;
|
||||||
|
private int tileSpacing;
|
||||||
|
private int genreSizePx;
|
||||||
|
private int genreSpanCount;
|
||||||
|
private int genreSpacing;
|
||||||
|
private int GenreSpacing;
|
||||||
|
private int discoverWidthPx;
|
||||||
|
private int discoverHeightPx;
|
||||||
|
private boolean tileIsInitialized;
|
||||||
|
private boolean genreIsInitialized;
|
||||||
|
private boolean discoverIsInitialized;
|
||||||
|
|
||||||
|
private TileSizeManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TileSizeManager getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new TileSizeManager();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTileSizePx(Context context) {
|
||||||
|
if( !tileIsInitialized )
|
||||||
|
calculateTileSize(context);
|
||||||
|
return tileSizePx;
|
||||||
|
}
|
||||||
|
public int getTileSpanCount(Context context) {
|
||||||
|
if( !tileIsInitialized )
|
||||||
|
calculateTileSize(context);
|
||||||
|
return tileSpanCount;
|
||||||
|
}
|
||||||
|
public int getTileSpacing(Context context) {
|
||||||
|
if( !tileIsInitialized )
|
||||||
|
calculateTileSize(context);
|
||||||
|
return tileSpacing;
|
||||||
|
}
|
||||||
|
public int getGenreSizePx(Context context) {
|
||||||
|
if( !genreIsInitialized )
|
||||||
|
calculateGenreSize(context);
|
||||||
|
return genreSizePx;
|
||||||
|
}
|
||||||
|
public int getGenreSpanCount(Context context) {
|
||||||
|
if( !genreIsInitialized )
|
||||||
|
calculateGenreSize(context);
|
||||||
|
return genreSpanCount;
|
||||||
|
}
|
||||||
|
public int getGenreSpacing(Context context) {
|
||||||
|
if( !genreIsInitialized )
|
||||||
|
calculateGenreSize(context);
|
||||||
|
return genreSpacing;
|
||||||
|
}
|
||||||
|
public int getDiscoverWidthPx(Context context) {
|
||||||
|
if( !discoverIsInitialized )
|
||||||
|
calculateTileSize(context);
|
||||||
|
return discoverWidthPx;
|
||||||
|
}
|
||||||
|
public int getDiscoverHeightPx(Context context) {
|
||||||
|
if( !discoverIsInitialized )
|
||||||
|
calculateTileSize(context);
|
||||||
|
return discoverHeightPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculateTileSize(Context context) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
float screenWidth = metrics.widthPixels;
|
||||||
|
float screenHeight = metrics.heightPixels;
|
||||||
|
|
||||||
|
// retrieve the divisor in the preferences
|
||||||
|
int userTileSize = Math.max(2, Math.min(6, Preferences.getTileSize()));
|
||||||
|
float divisor = (float)userTileSize;
|
||||||
|
|
||||||
|
// little pading = 10
|
||||||
|
tileSizePx = Math.round(Math.min(screenWidth, screenHeight) / divisor) - 10;
|
||||||
|
tileSpanCount = Math.max(2, Math.round(screenWidth / (float)tileSizePx) );
|
||||||
|
|
||||||
|
switch (userTileSize) {
|
||||||
|
default:
|
||||||
|
case 2: // XL
|
||||||
|
tileSpacing = 20;
|
||||||
|
break;
|
||||||
|
case 3: // L
|
||||||
|
tileSpacing = 15;
|
||||||
|
break;
|
||||||
|
case 4: // M
|
||||||
|
tileSpacing = 10;
|
||||||
|
break;
|
||||||
|
case 5: // S
|
||||||
|
tileSpacing = 6;
|
||||||
|
break;
|
||||||
|
case 6: // SX
|
||||||
|
tileSpacing = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tileIsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculateGenreSize(Context context) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
float screenWidth = metrics.widthPixels;
|
||||||
|
float screenHeight = metrics.heightPixels;
|
||||||
|
|
||||||
|
// retrieve the divisor in the preferences
|
||||||
|
int userTileSize = Math.max(2, Math.min(3, Preferences.getTileSize()));
|
||||||
|
float divisor = (float)userTileSize;
|
||||||
|
|
||||||
|
// little pading = 10
|
||||||
|
genreSizePx = Math.round(Math.min(screenWidth, screenHeight) / divisor) - 10;
|
||||||
|
genreSpanCount = Math.max(2, Math.round(screenWidth / (float)genreSizePx) );
|
||||||
|
|
||||||
|
switch (userTileSize) {
|
||||||
|
default:
|
||||||
|
case 2: // XL
|
||||||
|
genreSpacing = 20;
|
||||||
|
break;
|
||||||
|
case 3: // L
|
||||||
|
genreSpacing = 15;
|
||||||
|
break;
|
||||||
|
case 4: // M
|
||||||
|
genreSpacing = 10;
|
||||||
|
break;
|
||||||
|
case 5: // S
|
||||||
|
genreSpacing = 6;
|
||||||
|
break;
|
||||||
|
case 6: // XS
|
||||||
|
genreSpacing = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
genreIsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculateDiscoverSize(Context context) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
float screenWidth = metrics.widthPixels;
|
||||||
|
float screenHeight = metrics.heightPixels;
|
||||||
|
float discoverDivisor;
|
||||||
|
|
||||||
|
// retrieve the divisor in the preferences
|
||||||
|
int userTileSize = Math.max(2, Math.min(6, Preferences.getTileSize()));
|
||||||
|
|
||||||
|
switch (userTileSize) {
|
||||||
|
default:
|
||||||
|
case 2: // XL
|
||||||
|
discoverDivisor = 1.0f;
|
||||||
|
break;
|
||||||
|
case 3: // L
|
||||||
|
discoverDivisor = 1.25f;
|
||||||
|
break;
|
||||||
|
case 4: // M
|
||||||
|
discoverDivisor = 1.5f;
|
||||||
|
break;
|
||||||
|
case 5: // S
|
||||||
|
discoverDivisor = 1.75f;
|
||||||
|
break;
|
||||||
|
case 6: // XS
|
||||||
|
discoverDivisor = 2.0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
discoverWidthPx = Math.round(Math.min(screenWidth, screenHeight) / discoverDivisor) - 50;
|
||||||
|
discoverHeightPx = Math.round((float)discoverWidthPx * 0.6f);
|
||||||
|
discoverIsInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,14 +20,36 @@ public class PlaylistPageViewModel extends AndroidViewModel {
|
|||||||
private Playlist playlist;
|
private Playlist playlist;
|
||||||
private boolean isOffline;
|
private boolean isOffline;
|
||||||
|
|
||||||
|
private final MutableLiveData<List<Child>> songLiveList = new MutableLiveData<>();
|
||||||
|
|
||||||
public PlaylistPageViewModel(@NonNull Application application) {
|
public PlaylistPageViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
||||||
playlistRepository = new PlaylistRepository();
|
playlistRepository = new PlaylistRepository();
|
||||||
|
playlistRepository.getPlaylistUpdateTrigger().observeForever(needsRefresh -> {
|
||||||
|
if (needsRefresh != null && needsRefresh && playlist != null) {
|
||||||
|
refreshSongs();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Child>> getPlaylistSongLiveList() {
|
public LiveData<List<Child>> getPlaylistSongLiveList() {
|
||||||
return playlistRepository.getPlaylistSongs(playlist.getId());
|
if (songLiveList.getValue() == null && playlist != null) {
|
||||||
|
refreshSongs();
|
||||||
|
}
|
||||||
|
return songLiveList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshSongs() {
|
||||||
|
if (playlist == null) return;
|
||||||
|
LiveData<List<Child>> remoteData = playlistRepository.getPlaylistSongs(playlist.getId());
|
||||||
|
remoteData.observeForever(new androidx.lifecycle.Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> songs) {
|
||||||
|
songLiveList.postValue(songs);
|
||||||
|
remoteData.removeObserver(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Playlist getPlaylist() {
|
public Playlist getPlaylist() {
|
||||||
@@ -35,7 +57,10 @@ public class PlaylistPageViewModel extends AndroidViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setPlaylist(Playlist playlist) {
|
public void setPlaylist(Playlist playlist) {
|
||||||
this.playlist = playlist;
|
if (this.playlist == null || !this.playlist.getId().equals(playlist.getId())) {
|
||||||
|
this.playlist = playlist;
|
||||||
|
this.songLiveList.setValue(null); // Clear old data immediately
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Boolean> isPinned(LifecycleOwner owner) {
|
public LiveData<Boolean> isPinned(LifecycleOwner owner) {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import android.app.Application;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||||
import com.cappielloantonio.tempo.repository.SearchingRepository;
|
import com.cappielloantonio.tempo.repository.SearchingRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
||||||
|
import com.cappielloantonio.tempo.ui.fragment.SearchFragment;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -43,8 +45,9 @@ public class SearchViewModel extends AndroidViewModel {
|
|||||||
return searchingRepository.search2(title);
|
return searchingRepository.search2(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<SearchResult3> search3(String title) {
|
@UnstableApi
|
||||||
return searchingRepository.search3(title);
|
public LiveData<SearchResult3> search3(SearchFragment sf, String title) {
|
||||||
|
return searchingRepository.search3(sf, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void insertNewSearch(String search) {
|
public void insertNewSearch(String search) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.cappielloantonio.tempo.model.Download;
|
|||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SharingRepository;
|
import com.cappielloantonio.tempo.repository.SharingRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
@@ -39,6 +40,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
|||||||
private final ArtistRepository artistRepository;
|
private final ArtistRepository artistRepository;
|
||||||
private final FavoriteRepository favoriteRepository;
|
private final FavoriteRepository favoriteRepository;
|
||||||
private final SharingRepository sharingRepository;
|
private final SharingRepository sharingRepository;
|
||||||
|
private final PlaylistRepository playlistRepository;
|
||||||
|
|
||||||
private Child song;
|
private Child song;
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
|||||||
artistRepository = new ArtistRepository();
|
artistRepository = new ArtistRepository();
|
||||||
favoriteRepository = new FavoriteRepository();
|
favoriteRepository = new FavoriteRepository();
|
||||||
sharingRepository = new SharingRepository();
|
sharingRepository = new SharingRepository();
|
||||||
|
playlistRepository = new PlaylistRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Child getSong() {
|
public Child getSong() {
|
||||||
@@ -62,6 +65,10 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
|||||||
this.song = song;
|
this.song = song;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeFromPlaylist(String playlistId, int index, PlaylistRepository.AddToPlaylistCallback callback) {
|
||||||
|
playlistRepository.removeSongFromPlaylist(playlistId, index, callback);
|
||||||
|
}
|
||||||
|
|
||||||
public void setFavorite(Context context) {
|
public void setFavorite(Context context) {
|
||||||
if (song.getStarred() != null) {
|
if (song.getStarred() != null) {
|
||||||
if (NetworkUtil.isOffline()) {
|
if (NetworkUtil.isOffline()) {
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_aa_added_album.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M720,800L720,680L600,680L600,600L720,600L720,480L800,480L800,600L920,600L920,680L800,680L800,800L720,800ZM120,840Q87,840 63.5,816.5Q40,793 40,760L40,200Q40,167 63.5,143.5Q87,120 120,120L680,120Q713,120 736.5,143.5Q760,167 760,200L760,400L680,400L680,320L120,320L120,760Q120,760 120,760Q120,760 120,760L640,760L640,840L120,840ZM120,240L680,240L680,200Q680,200 680,200Q680,200 680,200L120,200Q120,200 120,200Q120,200 120,200L120,240ZM120,240L120,200Q120,200 120,200Q120,200 120,200L120,200Q120,200 120,200Q120,200 120,200L120,240Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_added_title.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,468Q821,459 801,452.5Q781,446 760,443L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L442,760Q445,782 451.5,802Q458,822 467,840L200,840ZM200,720Q200,731 200,740.5Q200,750 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,443Q200,441 200,440.5Q200,440 200,440Q200,440 200,522Q200,604 200,720ZM280,680L443,680Q446,659 452.5,639Q459,619 467,600L280,600L280,680ZM280,520L524,520Q556,490 595.5,470Q635,450 680,443L680,440L280,440L280,520ZM280,360L680,360L680,280L280,280L280,360ZM720,920Q637,920 578.5,861.5Q520,803 520,720Q520,637 578.5,578.5Q637,520 720,520Q803,520 861.5,578.5Q920,637 920,720Q920,803 861.5,861.5Q803,920 720,920ZM700,840L740,840L740,740L840,740L840,700L740,700L740,600L700,600L700,700L600,700L600,740L700,740L700,840Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_aa_albums.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,660Q555,660 607.5,607.5Q660,555 660,480Q660,405 607.5,352.5Q555,300 480,300Q405,300 352.5,352.5Q300,405 300,480Q300,555 352.5,607.5Q405,660 480,660ZM451.5,508.5Q440,497 440,480Q440,463 451.5,451.5Q463,440 480,440Q497,440 508.5,451.5Q520,463 520,480Q520,497 508.5,508.5Q497,520 480,520Q463,520 451.5,508.5ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_aa_artists.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M740,400L880,400L880,480L800,480L800,700Q800,742 771,771Q742,800 700,800Q658,800 629,771Q600,742 600,700Q600,658 629,629Q658,600 700,600Q708,600 718,601.5Q728,603 740,608L740,400ZM120,800L120,688Q120,653 137.5,625Q155,597 184,582Q246,551 310,535.5Q374,520 440,520Q482,520 523.5,526.5Q565,533 607,546Q587,558 571,575Q555,592 543,612Q517,606 491.5,603Q466,600 440,600Q383,600 328,614Q273,628 220,654Q211,659 205.5,668Q200,677 200,688L200,720L521,720Q523,740 530.5,760Q538,780 551,800L120,800ZM327,433Q280,386 280,320Q280,254 327,207Q374,160 440,160Q506,160 553,207Q600,254 600,320Q600,386 553,433Q506,480 440,480Q374,480 327,433ZM496.5,376.5Q520,353 520,320Q520,287 496.5,263.5Q473,240 440,240Q407,240 383.5,263.5Q360,287 360,320Q360,353 383.5,376.5Q407,400 440,400Q473,400 496.5,376.5ZM440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320Q440,320 440,320ZM440,720L440,720L440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Q440,720 440,720Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_folders.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M120,840Q87,840 63.5,816.5Q40,793 40,760L40,240L120,240L120,760Q120,760 120,760Q120,760 120,760L800,760L800,840L120,840ZM280,680Q247,680 223.5,656.5Q200,633 200,600L200,160Q200,127 223.5,103.5Q247,80 280,80L480,80L560,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,600Q920,633 896.5,656.5Q873,680 840,680L280,680ZM280,600L840,600Q840,600 840,600Q840,600 840,600L840,240Q840,240 840,240Q840,240 840,240L527,240L447,160L280,160Q280,160 280,160Q280,160 280,160L280,600Q280,600 280,600Q280,600 280,600ZM280,600Q280,600 280,600Q280,600 280,600L280,160Q280,160 280,160Q280,160 280,160L280,160L280,240L280,240Q280,240 280,240Q280,240 280,240L280,600Q280,600 280,600Q280,600 280,600Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_for_you.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M649,463.5Q737,447 800,420L800,820Q740,847 654,863.5Q568,880 480,880Q392,880 306,863.5Q220,847 160,820L160,420Q223,447 311,463.5Q399,480 480,480Q561,480 649,463.5ZM720,760L720,530Q670,544 604.5,552Q539,560 480,560Q421,560 355.5,552Q290,544 240,530L240,760Q290,778 355,789Q420,800 480,800Q540,800 605,789Q670,778 720,760ZM593,127Q640,174 640,240Q640,306 593,353Q546,400 480,400Q414,400 367,353Q320,306 320,240Q320,174 367,127Q414,80 480,80Q546,80 593,127ZM536.5,296.5Q560,273 560,240Q560,207 536.5,183.5Q513,160 480,160Q447,160 423.5,183.5Q400,207 400,240Q400,273 423.5,296.5Q447,320 480,320Q513,320 536.5,296.5ZM480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240Q480,240 480,240ZM480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665L480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665Q480,665 480,665Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_aa_genres.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M480,660Q555,660 607.5,607.5Q660,555 660,480Q660,405 607.5,352.5Q555,300 480,300Q405,300 352.5,352.5Q300,405 300,480Q300,555 352.5,607.5Q405,660 480,660ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_home.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_mostplayed.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M400,640L640,480L400,320L400,640ZM324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,437 89,395.5Q98,354 115,315L177,377Q169,403 164.5,428.5Q160,454 160,480Q160,614 253,707Q346,800 480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q453,160 427.5,164.5Q402,169 377,177L316,116Q356,98 396,89Q436,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Q397,880 324,848.5ZM177.5,262.5Q160,245 160,220Q160,195 177.5,177.5Q195,160 220,160Q245,160 262.5,177.5Q280,195 280,220Q280,245 262.5,262.5Q245,280 220,280Q195,280 177.5,262.5ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_other.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M120,880L120,800L840,800L840,880L120,880ZM120,640L120,560L840,560L840,640L120,640ZM120,400L120,320L840,320L840,400L120,400ZM120,160L120,80L840,80L840,160L120,160Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_playlist.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_podcasts.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M440,880L440,549Q422,538 411,520.5Q400,503 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,503 549,521Q538,539 520,549L520,880L440,880ZM204,770Q147,715 113.5,640.5Q80,566 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,566 846.5,641Q813,716 756,770L700,714Q746,670 773,609.5Q800,549 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,549 187,609Q214,669 261,713L204,770ZM317,657Q282,624 261,578.5Q240,533 240,480Q240,380 310,310Q380,240 480,240Q580,240 650,310Q720,380 720,480Q720,533 699,579Q678,625 643,657L586,600Q611,577 625.5,546Q640,515 640,480Q640,414 593,367Q546,320 480,320Q414,320 367,367Q320,414 320,480Q320,516 334.5,546.5Q349,577 374,600L317,657Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_radio.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,320Q80,295 93.5,275Q107,255 130,246L636,40L662,106L332,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,800Q880,833 856.5,856.5Q833,880 800,880L160,880ZM160,800L800,800Q800,800 800,800Q800,800 800,800L800,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM391,731Q420,702 420,660Q420,618 391,589Q362,560 320,560Q278,560 249,589Q220,618 220,660Q220,702 249,731Q278,760 320,760Q362,760 391,731ZM160,440L640,440L640,360L720,360L720,440L800,440L800,320Q800,320 800,320Q800,320 800,320L160,320Q160,320 160,320Q160,320 160,320L160,440ZM160,800Q160,800 160,800Q160,800 160,800L160,520L160,520L160,800Q160,800 160,800Q160,800 160,800Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_random.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M280,800L80,600L280,400L336,457L233,560L520,560L520,640L233,640L336,743L280,800ZM680,560L624,503L727,400L440,400L440,320L727,320L624,217L680,160L880,360L680,560Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
12
app/src/main/res/drawable/ic_aa_recent.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="
|
||||||
|
M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z
|
||||||
|
|
||||||
|
"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_recent_title.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M380,660L380,300L660,480L380,660ZM480,920Q372,920 277.5,870.5Q183,821 120,732L120,840L40,840L40,600L280,600L280,680L182,680Q233,755 311.5,797.5Q390,840 480,840Q595,840 688.5,774Q782,708 820,599L898,617Q853,753 738,836.5Q623,920 480,920ZM42,440Q49,373 74,311.5Q99,250 143,198L200,255Q168,296 148,342.5Q128,389 123,440L42,440ZM256,199L199,142Q252,98 313,72.5Q374,47 440,42L440,122Q389,127 343,147Q297,167 256,199ZM705,199Q664,167 617.5,147Q571,127 520,122L520,42Q587,48 648.5,73Q710,98 762,142L705,199ZM838,440Q833,389 813,342.5Q793,296 761,255L818,198Q862,250 887,311.5Q912,373 918,440L838,440Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_star_album.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M320,720L480,598L640,720L580,522L740,408L544,408L480,200L416,408L220,408L380,522L320,720ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_aa_star_title.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M489,500L580,445L671,500L647,396L727,327L622,318L580,220L538,318L433,327L513,396L489,500ZM508,760L732,760Q725,786 708,802Q691,818 664,822L228,875Q195,880 168.5,859.5Q142,839 138,806L85,369Q81,336 101,310Q121,284 154,280L200,274L200,354L164,359Q164,359 164,359Q164,359 164,359L218,796Q218,796 218,796Q218,796 218,796L508,760ZM360,680Q327,680 303.5,656.5Q280,633 280,600L280,160Q280,127 303.5,103.5Q327,80 360,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,600Q880,633 856.5,656.5Q833,680 800,680L360,680ZM360,600L800,600Q800,600 800,600Q800,600 800,600L800,160Q800,160 800,160Q800,160 800,160L360,160Q360,160 360,160Q360,160 360,160L360,600Q360,600 360,600Q360,600 360,600ZM580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380L580,380Q580,380 580,380Q580,380 580,380ZM218,796L218,796L218,796L218,796L218,796Q218,796 218,796Q218,796 218,796Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
23
app/src/main/res/drawable/ic_albums.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M2,17.4V2.6C2,2.269 2.269,2 2.6,2H17.4C17.731,2 18,2.269 18,2.6V17.4C18,17.731 17.731,18 17.4,18H2.6C2.269,18 2,17.731 2,17.4Z"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8,22H21.4C21.731,22 22,21.731 22,21.4V8"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11,12.5C11,13.328 10.328,14 9.5,14C8.672,14 8,13.328 8,12.5C8,11.672 8.672,11 9.5,11C10.328,11 11,11.672 11,12.5ZM11,12.5V6.6C11,6.269 11.269,6 11.6,6H13"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
32
app/src/main/res/drawable/ic_artists.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M1,20V19C1,15.134 4.134,12 8,12V12C11.866,12 15,15.134 15,19V20"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M13,14V14C13,11.239 15.239,9 18,9V9C20.761,9 23,11.239 23,14V14.5"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8,12C10.209,12 12,10.209 12,8C12,5.791 10.209,4 8,4C5.791,4 4,5.791 4,8C4,10.209 5.791,12 8,12Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18,9C19.657,9 21,7.657 21,6C21,4.343 19.657,3 18,3C16.343,3 15,4.343 15,6C15,7.657 16.343,9 18,9Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||