84 Commits
3.4.8 ... 3.5.3

Author SHA1 Message Date
antonio
c977982d64 gradle: bump up code version 2023-08-25 15:38:05 +02:00
antonio
28fc3dca36 fix: set initial visibility of the grid to "Gone" and make it appear once the elements are successfully loaded 2023-08-25 15:36:55 +02:00
antonio
f1cf65a371 fix: redefined proguard rules to make Retrofit work for everyone 2023-08-25 15:28:47 +02:00
antonio
beb1d29e8f fix: delayed grid display until server connectivity is confirmed 2023-08-25 15:27:39 +02:00
antonio
1eda7cef9e fix: fix a crash when clicking the dot menu of an empty playlist 2023-08-25 12:32:24 +02:00
antonio
1af92ad949 fix: now refreshing playlist view every time library is visited 2023-08-25 12:23:26 +02:00
antonio
3fc9b35fe4 fix: a callback on playlist editor dialog closing tells me when to refresh the playlist view 2023-08-25 12:22:43 +02:00
antonio
56b48dbd4d fix: fixed race condition issue in playlist update 2023-08-25 12:20:49 +02:00
antonio
1cb371dc5a gradle: dependencies update 2023-08-25 08:50:52 +02:00
antonio
499001a269 fix: podcast playback issue resolved 2023-08-24 10:04:12 +02:00
antonio
4c9e47379d style: code cleanup 2023-08-22 14:35:21 +02:00
CappielloAntonio
a0dbb5c81f clean: readme cleanup 2023-08-22 14:34:49 +02:00
CappielloAntonio
dab53c6bbf Merge pull request #42 from dnno/fix/null-pointer-genre-scrolling
fix: check if children exist before adding
2023-08-22 14:30:35 +02:00
antonio
c0a665c00a fix: fix click on mediaitem 2023-08-22 14:22:20 +02:00
Reinhard Prechtl
41b5c57240 Check if children exist before adding 2023-08-22 13:09:30 +02:00
antonio
1d65a79c20 Fix: manually collapse bottomSheet on device state change 2023-08-21 16:40:01 +02:00
antonio
efb6e72636 Merge remote-tracking branch 'origin/main' 2023-08-21 16:15:26 +02:00
antonio
af83ffd608 fix: manually encoded the username when creating the uri for streaming and fetching coverArt 2023-08-21 16:15:08 +02:00
antonio
0201077bc4 gradle: dependencies update 2023-08-21 15:49:26 +02:00
CappielloAntonio
91d91d3024 Merge pull request #39 from dnno/main
Add new localized strings for german language
2023-08-20 15:25:54 +02:00
Reinhard Prechtl
9784a2b6c5 Add new localized strings for german language 2023-08-20 14:24:55 +02:00
antonio
87a3301912 feat: implemented hard deletion of downloaded files upon storage change or explicit user request 2023-08-19 12:33:39 +02:00
antonio
295795edc9 clean: AndroidManifest cleaning 2023-08-19 11:57:05 +02:00
antonio
6120ab66ba style: implemented string conversion method to PascalCase 2023-08-17 14:09:56 +02:00
antonio
7bea180c58 feat: implemented language picker in app settings, dynamically populating the list of available languages programmatically. 2023-08-17 14:09:17 +02:00
antonio
a29cee488e build: started implementing language picker via appcompat instead of automatically generating language configurations to maintain compatibility with Android versions below 13 2023-08-17 14:07:56 +02:00
antonio
74b4b04693 feat: enabled automatic per-app language support 2023-08-16 23:54:47 +02:00
antonio
b8b9c80bdc fix: null checking 2023-08-16 23:48:14 +02:00
antonio
a50fc74117 build: update workflow 2023-08-16 23:31:33 +02:00
antonio
c1af438a3a build: update workflow 2023-08-16 23:27:55 +02:00
antonio
80b251cddc gradle: bump up gradle version 2023-08-16 22:53:02 +02:00
antonio
7d9a48818e gradle: installing jetbrains runtime 17.0.8 2023-08-16 22:47:54 +02:00
antonio
ca3da0839b Merge remote-tracking branch 'origin/main' 2023-08-16 22:42:37 +02:00
CappielloAntonio
cf463d8fa1 Update github_release.yml 2023-08-16 22:41:44 +02:00
antonio
635fdc4c5c gradle: started working on Gradle upgrade, for testing purposes 2023-08-16 22:33:52 +02:00
antonio
3e913931c4 gradle: bump up code version 2023-08-15 19:16:32 +02:00
antonio
46420da038 chore: uploaded Android Room migration file 2023-08-15 19:08:10 +02:00
antonio
d5b7619dd1 style: aligned surface colors with recommended Material You colors 2023-08-15 19:06:31 +02:00
antonio
89b39123da fix: updated status bar and music player background colors 2023-08-14 19:24:38 +02:00
antonio
5a43137984 fix: fixed issue with dynamic color not being correctly applied 2023-08-14 19:02:26 +02:00
CappielloAntonio
761b07450f Merge pull request #36 from ThePBone/fix-filter
fix: filter array of songs in setItems (DownloadHorizontalAdapter)
2023-08-14 18:10:49 +02:00
Tim Schneeberger
a0b67a06f4 refactor: put grouped into bundle instead of songs 2023-08-14 18:08:11 +02:00
Tim Schneeberger
59d7adb66d fix: filter array of songs in setItems() 2023-08-14 17:43:33 +02:00
antonio
0c2f0d23cd build: bump up code version 2023-08-14 11:46:33 +02:00
CappielloAntonio
dc201e6c8f Merge pull request #35 from ThePBone/fix-npe
fix: handle null values for genres, artist ids, and album ids in DownloadHorizontalAdapter
2023-08-14 00:44:24 +02:00
Tim Schneeberger
9bf3399371 fix: handle null values for genres, artists, and albums in DownloadHorizontalAdapter 2023-08-14 00:30:44 +02:00
antonio
28565b691a feat: always display the download button for tracks as users can now re-download the same track multiple times, either by relying on the original download endpoint or setting their preferred codec and bitrate 2023-08-13 23:20:01 +02:00
antonio
7c6faf66c1 feat: implemented logic for track download with codec and bitrate definition 2023-08-13 23:18:17 +02:00
antonio
b160274859 feat: added options in settings to define codec and bitrate for downloaded tracks 2023-08-13 23:17:24 +02:00
antonio
3a1ced65d5 feat: added options in settings to define codec and bitrate for downloaded tracks 2023-08-13 23:17:05 +02:00
antonio
afeb72803d fix: added file URI to Download object for improved download indexing 2023-08-13 23:15:25 +02:00
antonio
600c28c2a3 fix: added file URI to Download object for improved download indexing 2023-08-13 20:27:38 +02:00
antonio
ec799fff96 feat: included download URI in the download class 2023-08-13 20:16:06 +02:00
antonio
60da08cdbc feat: implemented folder download feature for on-screen visible files (not folder and files in subfolder) 2023-08-12 23:25:00 +02:00
antonio
bdda3743db style: made the distinction between folders and files visible in folder navigation 2023-08-12 23:14:44 +02:00
antonio
59fcf11ae3 build: started preparations for F-Droid release 2023-08-12 21:36:13 +02:00
antonio
77c80b7695 style: add graphic constraints 2023-08-12 11:27:55 +02:00
CappielloAntonio
13f168f78c Merge pull request #33 from dnno/fix/update-german-localization
fix: update german localization
2023-08-12 11:20:52 +02:00
antonio
324eed7e02 Merge remote-tracking branch 'origin/main' 2023-08-12 11:19:43 +02:00
antonio
4a99c7e9b1 feat: implemented last set filter as default grouping for downloaded tracks 2023-08-12 11:17:46 +02:00
antonio
01e5917642 clean: increase distance between the title and the list 2023-08-12 10:57:36 +02:00
Reinhard Prechtl
ae00f4279e Fix translation error 2023-08-11 21:05:10 +02:00
Reinhard Prechtl
642c69eb96 Update german localization resource
- Remove non-translatable strings
- Add previously untranslated strings
2023-08-11 19:59:56 +02:00
Reinhard Prechtl
ac69361735 Mark placeholder and separator as not translatable 2023-08-11 19:59:35 +02:00
CappielloAntonio
e24efc4948 Merge pull request #32 from dnno/feat/add-german-localization
feat: add german localization
2023-08-11 16:44:01 +02:00
CappielloAntonio
f8ad18ed5a Merge pull request #30 from dnno/fix/hardcoded-strings
clean: extract hard coded strings to resources
2023-08-11 16:42:30 +02:00
antonio
17345372a2 fix: set placeholder if track number is null 2023-08-11 16:37:57 +02:00
antonio
db76494525 fix: mediaitem null-proofing 2023-08-11 16:37:14 +02:00
antonio
e87eda2757 feat: added the ability to filter and group downloaded songs 2023-08-11 16:23:53 +02:00
antonio
06e2729aca feat: created contextual menu for filtering and grouping downloaded songs 2023-08-11 16:23:01 +02:00
antonio
37ffb88d67 fix: null checking 2023-08-11 16:20:28 +02:00
antonio
3d4437151a build: dependencies update 2023-08-11 16:20:02 +02:00
Reinhard Prechtl
af1961b185 Add further localized strings and mark settings version as non translatable 2023-08-07 20:12:52 +02:00
Reinhard Prechtl
c983e33522 Fix typo in localization 2023-08-07 20:01:21 +02:00
Reinhard Prechtl
b18daec708 Add further localized strings 2023-08-07 20:01:11 +02:00
Reinhard Prechtl
6d20995e70 Add german localization to resources 2023-08-07 20:01:02 +02:00
Reinhard Prechtl
14cacd1bbc Extract hard coded strings to resources 2023-08-06 22:39:57 +02:00
antonio
9d7acdb892 feat: added the option in the settings to delete all saved offline content. 2023-08-06 00:01:02 +02:00
antonio
05325913f7 fix: semantic error 2023-08-05 11:57:22 +02:00
antonio
5733dca68a feat: implemented the ability to choose external storage (if available) as storage for offline file downloads 2023-08-04 23:46:33 +02:00
antonio
838d4496e5 fix: semantic error 2023-08-04 23:42:43 +02:00
antonio
4967363116 build: added foreground service types and permissions 2023-08-04 23:41:30 +02:00
antonio
fc61308be5 feat: added downloaded file title to notification 2023-08-03 07:22:28 +02:00
antonio
5c4a292542 feat: preventing closing of bottom sheet dialog when heart icon is clicked 2023-08-02 15:13:19 +02:00
94 changed files with 2871 additions and 284 deletions

View File

@@ -6,19 +6,15 @@ on:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Setup JDK 8
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 8
build:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Setup JDK 17
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '17'
- uses: actions/checkout@v3
- name: Cache Gradle and wrapper

2
.idea/gradle.xml generated
View File

@@ -7,7 +7,7 @@
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="11" />
<option name="gradleJvm" value="17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

2
.idea/misc.xml generated
View File

@@ -191,7 +191,7 @@
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -33,12 +33,8 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
<img src="mockup/feat/6_screenshot.png" width=200>
<img src="mockup/feat/7_screenshot.png" width=200>
<img src="mockup/feat/8_screenshot.png" width=200>
<img src="mockup/feat/9_screenshot.png" width=200>
</p>
## Disclaimer
Tempo is currently under active development and is in alpha state. This means that the app may contain stability issues, bugs, or incomplete features. While we strive to provide a smooth and reliable experience, please be aware that using Tempo in its current state may not be as stable as a fully released version. I appreciate your understanding and patience as I work towards improving the app.
## Sponsors
Tempo is an open-source project developed and maintained solely by me. I would like to express my heartfelt thanks to all the users who have shown their love and support for Tempo. Your contributions and encouragement mean a lot to me, and they help drive the development and improvement of the app.

View File

@@ -3,8 +3,8 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
android {
compileSdkVersion 34
buildToolsVersion '33.0.0'
compileSdk 34
buildToolsVersion "34.0.0"
defaultConfig {
minSdkVersion 24
@@ -28,8 +28,8 @@ android {
tempo {
dimension "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 14
versionName '3.4.7'
versionCode 18
versionName '3.5.3'
}
notquitemy {
@@ -71,28 +71,29 @@ dependencies {
// AndroidX
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.appcompat:appcompat:1.6.1"
// Android Material
implementation 'com.google.android.material:material:1.9.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'com.github.bumptech.glide:annotations:4.15.1'
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'com.github.bumptech.glide:annotations:4.16.0'
// Media3
implementation 'androidx.media3:media3-session:1.1.0'
implementation 'androidx.media3:media3-common:1.1.0'
implementation 'androidx.media3:media3-exoplayer:1.1.0'
implementation 'androidx.media3:media3-ui:1.1.0'
tempoImplementation 'androidx.media3:media3-cast:1.1.0'
implementation 'androidx.media3:media3-session:1.1.1'
implementation 'androidx.media3:media3-common:1.1.1'
implementation 'androidx.media3:media3-exoplayer:1.1.1'
implementation 'androidx.media3:media3-ui:1.1.1'
tempoImplementation 'androidx.media3:media3-cast:1.1.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
// Retrofit

View File

@@ -21,4 +21,5 @@
#-renamesourcefileattribute SourceFile
-keepattributes SourceFile, LineNumberTable
-keep public class * extends java.lang.Exception
-keep public class * extends java.lang.Exception
-keep class retrofit2.** { *; }

View File

@@ -0,0 +1,797 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "6ea111217793c58d54eabb1190dd92ec",
"entities": [
{
"tableName": "queue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "trackOrder",
"columnName": "track_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastPlay",
"columnName": "last_play",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "playingChanged",
"columnName": "playing_changed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamId",
"columnName": "stream_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"track_order"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverName",
"columnName": "server_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLowSecurity",
"columnName": "low_security",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recent_search",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))",
"fields": [
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"search"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "playlistName",
"columnName": "playlist_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "downloadState",
"columnName": "download_state",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "downloadUri",
"columnName": "download_uri",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "chronology",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "server",
"columnName": "server",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))",
"fields": [
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "toStar",
"columnName": "toStar",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"timestamp"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6ea111217793c58d54eabb1190dd92ec')"
]
}
}

View File

@@ -1,50 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:name="App"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locale_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme.SplashScreen"
android:usesCleartextTraffic="true"
android:allowBackup="false">
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
<meta-data
android:name="androidx.car.app.TintableAttributionIcon"
android:resource="@drawable/ic_graphic_eq" />
<activity
android:name=".ui.activity.MainActivity"
android:windowSoftInputMode="adjustPan|adjustResize"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustPan|adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.MediaService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="androidx.media3.session.MediaBrowserService" />
</intent-filter>
</service>
<service android:name=".service.DownloaderService"
android:exported="true">
<service
android:name=".service.DownloaderService"
android:exported="true"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application>
</manifest>

View File

@@ -10,7 +10,6 @@ import com.cappielloantonio.tempo.helper.ThemeHelper;
import com.cappielloantonio.tempo.subsonic.Subsonic;
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.color.DynamicColors;
public class App extends Application {
private static App instance;
@@ -22,7 +21,6 @@ public class App extends Application {
public void onCreate() {
super.onCreate();
DynamicColors.applyToActivitiesIfAvailable(this);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
String themePref = sharedPreferences.getString(Preferences.THEME, ThemeHelper.DEFAULT_MODE);
ThemeHelper.applyTheme(themePref);

View File

@@ -22,9 +22,9 @@ import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.model.Server;
@Database(
version = 2,
version = 3,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class},
autoMigrations = {@AutoMigration(from = 1, to = 2)}
autoMigrations = {@AutoMigration(from = 2, to = 3)}
)
@TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase {

View File

@@ -15,6 +15,9 @@ public interface DownloadDao {
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, track ASC")
LiveData<List<Download>> getAll();
@Query("SELECT * FROM download WHERE id = :id")
Download getOne(String id);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Download download);

View File

@@ -16,6 +16,7 @@ import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.signature.ObjectKey;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.Util;
import com.google.android.material.elevation.SurfaceColors;
import java.util.Map;
@@ -46,7 +47,7 @@ public class CustomGlideRequest {
uri.append("getCoverArt");
if (params.containsKey("u") && params.get("u") != null)
uri.append("?u=").append(params.get("u"));
uri.append("?u=").append(Util.encode(params.get("u")));
if (params.containsKey("p") && params.get("p") != null)
uri.append("&p=").append(params.get("p"));
if (params.containsKey("s") && params.get("s") != null)

View File

@@ -0,0 +1,13 @@
package com.cappielloantonio.tempo.interfaces;
import androidx.annotation.Keep;
@Keep
public interface DialogClickCallback {
default void onPositiveClick() {}
default void onNegativeClick() {}
default void onNeutralClick() {}
}

View File

@@ -0,0 +1,8 @@
package com.cappielloantonio.tempo.interfaces;
import androidx.annotation.Keep;
@Keep
public interface PlaylistCallback {
default void onDismiss() {}
}

View File

@@ -20,6 +20,9 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
@ColumnInfo(name = "download_state", defaultValue = "1")
var downloadState: Int = 0
@ColumnInfo(name = "download_uri", defaultValue = "")
var downloadUri: String? = null
constructor(child: Child) : this(child.id) {
parentId = child.parentId
isDir = child.isDir
@@ -52,4 +55,10 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
originalWidth = child.originalWidth
originalHeight = child.originalHeight
}
}
}
@Keep
data class DownloadStack(
var id: String,
var view: String?
)

View File

@@ -63,8 +63,10 @@ public class ArtistRepository {
if (response.isSuccessful() && response.body() != null) {
List<ArtistID3> artists = new ArrayList<>();
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
artists.addAll(index.getArtists());
if(response.body().getSubsonicResponse().getArtists() != null) {
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
artists.addAll(index.getArtists());
}
}
if (random) {

View File

@@ -4,8 +4,11 @@ import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.DownloadDao;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Favorite;
import java.util.ArrayList;
import java.util.List;
public class DownloadRepository {
@@ -15,6 +18,43 @@ public class DownloadRepository {
return downloadDao.getAll();
}
public Download getDownload(String id) {
Download download = null;
GetDownloadThreadSafe getDownloadThreadSafe = new GetDownloadThreadSafe(downloadDao, id);
Thread thread = new Thread(getDownloadThreadSafe);
thread.start();
try {
thread.join();
download = getDownloadThreadSafe.getDownload();
} catch (InterruptedException e) {
e.printStackTrace();
}
return download;
}
private static class GetDownloadThreadSafe implements Runnable {
private final DownloadDao downloadDao;
private final String id;
private Download download;
public GetDownloadThreadSafe(DownloadDao downloadDao, String id) {
this.downloadDao = downloadDao;
this.id = id;
}
@Override
public void run() {
download = downloadDao.getOne(id);
}
public Download getDownload() {
return download;
}
}
public void insert(Download download) {
InsertThreadSafe insert = new InsertThreadSafe(downloadDao, download);
Thread thread = new Thread(insert);

View File

@@ -95,12 +95,29 @@ public class PlaylistRepository {
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
Log.d("PLAYLIST", response.toString());
Log.d("createPlaylist", "onResponse: ");
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d("PLAYLIST", t.toString());
}
});
}
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.deletePlaylist(playlistId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
createPlaylist(null, name, songsId);
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}

View File

@@ -77,6 +77,8 @@ public class DownloaderManager {
}
public void download(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) {
download.setDownloadUri(mediaItem.requestMetadata.mediaUri.toString());
DownloadService.sendAddDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem), false);
insertDatabase(download);
}
@@ -89,6 +91,7 @@ public class DownloaderManager {
public void remove(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) {
DownloadService.sendRemoveDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem).id, false);
deleteDatabase(download.getId());
}
public void remove(List<MediaItem> mediaItems, List<com.cappielloantonio.tempo.model.Download> downloads) {
@@ -97,6 +100,12 @@ public class DownloaderManager {
}
}
public void removeAll() {
DownloadService.sendRemoveAllDownloads(context, DownloaderService.class, false);
deleteAllDatabase();
DownloadUtil.eraseDownloadFolder(context);
}
private void loadDownloads() {
try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
while (loadedDownloads.moveToNext()) {
@@ -108,6 +117,11 @@ public class DownloaderManager {
}
}
public static String getDownloadNotificationMessage(String id) {
com.cappielloantonio.tempo.model.Download download = getDownloadRepository().getDownload(id);
return download != null ? download.getTitle() : null;
}
private static DownloadRepository getDownloadRepository() {
return new DownloadRepository();
}
@@ -120,6 +134,10 @@ public class DownloaderManager {
getDownloadRepository().delete(id);
}
public static void deleteAllDatabase() {
getDownloadRepository().deleteAll();
}
public static void updateDatabase(String id) {
getDownloadRepository().update(id);
}

View File

@@ -7,7 +7,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.util.NotificationUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.offline.Download;
import androidx.media3.exoplayer.offline.DownloadManager;
import androidx.media3.exoplayer.offline.DownloadNotificationHelper;
@@ -52,6 +51,8 @@ public class DownloaderService extends androidx.media3.exoplayer.offline.Downloa
}
private static final class TerminalStateNotificationHelper implements DownloadManager.Listener {
private static final String TAG = "TerminalStateNotificatinHelper";
private final Context context;
private final DownloadNotificationHelper notificationHelper;
@@ -68,10 +69,10 @@ public class DownloaderService extends androidx.media3.exoplayer.offline.Downloa
Notification notification;
if (download.state == Download.STATE_COMPLETED) {
notification = notificationHelper.buildDownloadCompletedNotification(context, R.drawable.ic_check_circle, null, Util.fromUtf8Bytes(download.request.data));
notification = notificationHelper.buildDownloadCompletedNotification(context, R.drawable.ic_check_circle, null, DownloaderManager.getDownloadNotificationMessage(download.request.id));
DownloaderManager.updateDatabase(download.request.id);
} else if (download.state == Download.STATE_FAILED) {
notification = notificationHelper.buildDownloadFailedNotification(context, R.drawable.ic_error, null, Util.fromUtf8Bytes(download.request.data));
notification = notificationHelper.buildDownloadFailedNotification(context, R.drawable.ic_error, null, DownloaderManager.getDownloadNotificationMessage(download.request.id));
} else {
return;
}

View File

@@ -31,6 +31,7 @@ import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.color.DynamicColors;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.Objects;
@@ -51,10 +52,10 @@ public class MainActivity extends BaseActivity {
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this);
DynamicColors.applyToActivityIfAvailable(this);
super.onCreate(savedInstanceState);
@@ -118,6 +119,8 @@ public class MainActivity extends BaseActivity {
fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit();
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
collapseBottomSheet();
}
public void setBottomSheetInPeek(Boolean isVisible) {

View File

@@ -98,6 +98,7 @@ public class BaseActivity extends AppCompatActivity {
}
private void setNavigationBarColor() {
getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 10));
getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 8));
getWindow().setStatusBarColor(SurfaceColors.getColorForElevation(this, 0));
}
}

View File

@@ -11,25 +11,35 @@ import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.ItemHorizontalDownloadBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@UnstableApi
public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHorizontalAdapter.ViewHolder> {
private final ClickCallback click;
private String view;
private String filterKey;
private String filterValue;
private List<Child> songs;
private List<Child> grouped;
public DownloadHorizontalAdapter(ClickCallback click) {
this.click = click;
this.view = Constants.DOWNLOAD_TYPE_TRACK;
this.songs = Collections.emptyList();
this.grouped = Collections.emptyList();
}
@NonNull
@@ -41,31 +51,43 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.downloadedSongTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.downloadedSongArtistTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration(), false)));
holder.item.downloadedSongAlbumTextView.setText(MusicUtil.getReadableString(song.getAlbum()));
if (position > 0 && songs.get(position - 1) != null && !Objects.equals(songs.get(position - 1).getAlbum(), songs.get(position).getAlbum())) {
holder.item.divider.setPadding(0, (int) holder.itemView.getContext().getResources().getDimension(R.dimen.downloaded_item_padding), 0, 0);
} else {
if (position > 0) holder.item.divider.setVisibility(View.GONE);
switch (view) {
case Constants.DOWNLOAD_TYPE_TRACK:
initTrackLayout(holder, position);
break;
case Constants.DOWNLOAD_TYPE_ALBUM:
initAlbumLayout(holder, position);
break;
case Constants.DOWNLOAD_TYPE_ARTIST:
initArtistLayout(holder, position);
break;
case Constants.DOWNLOAD_TYPE_GENRE:
initGenreLayout(holder, position);
break;
case Constants.DOWNLOAD_TYPE_YEAR:
initYearLayout(holder, position);
break;
}
}
@Override
public int getItemCount() {
return songs.size();
return grouped.size();
}
public void setItems(List<Child> songs) {
public void setItems(String view, String filterKey, String filterValue, List<Child> songs) {
this.view = filterValue != null ? view : filterKey;
this.filterKey = filterKey;
this.filterValue = filterValue;
this.songs = songs;
this.grouped = groupSong(songs);
notifyDataSetChanged();
}
public Child getItem(int id) {
return songs.get(id);
return grouped.get(id);
}
@Override
@@ -78,6 +100,145 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
return position;
}
private List<Child> groupSong(List<Child> songs) {
switch (view) {
case Constants.DOWNLOAD_TYPE_TRACK:
return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getId())).filter(Util.distinctByKey(Child::getId)).collect(Collectors.toList()));
case Constants.DOWNLOAD_TYPE_ALBUM:
return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getAlbumId())).filter(Util.distinctByKey(Child::getAlbumId)).collect(Collectors.toList()));
case Constants.DOWNLOAD_TYPE_ARTIST:
return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getArtistId())).filter(Util.distinctByKey(Child::getArtistId)).collect(Collectors.toList()));
case Constants.DOWNLOAD_TYPE_GENRE:
return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getGenre())).filter(Util.distinctByKey(Child::getGenre)).collect(Collectors.toList()));
case Constants.DOWNLOAD_TYPE_YEAR:
return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getYear())).filter(Util.distinctByKey(Child::getYear)).collect(Collectors.toList()));
}
return Collections.emptyList();
}
private List<Child> filterSong(String filterKey, String filterValue, List<Child> songs) {
if (filterValue != null) {
switch (filterKey) {
case Constants.DOWNLOAD_TYPE_TRACK:
return songs.stream().filter(child -> child.getId().equals(filterValue)).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_ALBUM:
return songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_GENRE:
return songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_YEAR:
return songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_ARTIST:
return songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).collect(Collectors.toList());
}
}
return songs;
}
private String countSong(String filterKey, String filterValue, List<Child> songs) {
if (filterValue != null) {
switch (filterKey) {
case Constants.DOWNLOAD_TYPE_TRACK:
return String.valueOf(songs.stream().filter(child -> child.getId().equals(filterValue)).count());
case Constants.DOWNLOAD_TYPE_ALBUM:
return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).count());
case Constants.DOWNLOAD_TYPE_GENRE:
return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).count());
case Constants.DOWNLOAD_TYPE_YEAR:
return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).count());
case Constants.DOWNLOAD_TYPE_ARTIST:
return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).count());
}
}
return "0";
}
private void initTrackLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration(), false)));
holder.item.downloadedItemPreTextView.setText(MusicUtil.getReadableString(song.getAlbum()));
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId())
.build()
.into(holder.item.itemCoverImageView);
holder.item.itemCoverImageView.setVisibility(View.VISIBLE);
holder.item.downloadedItemMoreButton.setVisibility(View.VISIBLE);
holder.item.divider.setVisibility(View.VISIBLE);
if (position > 0 && grouped.get(position - 1) != null && !Objects.equals(grouped.get(position - 1).getAlbum(), grouped.get(position).getAlbum())) {
holder.item.divider.setPadding(0, (int) holder.itemView.getContext().getResources().getDimension(R.dimen.downloaded_item_padding), 0, 0);
} else {
if (position > 0) holder.item.divider.setVisibility(View.GONE);
}
}
private void initAlbumLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getAlbum()));
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_ALBUM, song.getAlbumId(), songs)));
holder.item.downloadedItemPreTextView.setText(MusicUtil.getReadableString(song.getArtist()));
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId())
.build()
.into(holder.item.itemCoverImageView);
holder.item.itemCoverImageView.setVisibility(View.VISIBLE);
holder.item.downloadedItemMoreButton.setVisibility(View.GONE);
holder.item.divider.setVisibility(View.VISIBLE);
if (position > 0 && grouped.get(position - 1) != null && !Objects.equals(grouped.get(position - 1).getArtist(), grouped.get(position).getArtist())) {
holder.item.divider.setPadding(0, (int) holder.itemView.getContext().getResources().getDimension(R.dimen.downloaded_item_padding), 0, 0);
} else {
if (position > 0) holder.item.divider.setVisibility(View.GONE);
}
}
private void initArtistLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getArtist()));
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_ARTIST, song.getArtistId(), songs)));
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId())
.build()
.into(holder.item.itemCoverImageView);
holder.item.itemCoverImageView.setVisibility(View.VISIBLE);
holder.item.downloadedItemMoreButton.setVisibility(View.GONE);
holder.item.divider.setVisibility(View.GONE);
}
private void initGenreLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getGenre()));
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_GENRE, song.getGenre(), songs)));
holder.item.itemCoverImageView.setVisibility(View.GONE);
holder.item.downloadedItemMoreButton.setVisibility(View.GONE);
holder.item.divider.setVisibility(View.GONE);
}
private void initYearLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(String.valueOf(song.getYear()));
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_YEAR, song.getYear().toString(), songs)));
holder.item.itemCoverImageView.setVisibility(View.GONE);
holder.item.downloadedItemMoreButton.setVisibility(View.GONE);
holder.item.divider.setVisibility(View.GONE);
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemHorizontalDownloadBinding item;
@@ -86,30 +247,62 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
this.item = item;
item.downloadedSongTitleTextView.setSelected(true);
item.downloadedSongArtistTextView.setSelected(true);
item.downloadedItemTitleTextView.setSelected(true);
item.downloadedItemSubtitleTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.downloadedSongMoreButton.setOnClickListener(v -> onLongClick());
item.downloadedItemMoreButton.setOnClickListener(v -> onLongClick());
}
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
click.onMediaClick(bundle);
switch (view) {
case Constants.DOWNLOAD_TYPE_TRACK:
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(grouped));
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
click.onMediaClick(bundle);
break;
case Constants.DOWNLOAD_TYPE_ALBUM:
bundle.putString(Constants.DOWNLOAD_TYPE_ALBUM, grouped.get(getBindingAdapterPosition()).getAlbumId());
click.onAlbumClick(bundle);
break;
case Constants.DOWNLOAD_TYPE_ARTIST:
bundle.putString(Constants.DOWNLOAD_TYPE_ARTIST, grouped.get(getBindingAdapterPosition()).getArtistId());
click.onArtistClick(bundle);
break;
case Constants.DOWNLOAD_TYPE_GENRE:
bundle.putString(Constants.DOWNLOAD_TYPE_GENRE, grouped.get(getBindingAdapterPosition()).getGenre());
click.onGenreClick(bundle);
break;
case Constants.DOWNLOAD_TYPE_YEAR:
bundle.putString(Constants.DOWNLOAD_TYPE_YEAR, grouped.get(getBindingAdapterPosition()).getYear().toString());
click.onYearClick(bundle);
break;
}
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
click.onMediaLongClick(bundle);
switch (view) {
case Constants.DOWNLOAD_TYPE_TRACK:
bundle.putParcelable(Constants.TRACK_OBJECT, grouped.get(getBindingAdapterPosition()));
click.onMediaLongClick(bundle);
return true;
case Constants.DOWNLOAD_TYPE_ALBUM:
bundle.putString(Constants.DOWNLOAD_TYPE_ALBUM, grouped.get(getBindingAdapterPosition()).getAlbumId());
click.onAlbumLongClick(bundle);
return true;
case Constants.DOWNLOAD_TYPE_ARTIST:
bundle.putString(Constants.DOWNLOAD_TYPE_ARTIST, grouped.get(getBindingAdapterPosition()).getArtistId());
click.onArtistLongClick(bundle);
return true;
}
return true;
return false;
}
}
}

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
@@ -46,6 +47,9 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
.from(holder.itemView.getContext(), child.getCoverArtId())
.build()
.into(holder.item.musicDirectoryCoverImageView);
holder.item.musicDirectoryMoreButton.setVisibility(child.isDir() ? View.VISIBLE : View.GONE);
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.GONE : View.VISIBLE);
}
@Override

View File

@@ -48,7 +48,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
holder.item.searchResultSongTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.searchResultSongSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration() != null ? song.getDuration() : 0, false)));
holder.item.trackNumberTextView.setText(String.valueOf(song.getTrack()));
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
holder.item.searchResultDowanloadIndicatorImageView.setVisibility(View.VISIBLE);
@@ -80,6 +80,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
public Child getItem(int id) {
return songs.get(id);
}
@@ -104,7 +114,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())));
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(getBindingAdapterPosition()));
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()));
click.onMediaClick(bundle);
}

View File

@@ -0,0 +1,65 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.DialogFragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
import com.cappielloantonio.tempo.util.DownloadUtil;
@OptIn(markerClass = UnstableApi.class)
public class DeleteDownloadStorageDialog extends DialogFragment {
private DialogDeleteDownloadStorageBinding bind;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogDeleteDownloadStorageBinding.inflate(getLayoutInflater());
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(bind.getRoot())
.setTitle(R.string.delete_download_storage_dialog_title)
.setPositiveButton(R.string.delete_download_storage_dialog_positive_button, null)
.setNegativeButton(R.string.delete_download_storage_dialog_negative_button, null);
return builder.create();
}
@Override
public void onResume() {
super.onResume();
setButtonAction();
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void setButtonAction() {
AlertDialog dialog = ((AlertDialog) getDialog());
if (dialog != null) {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
dialog.dismiss();
});
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
negativeButton.setOnClickListener(v -> {
dialog.dismiss();
});
}
}
}

View File

@@ -0,0 +1,89 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.DialogFragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogDownloadStorageBinding;
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.Preferences;
@OptIn(markerClass = UnstableApi.class)
public class DownloadStorageDialog extends DialogFragment {
private DialogDownloadStorageBinding bind;
private final DialogClickCallback dialogClickCallback;
public DownloadStorageDialog(DialogClickCallback dialogClickCallback) {
this.dialogClickCallback = dialogClickCallback;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogDownloadStorageBinding.inflate(getLayoutInflater());
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(bind.getRoot())
.setTitle(R.string.download_storage_dialog_title)
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null);
return builder.create();
}
@Override
public void onResume() {
super.onResume();
setButtonAction();
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void setButtonAction() {
AlertDialog dialog = ((AlertDialog) getDialog());
if (dialog != null) {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
int currentPreference = Preferences.getDownloadStoragePreference();
int newPreference = 1;
if (currentPreference != newPreference) {
Preferences.setDownloadStoragePreference(newPreference);
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
dialogClickCallback.onPositiveClick();
}
dialog.dismiss();
});
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
negativeButton.setOnClickListener(v -> {
int currentPreference = Preferences.getDownloadStoragePreference();
int newPreference = 0;
if (currentPreference != newPreference) {
Preferences.setDownloadStoragePreference(newPreference);
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
dialogClickCallback.onNegativeClick();
}
dialog.dismiss();
});
}
}
}

View File

@@ -67,7 +67,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, playlistChooserViewModel.getSongToAdd());
PlaylistEditorDialog dialog = new PlaylistEditorDialog();
PlaylistEditorDialog dialog = new PlaylistEditorDialog(null);
dialog.setArguments(bundle);
dialog.show(requireActivity().getSupportFragmentManager(), null);

View File

@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogPlaylistEditorBinding;
import com.cappielloantonio.tempo.interfaces.PlaylistCallback;
import com.cappielloantonio.tempo.ui.adapter.PlaylistDialogSongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
@@ -25,10 +26,15 @@ import java.util.Objects;
public class PlaylistEditorDialog extends DialogFragment {
private DialogPlaylistEditorBinding bind;
private PlaylistEditorViewModel playlistEditorViewModel;
private PlaylistCallback playlistCallback;
private String playlistName;
private PlaylistDialogSongHorizontalAdapter playlistDialogSongHorizontalAdapter;
public PlaylistEditorDialog(PlaylistCallback playlistCallback) {
this.playlistCallback = playlistCallback;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
@@ -85,13 +91,13 @@ public class PlaylistEditorDialog extends DialogFragment {
playlistEditorViewModel.updatePlaylist(playlistName);
}
Objects.requireNonNull(getDialog()).dismiss();
dialogDismiss();
}
});
((AlertDialog) Objects.requireNonNull(getDialog())).getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
playlistEditorViewModel.deletePlaylist();
Objects.requireNonNull(getDialog()).dismiss();
dialogDismiss();
});
}
@@ -102,7 +108,9 @@ public class PlaylistEditorDialog extends DialogFragment {
playlistDialogSongHorizontalAdapter = new PlaylistDialogSongHorizontalAdapter();
bind.playlistSongRecyclerView.setAdapter(playlistDialogSongHorizontalAdapter);
playlistEditorViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> playlistDialogSongHorizontalAdapter.setItems(songs));
playlistEditorViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
if (songs != null) playlistDialogSongHorizontalAdapter.setItems(songs);
});
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) {
int originalPosition = -1;
@@ -157,4 +165,9 @@ public class PlaylistEditorDialog extends DialogFragment {
return true;
}
private void dialogDismiss() {
Objects.requireNonNull(getDialog()).dismiss();
playlistCallback.onDismiss();
}
}

View File

@@ -3,10 +3,14 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -19,14 +23,21 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentDirectoryBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.stream.Collectors;
@UnstableApi
public class DirectoryFragment extends Fragment implements ClickCallback {
private static final String TAG = "DirectoryFragment";
@@ -39,6 +50,18 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.directory_page_menu, menu);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@@ -71,6 +94,24 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
bind = null;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_download_directory) {
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
if (isVisible() && getActivity() != null) {
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
}
});
return true;
}
return false;
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -19,15 +20,19 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentDownloadBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.model.DownloadStack;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Objects;
@UnstableApi
@@ -59,7 +64,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
super.onViewCreated(view, savedInstanceState);
initAppBar();
initDownloadedSongView();
initDownloadedView();
}
@Override
@@ -90,11 +95,12 @@ public class DownloadFragment extends Fragment implements ClickCallback {
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
}
private void initDownloadedSongView() {
bind.downloadedTracksRecyclerView.setHasFixedSize(true);
private void initDownloadedView() {
bind.downloadedRecyclerView.setHasFixedSize(true);
downloadHorizontalAdapter = new DownloadHorizontalAdapter(this);
bind.downloadedTracksRecyclerView.setAdapter(downloadHorizontalAdapter);
bind.downloadedRecyclerView.setAdapter(downloadHorizontalAdapter);
downloadViewModel.getDownloadedTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs != null) {
if (songs.isEmpty()) {
@@ -102,26 +108,92 @@ public class DownloadFragment extends Fragment implements ClickCallback {
bind.emptyDownloadLayout.setVisibility(View.VISIBLE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE);
bind.downloadDownloadedTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
bind.downloadDownloadedTracksSector.setVisibility(View.GONE);
bind.downloadDownloadedPlaceholder.placeholder.setVisibility(View.VISIBLE);
bind.downloadDownloadedSector.setVisibility(View.GONE);
bind.downloadedGroupByImageView.setVisibility(View.GONE);
}
} else {
if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.GONE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE);
bind.downloadDownloadedTracksPlaceholder.placeholder.setVisibility(View.GONE);
bind.downloadDownloadedTracksSector.setVisibility(View.VISIBLE);
bind.downloadDownloadedPlaceholder.placeholder.setVisibility(View.GONE);
bind.downloadDownloadedSector.setVisibility(View.VISIBLE);
bind.downloadedTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.downloadedGroupByImageView.setVisibility(View.VISIBLE);
downloadHorizontalAdapter.setItems(songs);
finishDownloadView(songs);
}
}
if (bind != null) bind.loadingProgressBar.setVisibility(View.GONE);
}
});
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
}
private void finishDownloadView(List<Child> songs) {
downloadViewModel.getViewStack().observe(getViewLifecycleOwner(), stack -> {
bind.downloadedRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
DownloadStack lastLevel = stack.get(stack.size() - 1);
switch (lastLevel.getId()) {
case Constants.DOWNLOAD_TYPE_TRACK:
downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs);
break;
case Constants.DOWNLOAD_TYPE_ALBUM:
downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs);
break;
case Constants.DOWNLOAD_TYPE_ARTIST:
downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_ALBUM, lastLevel.getId(), lastLevel.getView(), songs);
break;
case Constants.DOWNLOAD_TYPE_GENRE:
downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs);
break;
case Constants.DOWNLOAD_TYPE_YEAR:
downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs);
break;
}
bind.downloadedGoBackImageView.setVisibility(stack.size() > 1 ? View.VISIBLE : View.GONE);
});
}
private void showPopupMenu(View view, int menuResource) {
PopupMenu popup = new PopupMenu(requireContext(), view);
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
popup.setOnMenuItemClickListener(menuItem -> {
if (menuItem.getItemId() == R.id.menu_download_group_by_track) {
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_TRACK, null));
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_TRACK);
return true;
} else if (menuItem.getItemId() == R.id.menu_download_group_by_album) {
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ALBUM, null));
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_ALBUM);
return true;
} else if (menuItem.getItemId() == R.id.menu_download_group_by_artist) {
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ARTIST, null));
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_ARTIST);
return true;
} else if (menuItem.getItemId() == R.id.menu_download_group_by_genre) {
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_GENRE, null));
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_GENRE);
return true;
} else if (menuItem.getItemId() == R.id.menu_download_group_by_year) {
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
return true;
}
return false;
});
popup.show();
}
private void initializeMediaBrowser() {
@@ -132,6 +204,26 @@ public class DownloadFragment extends Fragment implements ClickCallback {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
@Override
public void onYearClick(Bundle bundle) {
downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, bundle.getString(Constants.DOWNLOAD_TYPE_YEAR)));
}
@Override
public void onGenreClick(Bundle bundle) {
downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_GENRE, bundle.getString(Constants.DOWNLOAD_TYPE_GENRE)));
}
@Override
public void onArtistClick(Bundle bundle) {
downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ARTIST, bundle.getString(Constants.DOWNLOAD_TYPE_ARTIST)));
}
@Override
public void onAlbumClick(Bundle bundle) {
downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ALBUM, bundle.getString(Constants.DOWNLOAD_TYPE_ALBUM)));
}
@Override
public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));

View File

@@ -5,7 +5,6 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -619,25 +618,6 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
});
}
public void reorder() {
if (bind != null) {
// bind.homeLinearLayoutContainer.removeAllViews();
// bind.homeLinearLayoutContainer.addView(bind.homeDiscoverSector);
// bind.homeLinearLayoutContainer.addView(bind.homeSimilarTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.homeRadioArtistSector);
// bind.homeLinearLayoutContainer.addView(bind.homeGridTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.starredTracksSector);
// bind.homeLinearLayoutContainer.addView(bind.starredAlbumsSector);
// bind.homeLinearLayoutContainer.addView(bind.starredArtistsSector);
// bind.homeLinearLayoutContainer.addView(bind.homeRecentlyAddedAlbumsSector);
// bind.homeLinearLayoutContainer.addView(bind.homeFlashbackSector);
// bind.homeLinearLayoutContainer.addView(bind.homeMostPlayedAlbumsSector);
// bind.homeLinearLayoutContainer.addView(bind.homeRecentlyPlayedAlbumsSector);
}
}
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}

View File

@@ -1,6 +1,7 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -18,6 +19,7 @@ import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding;
import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.interfaces.PlaylistCallback;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
@@ -71,7 +73,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
initAlbumView();
initArtistView();
initGenreView();
initPlaylistSlideView();
initPlaylistView();
}
@Override
@@ -80,6 +82,12 @@ public class LibraryFragment extends Fragment implements ClickCallback {
activity.setBottomNavigationBarVisibility(true);
}
@Override
public void onResume() {
super.onResume();
refreshPlaylistView();
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -222,7 +230,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
genreSnapHelper.attachToRecyclerView(bind.genreRecyclerView);
}
private void initPlaylistSlideView() {
private void initPlaylistView() {
bind.playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.playlistRecyclerView.setHasFixedSize(true);
@@ -244,6 +252,12 @@ public class LibraryFragment extends Fragment implements ClickCallback {
});
}
private void refreshPlaylistView() {
final Handler handler = new Handler();
final Runnable runnable = () -> libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner());
handler.postDelayed(runnable, 100);
}
@Override
public void onAlbumClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle);
@@ -276,7 +290,13 @@ public class LibraryFragment extends Fragment implements ClickCallback {
@Override
public void onPlaylistLongClick(Bundle bundle) {
PlaylistEditorDialog dialog = new PlaylistEditorDialog();
PlaylistEditorDialog dialog = new PlaylistEditorDialog(new PlaylistCallback() {
@Override
public void onDismiss() {
refreshPlaylistView();
}
});
dialog.setArguments(bundle);
dialog.show(activity.getSupportFragmentManager(), null);
}

View File

@@ -86,7 +86,7 @@ public class PlayerBottomSheetFragment extends Fragment {
}
private void customizeBottomSheetBackground() {
bind.playerHeaderLayout.getRoot().setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 2));
bind.playerHeaderLayout.getRoot().setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8));
}
private void customizeBottomSheetAction() {

View File

@@ -178,7 +178,7 @@ public class PlaylistCatalogueFragment extends Fragment implements ClickCallback
@Override
public void onPlaylistLongClick(Bundle bundle) {
PlaylistEditorDialog dialog = new PlaylistEditorDialog();
PlaylistEditorDialog dialog = new PlaylistEditorDialog(null);
dialog.setArguments(bundle);
dialog.show(activity.getSupportFragmentManager(), null);
hideKeyboard(requireView());

View File

@@ -162,7 +162,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
private void initBackCover() {
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
if (bind != null) {
if (bind != null && songs != null && !songs.isEmpty()) {
Collections.shuffle(songs);
// Pic top-left

View File

@@ -12,6 +12,8 @@ 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.preference.ListPreference;
@@ -21,12 +23,19 @@ import androidx.preference.PreferenceFragmentCompat;
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.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.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 SettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = "SettingsFragment";
@@ -72,39 +81,16 @@ public class SettingsFragment extends PreferenceFragmentCompat {
super.onResume();
checkEqualizer();
checkStorage();
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
setAppLanguage();
setVersion();
findPreference("logout").setOnPreferenceClickListener(preference -> {
activity.quit();
return true;
});
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) {
getScanStatus();
}
});
return true;
});
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof Boolean) {
if ((Boolean) newValue) {
StarredSyncDialog dialog = new StarredSyncDialog();
dialog.show(activity.getSupportFragmentManager(), null);
}
}
return true;
});
actionLogout();
actionScan();
actionSyncStarredTracks();
actionChangeDownloadStorage();
actionDeleteDownloadStorage();
}
@Override
@@ -144,6 +130,110 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
private void checkStorage() {
Preference storage = findPreference("download_storage");
if (storage == null) return;
try {
if (requireContext().getExternalFilesDirs(null)[1] == null) {
storage.setVisible(false);
} else {
storage.setSummary(Preferences.getDownloadStoragePreference() == 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 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);
localePref.setDefaultValue(entryValues[0]);
localePref.setSummary(Locale.forLanguageTag(localePref.getValue()).getDisplayLanguage());
localePref.setOnPreferenceChangeListener((preference, newValue) -> {
LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue);
AppCompatDelegate.setApplicationLocales(appLocale);
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) {
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();
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);
}
@Override
public void onNegativeClick() {
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
}
});
dialog.show(activity.getSupportFragmentManager(), null);
return true;
});
}
private void actionDeleteDownloadStorage() {
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
dialog.show(activity.getSupportFragmentManager(), null);
return true;
});
}
private void getScanStatus() {
settingViewModel.getScanStatus(new ScanCallback() {
@Override

View File

@@ -94,7 +94,6 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
albumBottomSheetViewModel.setFavorite();
dismissBottomSheet();
});
TextView playRadio = view.findViewById(R.id.play_radio_text_view);

View File

@@ -82,7 +82,6 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
artistBottomSheetViewModel.setFavorite();
dismissBottomSheet();
});
TextView playRadio = view.findViewById(R.id.play_radio_text_view);

View File

@@ -90,7 +90,6 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
songBottomSheetViewModel.setFavorite(requireContext());
dismissBottomSheet();
});
favoriteToggle.setOnLongClickListener(v -> {
Bundle bundle = new Bundle();
@@ -216,7 +215,6 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private void initDownloadUI(TextView download, TextView remove) {
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
download.setVisibility(View.GONE);
remove.setVisibility(View.VISIBLE);
} else {
download.setVisibility(View.VISIBLE);

View File

@@ -76,6 +76,12 @@ object Constants {
const val DOWNLOAD_URI = "rest/download"
const val DOWNLOAD_TYPE_TRACK = "download_type_track"
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
const val DOWNLOAD_TYPE_ARTIST = "download_type_artist"
const val DOWNLOAD_TYPE_GENRE = "download_type_genre"
const val DOWNLOAD_TYPE_YEAR = "download_type_year"
const val PLAYABLE_MEDIA_LIMIT = 100
const val PRE_PLAYABLE_MEDIA = 15
}

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.util;
import android.content.Context;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.StandaloneDatabaseProvider;
@@ -23,6 +24,8 @@ import java.io.File;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
@UnstableApi
@@ -127,9 +130,19 @@ public final class DownloadUtil {
private static synchronized File getDownloadDirectory(Context context) {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null);
if (downloadDirectory == null) {
downloadDirectory = context.getFilesDir();
if (Preferences.getDownloadStoragePreference() == 0) {
downloadDirectory = context.getExternalFilesDirs(null)[0];
if (downloadDirectory == null) {
downloadDirectory = context.getFilesDir();
}
} else {
try {
downloadDirectory = context.getExternalFilesDirs(null)[1];
} catch (Exception exception) {
downloadDirectory = context.getExternalFilesDirs(null)[0];
Preferences.setDownloadStoragePreference(0);
}
}
}
@@ -143,4 +156,32 @@ public final class DownloadUtil {
.setCacheWriteDataSinkFactory(null)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
}
public static synchronized void eraseDownloadFolder(Context context) {
File directory = getDownloadDirectory(context);
ArrayList<File> files = listFiles(directory, new ArrayList<>());
for (File file : files) {
file.delete();
}
}
private static synchronized ArrayList<File> listFiles(File directory, ArrayList<File> files) {
if (directory.isDirectory()) {
File[] list = directory.listFiles();
if (list != null) {
for (File file : list) {
if (file.isFile() && file.getName().toLowerCase().endsWith(".exo")) {
files.add(file);
} else if (file.isDirectory()) {
listFiles(file, files);
}
}
}
}
return files;
}
}

View File

@@ -10,6 +10,8 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
@@ -54,8 +56,8 @@ public class MappingUtil {
bundle.putBoolean("isVideo", media.isVideo());
bundle.putInt("userRating", media.getUserRating() != null ? media.getUserRating() : 0);
bundle.putDouble("averageRating", media.getAverageRating() != null ? media.getAverageRating() : 0);
bundle.putLong("playCount", media.getPlayCount() != null ? media.getTrack() : 0);
bundle.putInt("discNumber", media.getDiscNumber() != null ? media.getTrack() : 0);
bundle.putLong("playCount", media.getPlayCount() != null ? media.getPlayCount() : 0);
bundle.putInt("discNumber", media.getDiscNumber() != null ? media.getDiscNumber() : 0);
bundle.putLong("created", media.getCreated() != null ? media.getCreated().getTime() : 0);
bundle.putLong("starred", media.getStarred() != null ? media.getStarred().getTime() : 0);
bundle.putString("albumId", media.getAlbumId());
@@ -71,9 +73,9 @@ public class MappingUtil {
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(media.getTitle()))
.setTrackNumber(media.getTrack())
.setDiscNumber(media.getDiscNumber())
.setReleaseYear(media.getYear())
.setTrackNumber(media.getTrack() != null ? media.getTrack() : 0)
.setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0)
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.setExtras(bundle)
@@ -106,20 +108,20 @@ public class MappingUtil {
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(media.getTitle()))
.setTrackNumber(media.getTrack())
.setDiscNumber(media.getDiscNumber())
.setReleaseYear(media.getYear())
.setTrackNumber(media.getTrack() != null ? media.getTrack() : 0)
.setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0)
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.build()
)
.setRequestMetadata(
new MediaItem.RequestMetadata.Builder()
.setMediaUri(MusicUtil.getDownloadUri(media.getId()))
.setMediaUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId()))
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(MusicUtil.getDownloadUri(media.getId()))
.setUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId()))
.build();
}
@@ -178,8 +180,8 @@ public class MappingUtil {
bundle.putBoolean("isVideo", podcastEpisode.isVideo());
bundle.putInt("userRating", podcastEpisode.getUserRating() != null ? podcastEpisode.getUserRating() : 0);
bundle.putDouble("averageRating", podcastEpisode.getAverageRating() != null ? podcastEpisode.getAverageRating() : 0);
bundle.putLong("playCount", podcastEpisode.getPlayCount() != null ? podcastEpisode.getTrack() : 0);
bundle.putInt("discNumber", podcastEpisode.getDiscNumber() != null ? podcastEpisode.getTrack() : 0);
bundle.putLong("playCount", podcastEpisode.getPlayCount() != null ? podcastEpisode.getPlayCount() : 0);
bundle.putInt("discNumber", podcastEpisode.getDiscNumber() != null ? podcastEpisode.getDiscNumber() : 0);
bundle.putLong("created", podcastEpisode.getCreated() != null ? podcastEpisode.getCreated().getTime() : 0);
bundle.putLong("starred", podcastEpisode.getStarred() != null ? podcastEpisode.getStarred().getTime() : 0);
bundle.putString("albumId", podcastEpisode.getAlbumId());
@@ -195,9 +197,9 @@ public class MappingUtil {
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(podcastEpisode.getTitle()))
.setTrackNumber(podcastEpisode.getTrack())
.setDiscNumber(podcastEpisode.getDiscNumber())
.setReleaseYear(podcastEpisode.getYear())
.setTrackNumber(podcastEpisode.getTrack() != null ? podcastEpisode.getTrack() : 0)
.setDiscNumber(podcastEpisode.getDiscNumber() != null ? podcastEpisode.getDiscNumber() : 0)
.setReleaseYear(podcastEpisode.getYear() != null ? podcastEpisode.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(podcastEpisode.getAlbum()))
.setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist()))
.setExtras(bundle)
@@ -216,13 +218,18 @@ public class MappingUtil {
private static Uri getUri(Child media) {
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
? MusicUtil.getDownloadUri(media.getId())
? getDownloadUri(media.getId())
: MusicUtil.getStreamUri(media.getId());
}
private static Uri getUri(PodcastEpisode podcastEpisode) {
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getId())
? MusicUtil.getDownloadUri(podcastEpisode.getId())
: MusicUtil.getStreamUri(podcastEpisode.getId());
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
? getDownloadUri(podcastEpisode.getStreamId())
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
}
private static Uri getDownloadUri(String id) {
Download download = new DownloadRepository().getDownload(id);
return download != null && !download.getDownloadUri().isEmpty() ? Uri.parse(download.getDownloadUri()) : MusicUtil.getDownloadUri(id);
}
}

View File

@@ -9,6 +9,9 @@ import android.text.Html;
import android.util.Log;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
import java.util.ArrayList;
@@ -29,7 +32,7 @@ public class MusicUtil {
uri.append("stream");
if (params.containsKey("u") && params.get("u") != null)
uri.append("?u=").append(params.get("u"));
uri.append("?u=").append(Util.encode(params.get("u")));
if (params.containsKey("p") && params.get("p") != null)
uri.append("&p=").append(params.get("p"));
if (params.containsKey("s") && params.get("s") != null)
@@ -54,15 +57,49 @@ public class MusicUtil {
}
public static Uri getDownloadUri(String id) {
StringBuilder uri = new StringBuilder();
Download download = new DownloadRepository().getDownload(id);
if (download == null || download.getDownloadUri().isEmpty()) {
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
uri.append(App.getSubsonicClientInstance(false).getUrl());
uri.append("download");
if (params.containsKey("u") && params.get("u") != null)
uri.append("?u=").append(Util.encode(params.get("u")));
if (params.containsKey("p") && params.get("p") != null)
uri.append("&p=").append(params.get("p"));
if (params.containsKey("s") && params.get("s") != null)
uri.append("&s=").append(params.get("s"));
if (params.containsKey("t") && params.get("t") != null)
uri.append("&t=").append(params.get("t"));
if (params.containsKey("v") && params.get("v") != null)
uri.append("&v=").append(params.get("v"));
if (params.containsKey("c") && params.get("c") != null)
uri.append("&c=").append(params.get("c"));
uri.append("&id=").append(id);
} else {
uri.append(download.getDownloadUri());
}
Log.d(TAG, "getDownloadUri: " + uri);
return Uri.parse(uri.toString());
}
public static Uri getTranscodedDownloadUri(String id) {
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
StringBuilder uri = new StringBuilder();
uri.append(App.getSubsonicClientInstance(false).getUrl());
uri.append("download");
uri.append("stream");
if (params.containsKey("u") && params.get("u") != null)
uri.append("?u=").append(params.get("u"));
uri.append("?u=").append(Util.encode(params.get("u")));
if (params.containsKey("p") && params.get("p") != null)
uri.append("&p=").append(params.get("p"));
if (params.containsKey("s") && params.get("s") != null)
@@ -74,13 +111,19 @@ public class MusicUtil {
if (params.containsKey("c") && params.get("c") != null)
uri.append("&c=").append(params.get("c"));
if (!Preferences.isServerPrioritizedInTranscodedDownload())
uri.append("&maxBitRate=").append(getBitratePreferenceForDownload());
if (!Preferences.isServerPrioritizedInTranscodedDownload())
uri.append("&format=").append(getTranscodingFormatPreferenceForDownload());
uri.append("&id=").append(id);
Log.d(TAG, "getDownloadUri: " + uri);
Log.d(TAG, "getTranscodedDownloadUri: " + uri);
return Uri.parse(uri.toString());
}
public static String getReadableDurationString(long duration, boolean millis) {
long minutes;
long seconds;
@@ -122,6 +165,14 @@ public class MusicUtil {
return "";
}
public static String getReadableTrackNumber(Context context, Integer trackNumber) {
if (trackNumber != null) {
return String.valueOf(trackNumber);
}
return context.getString(R.string.label_placeholder);
}
public static String forceReadableString(String string) {
if (string != null) {
return getReadableString(string)
@@ -196,6 +247,19 @@ public class MusicUtil {
}
}
public static String getBitratePreferenceForDownload() {
String audioTranscodeFormat = getTranscodingFormatPreferenceForDownload();
if (audioTranscodeFormat.equals("raw"))
return "0";
return Preferences.getBitrateTranscodedDownload();
}
public static String getTranscodingFormatPreferenceForDownload() {
return Preferences.getAudioTranscodeFormatTranscodedDownload();
}
public static List<Child> limitPlayableMedia(List<Child> toLimit, int position) {
if (!toLimit.isEmpty() && toLimit.size() > Constants.PLAYABLE_MEDIA_LIMIT) {
int from = position < Constants.PRE_PLAYABLE_MEDIA ? 0 : position - Constants.PRE_PLAYABLE_MEDIA;
@@ -207,8 +271,12 @@ public class MusicUtil {
return toLimit;
}
public static int getPlayableMediaPosition(int initialPosition) {
return initialPosition > Constants.PLAYABLE_MEDIA_LIMIT ? Constants.PRE_PLAYABLE_MEDIA : initialPosition;
public static int getPlayableMediaPosition(List<Child> toLimit, int position) {
if (!toLimit.isEmpty() && toLimit.size() > Constants.PLAYABLE_MEDIA_LIMIT) {
return Math.min(position, Constants.PRE_PLAYABLE_MEDIA);
}
return position;
}
private static ConnectivityManager getConnectivityManager() {

View File

@@ -33,6 +33,12 @@ object Preferences {
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
private const val DOWNLOAD_STORAGE = "download_storage"
private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
private const val MAX_BITRATE_DOWNLOAD = "max_bitrate_download"
private const val AUDIO_TRANSCODE_FORMAT_DOWNLOAD = "audio_transcode_format_download"
@JvmStatic
fun getServer(): String? {
@@ -258,4 +264,53 @@ object Preferences {
fun isServerPrioritized(): Boolean {
return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_PRIORITY, false)
}
@JvmStatic
fun getDownloadStoragePreference(): Int {
return App.getInstance().preferences.getString(DOWNLOAD_STORAGE, "0")!!.toInt()
}
@JvmStatic
fun setDownloadStoragePreference(storagePreference: Int) {
return App.getInstance().preferences.edit().putString(
DOWNLOAD_STORAGE,
storagePreference.toString()
).apply()
}
@JvmStatic
fun getDefaultDownloadViewType(): String {
return App.getInstance().preferences.getString(
DEFAULT_DOWNLOAD_VIEW_TYPE,
Constants.DOWNLOAD_TYPE_TRACK
)!!
}
@JvmStatic
fun setDefaultDownloadViewType(viewType: String) {
return App.getInstance().preferences.edit().putString(
DEFAULT_DOWNLOAD_VIEW_TYPE,
viewType
).apply()
}
@JvmStatic
fun preferTranscodedDownload(): Boolean {
return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_DOWNLOAD, false)
}
@JvmStatic
fun isServerPrioritizedInTranscodedDownload(): Boolean {
return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_DOWNLOAD_PRIORITY, false)
}
@JvmStatic
fun getBitrateTranscodedDownload(): String {
return App.getInstance().preferences.getString(MAX_BITRATE_DOWNLOAD, "0")!!
}
@JvmStatic
fun getAudioTranscodeFormatTranscodedDownload(): String {
return App.getInstance().preferences.getString(AUDIO_TRANSCODE_FORMAT_DOWNLOAD, "raw")!!
}
}

View File

@@ -5,8 +5,21 @@ import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import androidx.core.os.LocaleListCompat;
import androidx.recyclerview.widget.DividerItemDecoration;
import com.cappielloantonio.tempo.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class UIUtil {
public static int getSpanCount(int itemCount, int maxSpan) {
int itemSize = itemCount == 0 ? 1 : itemCount;
@@ -31,4 +44,44 @@ public class UIUtil {
return itemDecoration;
}
private static LocaleListCompat getLocalesFromResources(Context context) {
final List<String> tagsList = new ArrayList<>();
XmlPullParser xpp = context.getResources().getXml(R.xml.locale_config);
try {
while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) {
String tagName = xpp.getName();
if (xpp.getEventType() == XmlPullParser.START_TAG) {
if ("locale".equals(tagName) && xpp.getAttributeCount() > 0 && xpp.getAttributeName(0).equals("name")) {
tagsList.add(xpp.getAttributeValue(0));
}
}
xpp.next();
}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
return LocaleListCompat.forLanguageTags(String.join(",", tagsList));
}
public static Map<String, String> getLangPreferenceDropdownEntries(Context context) {
LocaleListCompat localeList = getLocalesFromResources(context);
Map<String, String> map = new HashMap<>();
for (int i = 0; i < localeList.size(); i++) {
Locale locale = localeList.get(i);
if (locale != null) {
map.put(Util.toPascalCase(locale.getDisplayName()), locale.toLanguageTag());
}
}
return map;
}
}

View File

@@ -0,0 +1,64 @@
package com.cappielloantonio.tempo.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
public class Util {
public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
try {
Map<Object, Boolean> uniqueMap = new ConcurrentHashMap<>();
return t -> uniqueMap.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
} catch (NullPointerException exception) {
return null;
}
}
public static String toPascalCase(String name) {
if (name == null || name.isEmpty()) {
return name;
}
StringBuilder pascalCase = new StringBuilder();
char newChar;
boolean toUpper = false;
char[] charArray = name.toCharArray();
for (int ctr = 0; ctr <= charArray.length - 1; ctr++) {
if (ctr == 0) {
newChar = Character.toUpperCase(charArray[ctr]);
pascalCase = new StringBuilder(Character.toString(newChar));
continue;
}
if (charArray[ctr] == '_') {
toUpper = true;
continue;
}
if (toUpper) {
newChar = Character.toUpperCase(charArray[ctr]);
pascalCase.append(newChar);
toUpper = false;
continue;
}
pascalCase.append(charArray[ctr]);
}
return pascalCase.toString();
}
public static String encode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException ex) {
return value;
}
}
}

View File

@@ -8,27 +8,56 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.model.DownloadStack;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class DownloadViewModel extends AndroidViewModel {
private static final String TAG = "HomeViewModel";
private static final String TAG = "DownloadViewModel";
private final DownloadRepository downloadRepository;
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
public DownloadViewModel(@NonNull Application application) {
super(application);
downloadRepository = new DownloadRepository();
initViewStack(new DownloadStack(Preferences.getDefaultDownloadViewType(), null));
}
public LiveData<List<Child>> getDownloadedTracks(LifecycleOwner owner) {
downloadRepository.getLiveDownload().observe(owner, downloads -> downloadedTrackSample.postValue(downloads.stream().map(download -> (Child) download).collect(Collectors.toList())));
return downloadedTrackSample;
}
public LiveData<ArrayList<DownloadStack>> getViewStack() {
return viewStack;
}
public void initViewStack(DownloadStack level) {
ArrayList<DownloadStack> stack = new ArrayList<>();
stack.add(level);
viewStack.setValue(stack);
}
public void pushViewStack(DownloadStack level) {
ArrayList<DownloadStack> stack = viewStack.getValue();
stack.add(level);
viewStack.setValue(stack);
}
public void popViewStack() {
ArrayList<DownloadStack> stack = viewStack.getValue();
stack.remove(stack.size() - 1);
viewStack.setValue(stack);
}
}

View File

@@ -37,8 +37,7 @@ public class PlaylistEditorViewModel extends AndroidViewModel {
}
public void updatePlaylist(String name) {
playlistRepository.deletePlaylist(toEdit.getId());
playlistRepository.createPlaylist(toEdit.getId(), name, getPlaylistSongIds());
playlistRepository.updatePlaylist(toEdit.getId(), name, getPlaylistSongIds());
}
public void deletePlaylist() {

View File

@@ -73,9 +73,11 @@ public class SongListPageViewModel extends AndroidViewModel {
case Constants.MEDIA_BY_GENRE:
int page = (songList.getValue() != null ? songList.getValue().size() : 0) / 100;
songRepository.getSongsByGenre(genre.getGenre(), page).observe(owner, children -> {
List<Child> currentMedia = songList.getValue();
currentMedia.addAll(children);
songList.setValue(currentMedia);
if (children != null && !children.isEmpty()) {
List<Child> currentMedia = songList.getValue();
currentMedia.addAll(children);
songList.setValue(currentMedia);
}
});
break;
case Constants.MEDIA_BY_ARTIST:

View File

@@ -0,0 +1,15 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/delete_download_storage_dialog_summary" />
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/download_storage_dialog_summary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/download_storage_dialog_sub_summary" />
</LinearLayout>

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -66,6 +66,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="@dimen/global_padding_bottom"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -67,6 +67,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="@dimen/global_padding_bottom"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -38,10 +38,11 @@
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="24dp"
android:paddingBottom="16dp"
android:layout_marginBottom="8dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/directory_back_image_view"
app:layout_constraintStart_toStartOf="parent" />
<Button
@@ -50,16 +51,15 @@
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_arrow_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@+id/directory_title_label"
app:layout_constraintBottom_toBottomOf="@+id/directory_title_label"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
@@ -69,6 +69,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="12dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -62,45 +62,68 @@
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/download_linear_layout_container"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_downloaded_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="@dimen/global_padding_bottom">
<!-- Downloaded tracks -->
<LinearLayout
android:id="@+id/download_downloaded_tracks_sector"
<TextView
android:id="@+id/downloaded_text_view_refreshable"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/download_title_section"
android:layout_marginBottom="16dp"
app:layout_constraintEnd_toStartOf="@+id/downloaded_go_back_image_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/downloaded_go_back_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginHorizontal="12dp"
android:layout_gravity="center"
android:background="@drawable/ic_arrow_back"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<ImageView
android:id="@+id/downloaded_group_by_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:background="@drawable/ic_filter_list"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloaded_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/downloaded_tracks_text_view_refreshable"
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/download_title_section" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloaded_tracks_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingBottom="8dp" />
</LinearLayout>
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/downloaded_text_view_refreshable" />
<include
android:id="@+id/download_downloaded_tracks_placeholder"
android:id="@+id/download_downloaded_placeholder"
layout="@layout/item_placeholder_horizontal"
android:visibility="gone" />
</LinearLayout>
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/downloaded_text_view_refreshable" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -85,6 +85,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="@dimen/global_padding_bottom"

View File

@@ -296,7 +296,8 @@
android:id="@+id/home_grid_tracks_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/grid_tracks_pre_text_view"
@@ -306,7 +307,7 @@
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:text="Last week"
android:text="@string/home_title_last_week"
android:textAllCaps="true" />
<TextView
@@ -316,7 +317,7 @@
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="Your top songs" />
android:text="@string/home_title_top_songs" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_tracks_recycler_view"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -57,6 +57,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="12dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -9,12 +9,12 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -67,6 +67,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="12dp"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:paddingBottom="@dimen/global_padding_bottom"

View File

@@ -9,6 +9,7 @@
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -50,6 +50,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="@dimen/global_padding_bottom"

View File

@@ -10,6 +10,7 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
@@ -101,6 +102,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

View File

@@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -69,6 +69,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="12dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -3,8 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="horizontal"
android:paddingStart="16dp">
android:orientation="horizontal">
<LinearLayout
android:id="@+id/divider"
@@ -17,7 +16,7 @@
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/downloaded_song_album_text_view"
android:id="@+id/downloaded_item_pre_text_view"
style="@style/LabelExtraSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -28,8 +27,20 @@
android:layout_gravity="center_vertical" />
</LinearLayout>
<ImageView
android:id="@+id/item_cover_image_view"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toStartOf="@id/downloaded_item_title_text_view"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<TextView
android:id="@+id/downloaded_song_title_text_view"
android:id="@+id/downloaded_item_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -37,12 +48,14 @@
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@+id/downloaded_song_more_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintEnd_toStartOf="@+id/downloaded_item_more_button"
app:layout_constraintStart_toEndOf="@+id/item_cover_image_view"
app:layout_constraintTop_toTopOf="@+id/item_cover_image_view"
app:layout_constraintBottom_toTopOf="@id/downloaded_item_subtitle_text_view"/>
<TextView
android:id="@+id/downloaded_song_artist_text_view"
android:id="@+id/downloaded_item_subtitle_text_view"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -51,16 +64,15 @@
android:paddingBottom="6dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@+id/downloaded_song_more_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/downloaded_song_title_text_view" />
app:layout_constraintTop_toBottomOf="@id/downloaded_item_title_text_view"
app:layout_constraintEnd_toStartOf="@+id/downloaded_item_more_button"
app:layout_constraintStart_toStartOf="@+id/downloaded_item_title_text_view"
app:layout_constraintBottom_toBottomOf="@+id/item_cover_image_view" />
<ImageView
android:id="@+id/downloaded_song_more_button"
android:id="@+id/downloaded_item_more_button"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="10dp"
android:background="@drawable/ic_more_vert"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -52,5 +52,19 @@
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/music_directory_title_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view" />
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view"
android:visibility="gone"/>
<ImageView
android:id="@+id/music_directory_play_button"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/ic_play"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/music_directory_title_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_download_directory"
android:icon="@drawable/ic_file_download"
android:title="@string/menu_download_all_button"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_download_group_by_track"
android:title="@string/menu_group_by_track" />
<item
android:id="@+id/menu_download_group_by_album"
android:title="@string/menu_group_by_album" />
<item
android:id="@+id/menu_download_group_by_artist"
android:title="@string/menu_group_by_artist" />
<item
android:id="@+id/menu_download_group_by_genre"
android:title="@string/menu_group_by_genre" />
<item
android:id="@+id/menu_download_group_by_year"
android:title="@string/menu_group_by_year" />
</menu>

View File

@@ -0,0 +1,155 @@
<resources>
<string-array name="theme_list_titles">
<item>Hell</item>
<item>Dunkel</item>
<item>System Vorgabe</item>
</string-array>
<string-array name="theme_list_values">
<item>light</item>
<item>dark</item>
<item>default</item>
</string-array>
<string-array name="pref_cache_size_titles">
<item>Hoch</item>
<item>Mittel</item>
<item>Niedrig</item>
</string-array>
<string-array name="pref_cache_size_values">
<item>500</item>
<item>250</item>
<item>125</item>
</string-array>
<string-array name="pref_image_size_titles">
<item>Hoch</item>
<item>Mittel</item>
<item>Niedrig</item>
</string-array>
<string-array name="pref_image_size_values">
<item>-1</item>
<item>500</item>
<item>300</item>
</string-array>
<string-array name="max_bitrate_wifi_list_titles">
<item>Original</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_wifi_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="max_bitrate_mobile_list_titles">
<item>Original</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_mobile_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_titles">
<item>Direktes Abspielen</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="audio_transcode_format_mobile_list_titles">
<item>Direct play</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_mobile_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="queue_syncing_countdown_titles">
<item>Zehn Sekunden</item>
<item>Fünf Sekunden</item>
<item>Zwei Sekunden</item>
</string-array>
<string-array name="queue_syncing_countdown_values">
<item>10</item>
<item>5</item>
<item>2</item>
</string-array>
<string-array name="rounded_corner_size_titles">
<item>Hoch</item>
<item>Mittel</item>
<item>Niedrig</item>
</string-array>
<string-array name="rounded_corner_size_values">
<item>18</item>
<item>12</item>
<item>6</item>
</string-array>
<string-array name="replay_gain_titles">
<item>Deaktiviert</item>
<item>Track bevorzugt</item>
<item>Album bevorzugt</item>
</string-array>
<string-array name="replay_gain_values">
<item>disabled</item>
<item>track</item>
<item>album</item>
</string-array>
</resources>

View File

@@ -0,0 +1,300 @@
<resources>
<string name="activity_battery_optimizations_summary">Bitte deaktiviere die Batterieoptimierung, damit die Medienwiedergabe bei ausgeschaltetem Bildschirm richtig funktioniert.</string>
<string name="activity_battery_optimizations_conclusion">Bei Problemen besuche https://dontkillmyapp.com. Dort findest Du detaillierte Anweisungen wie Du Energiesparfunktionen, welche die App-Performance beeinflussen können, deaktivieren kannst.</string>
<string name="activity_battery_optimizations_title">Batterie Optimierung</string>
<string name="activity_info_offline_mode">Offlinebetrieb</string>
<string name="battery_optimization_negative_button">Ignorieren</string>
<string name="battery_optimization_positive_button">Ausschalten</string>
<string name="battery_optimization_neutral_button">Nicht wieder fragen</string>
<string name="album_bottom_sheet_add_to_queue">Zur Warteschlange hinzufügen</string>
<string name="album_bottom_sheet_download_all">Alle herunterladen</string>
<string name="album_bottom_sheet_go_to_artist">Gehe zu Künstler</string>
<string name="album_bottom_sheet_instant_mix">Sofort-Mix</string>
<string name="album_bottom_sheet_play_next">Nächsten Titel spielen</string>
<string name="album_bottom_sheet_remove_all">Alle entfernen</string>
<string name="album_bottom_sheet_shuffle">Mischen</string>
<string name="album_catalogue_title">Alben</string>
<string name="album_catalogue_title_expanded">Alben durchsuchen</string>
<string name="album_error_retrieving_artist">Error retrieving artist</string>
<string name="album_list_page_downloaded">Heruntergeladene Alben</string>
<string name="album_list_page_most_played">Oft gehörte Alben</string>
<string name="album_list_page_new_releases">Neue Releases</string>
<string name="album_list_page_recently_added">Kürzlich hinzugefügte Alben</string>
<string name="album_list_page_recently_played">Kürzlich gespielte Alben</string>
<string name="album_list_page_starred">Lieblingsalben</string>
<string name="album_list_page_title">Alben</string>
<string name="album_page_extra_info_button">Ähnliches</string>
<string name="album_page_play_button">Wiedergabe</string>
<string name="album_page_shuffle_button">Zufällige Wiedergabe</string>
<string name="app_name">Tempo</string>
<string name="artist_bottom_sheet_instant_mix">Instant mix</string>
<string name="artist_bottom_sheet_shuffle">Mischen</string>
<string name="artist_catalogue_title">Künstler</string>
<string name="artist_catalogue_title_expanded">Künstler durchsuchen</string>
<string name="artist_error_retrieving_radio">Fehler beim Abruf des Künstlerradios</string>
<string name="artist_error_retrieving_tracks">Fehler beim Abruf der Tracks des Künstlers</string>
<string name="artist_list_page_downloaded">Heruntergeladene Künstler</string>
<string name="artist_list_page_starred">Lieblingskünstler</string>
<string name="artist_list_page_title">Künstler</string>
<string name="artist_page_radio_button">Radio</string>
<string name="artist_page_shuffle_button">Mischen</string>
<string name="artist_page_title_album_more_like_this_button">Ähnliches</string>
<string name="artist_page_title_album_section">Alben</string>
<string name="artist_page_title_biography_more_button">Mehr</string>
<string name="artist_page_title_biography_section">Biographie</string>
<string name="artist_page_title_most_streamed_song_section">Oft gestreamte Tracks</string>
<string name="artist_page_title_most_streamed_song_see_all_button">Alles</string>
<string name="connection_alert_dialog_negative_button">Abbrechen</string>
<string name="connection_alert_dialog_neutral_button">Enable data saver</string>
<string name="connection_alert_dialog_positive_button">OK</string>
<string name="connection_alert_dialog_title">Wi-Fi ist nicht verbuden</string>
<string name="connection_alert_dialog_summary">Der Zugriff auf den Subsonic server ohne Wi-Fi Verbindung ist deaktiviert. Du kannst das in den App-Einstellungen ändern.</string>
<string name="delete_download_storage_dialog_negative_button">Abbrechen</string>
<string name="delete_download_storage_dialog_positive_button">Weiter</string>
<string name="delete_download_storage_dialog_summary">Wenn Du weitermachst werden alle zuvor heruntergeladenen Inhalte gelöscht.</string>
<string name="delete_download_storage_dialog_title">Heruntergeladene Inhalte löschen</string>
<string name="download_info_empty_subtitle">Wenn Du einen Track heruntergeladen hast findest Du ihn hier</string>
<string name="download_info_empty_title">Bisher keine Downloads!</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</string>
<string name="download_item_single_subtitle_formatter">%1$s items</string>
<string name="download_title_section">Downloads</string>
<string name="download_storage_internal_dialog_negative_button">Intern</string>
<string name="download_storage_external_dialog_positive_button">Extern</string>
<string name="download_storage_dialog_summary">Das Ändern des Speicherorts löscht alle Inhalte im zuvor gewählten Speicherort.</string>
<string name="download_storage_dialog_sub_summary">Neustart der Anwendung ist nötig.</string>
<string name="download_storage_dialog_title">Wähle den Speicherort aus</string>
<string name="empty_string" />
<string name="error_required">Benötigt</string>
<string name="error_server_prefix">http or https prefix benötigt</string>
<string name="exo_download_notification_channel_name">Downloads</string>
<string name="filter_info_selection">Wähle mindestens zwei Filter aus</string>
<string name="filter_title">Filter</string>
<string name="filter_title_expanded">Genres filtern</string>
<string name="genre_catalogue_title">Genre Übersicht</string>
<string name="genre_catalogue_title_expanded">Genres durchsuchen</string>
<string name="home_subtitle_new_internet_radio_station">Radio hinzufügen</string>
<string name="home_subtitle_new_podcast_channel">Podcast Kanal hinzufügen</string>
<string name="home_subtitle_made_for_you">Ein Mix von einem deiner Lieblingslieder erstellen</string>
<string name="home_sync_starred_title">Einige Lieblingslieder müssen synchronisiert werden</string>
<string name="home_sync_starred_subtitle">Das Herunterladen dieser Tracks kann erheblichen Datenverbrauch verursachen</string>
<string name="home_sync_starred_cancel">Abbrechen</string>
<string name="home_sync_starred_download">Download</string>
<string name="home_title_flashback">Flashback</string>
<string name="home_title_last_played">Zuletzt gespielt</string>
<string name="home_title_last_played_see_all_button">Alle zeigen</string>
<string name="home_title_made_for_you">Wie für Dich gemacht</string>
<string name="home_title_most_played">Oft gespielt</string>
<string name="home_title_most_played_see_all_button">Alle zeigen</string>
<string name="home_title_discovery">Entdeckungsreise</string>
<string name="home_title_recently_added">Kürzlich hinzugefügt</string>
<string name="home_title_recently_added_see_all_button">Alle zeigen</string>
<string name="home_title_discovery_shuffle_all_button">Alle mischen</string>
<string name="home_title_starred_albums">★ Lieblingsalben</string>
<string name="home_title_starred_albums_see_all_button">Alle zeigen</string>
<string name="home_title_starred_artists">★ Lieblingskünstler</string>
<string name="home_title_starred_artists_see_all_button">Alle zeigen</string>
<string name="home_title_starred_tracks">★ Lieblingslieder</string>
<string name="home_title_starred_tracks_see_all_button">Alle zeigen</string>
<string name="home_title_internet_radio_station">Internet Radios</string>
<string name="home_title_podcast_channels_see_all_button">Alle zeigen</string>
<string name="library_title_music_folder">Sammlung</string>
<string name="library_title_album">Alben</string>
<string name="library_title_album_see_all_button">Alle zeigen</string>
<string name="library_title_artist">Künstler</string>
<string name="library_title_artist_see_all_button">Alle zeigen</string>
<string name="library_title_genre">Genres</string>
<string name="library_title_genre_see_all_button">Alle zeigen</string>
<string name="library_title_playlist">Playlisten</string>
<string name="library_title_playlist_see_all_button">Alle zeigen</string>
<string name="login_empty">Kein Server hinzugefügt</string>
<string name="login_title">Subsonic Server</string>
<string name="login_title_expanded">Subsonic Server</string>
<string name="media_route_menu_title">Cast</string>
<string name="menu_add_button">Hinzufügen</string>
<string name="menu_download_all_button">Alle Herunterladen</string>
<string name="menu_download_label">Downloads</string>
<string name="menu_home_label">Start</string>
<string name="menu_library_label">Sammlung</string>
<string name="menu_search_button">Suche</string>
<string name="menu_settings_button">Einstellungen</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_server_priority">Server Priorität</string>
<string name="playlist_catalogue_title">Playlisten</string>
<string name="playlist_catalogue_title_expanded">Playlisten durchsuchen</string>
<string name="playlist_chooser_dialog_empty">Keine Playlisten erstellt</string>
<string name="playlist_chooser_dialog_negative_button">Abbrechen</string>
<string name="playlist_chooser_dialog_neutral_button">Erstellen</string>
<string name="playlist_chooser_dialog_title">Zu einer Playliste hinzufügen</string>
<string name="playlist_counted_tracks">%1$d Tracks • %2$s</string>
<string name="playlist_duration">Länge • %1$s</string>
<string name="playlist_editor_dialog_hint_name">Name der Playliste</string>
<string name="playlist_editor_dialog_negative_button">Abbrechen</string>
<string name="playlist_editor_dialog_neutral_button">Löschen</string>
<string name="playlist_editor_dialog_positive_button">Speichern</string>
<string name="playlist_editor_dialog_title">Playliste erstellen</string>
<string name="playlist_page_play_button">Wiedergabe</string>
<string name="playlist_page_shuffle_button">Shuffle</string>
<string name="playlist_song_count">Playliste • %1$d Tracks</string>
<string name="podcast_channel_catalogue_title_expanded">Kanäle durchsuchen</string>
<string name="podcast_channel_catalogue_title">Kanäle</string>
<string name="podcast_channel_page_title_description_section">Beschreibung</string>
<string name="podcast_channel_page_title_episode_section">Episoden</string>
<string name="podcast_channel_page_title_no_episode_available">Keine Episoden verfügbar</string>
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS Url</string>
<string name="podcast_channel_editor_dialog_title">Podcast Kanal</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="podcast_episode_download_request_snackbar">Der Request wurde an den Server geschickt.</string>
<string name="podcast_info_empty_subtitle">Wenn Du einen Kanal hinzufügst findest Du ihn hier</string>
<string name="podcast_info_empty_title">Keine Podcasts gefunden.</string>
<string name="podcast_info_empty_button">Hier klicken, um den Bereich auszublenden\nAnwendungsneustart ist notwendig</string>
<string name="radio_editor_dialog_hint_name">Radio Name</string>
<string name="radio_editor_dialog_hint_stream_url">Radio Stream URL</string>
<string name="radio_editor_dialog_hint_homepage_url">Radio Homepage URL</string>
<string name="radio_editor_dialog_negative_button">Abbrechen</string>
<string name="radio_editor_dialog_neutral_button">Löschen</string>
<string name="radio_editor_dialog_positive_button">Speichern</string>
<string name="radio_editor_dialog_title">Internet Radio Station</string>
<string name="radio_station_info_empty_subtitle">Wenn Du eine Radio Station hinzugefügt hast findest Du sie hier</string>
<string name="radio_station_info_empty_title">Keine Radio Stationen gefunden.</string>
<string name="radio_station_info_empty_button">Hier klicken, um den Bereich auszublenden\nAnwendungsneustart ist notwendig</string>
<string name="rating_dialog_negative_button">Abbrechen</string>
<string name="rating_dialog_positive_button">Speichern</string>
<string name="rating_dialog_title">Bewerten</string>
<string name="search_hint">Titel, Künstler oder Alben durchsuchen</string>
<string name="search_info_minimum_characters">Gib mindestens drei Zeichen ein</string>
<string name="search_title_album">Alben</string>
<string name="search_title_artist">Künstler</string>
<string name="search_title_song">Tracks</string>
<string name="server_signup_dialog_action_low_security">Niedrige Sicherheit</string>
<string name="server_signup_dialog_hint_name">Server Name</string>
<string name="server_signup_dialog_hint_password">Passwort</string>
<string name="server_signup_dialog_hint_url">Server URL</string>
<string name="server_signup_dialog_hint_username">Benutzername</string>
<string name="server_signup_dialog_negative_button">Abbrechen</string>
<string name="server_signup_dialog_neutral_button">Löschen</string>
<string name="server_signup_dialog_positive_button">Speichern</string>
<string name="server_signup_dialog_title">Server hinzufügen</string>
<string name="server_unreachable_dialog_negative_button">Abbrechen</string>
<string name="server_unreachable_dialog_neutral_button">Gehe zum Login</string>
<string name="server_unreachable_dialog_positive_button">Trotzdem weitermachen</string>
<string name="server_unreachable_dialog_title">Server nicht erreichbar</string>
<string name="server_unreachable_dialog_summary">Der angefragte Server ist nicht erreichbar. Wenn Du trotzdem weitermachst, wird dieser Dialog für eine Stunden nicht wieder erscheinen.</string>
<string name="settings_about_summary">Tempo ist ein nativ für Android entwickelter, leichtgewichtiger Open-Source Client für Subsonic.</string>
<string name="settings_about_title">Über</string>
<string name="settings_audio_transcode_download_priority_summary">Diese Option deaktiviert die Transkodierungssettings für Downloads.</string>
<string name="settings_audio_transcode_download_priority_title">Transkodierungseinstellungen des Servers für Downloads bevorzugen.</string>
<string name="settings_audio_transcode_download_summary">Diese Option aktiviert das Transkodieren für heruntergeladene Tracks.</string>
<string name="settings_audio_transcode_download_title">Transkodierte Tracks herunterladen</string>
<string name="settings_audio_transcode_download_format">Transkodierungs-Format</string>
<string name="settings_audio_transcode_priority_summary">Diese Option deaktiviert die weiter unten folgenden Transkodierungseinstellungen.</string>
<string name="settings_audio_transcode_priority_title">Transkodierungseinstellungen des Servers bevorzugen</string>
<string name="settings_audio_transcode_priority_toast">Servereinstellungen zur Transkodierung des Tracks werden bevorzugt</string>
<string name="settings_audio_transcode_format_download">Transkodierungs-Format für Downloads</string>
<string name="settings_audio_transcode_format_mobile">Transkodierungsformat im mobilen Netz</string>
<string name="settings_audio_transcode_format_wifi">Transkodierungsformat im Wi-Fi</string>
<string name="settings_covers_cache">Größe des Artwork Caches</string>
<string name="settings_data_saving_mode_summary">Um das Datenvolumen zu begrenzen werden keine Cover heruntergeladen.</string>
<string name="settings_data_saving_mode_title">Mobile Datennutzung begrenzen</string>
<string name="settings_delete_download_storage_title">Gespeicherte Inhalte löschen</string>
<string name="settings_delete_download_storage_summary">Wenn Du weitermachst werden alle gespeicherten Inhalte unwiderruflich gelöscht.</string>
<string name="settings_download_storage_title">Download storage</string>
<string name="settings_equalizer_summary">Audio Einstellungen anpassen</string>
<string name="settings_equalizer_title">Equalizer</string>
<string name="settings_github_link">https://github.com/CappielloAntonio/tempo</string>
<string name="settings_github_summary">Verfolge die Entwicklung</string>
<string name="settings_github_title">Github</string>
<string name="settings_image_size">Bilder Auflösung anpassen</string>
<string name="settings_language">Sprache</string>
<string name="settings_logout_title">Abmelden</string>
<string name="settings_max_bitrate_download">Bitrate für Downloads</string>
<string name="settings_max_bitrate_wifi">Bitrate bei Wi-Fi Nutzung</string>
<string name="settings_max_bitrate_mobile">Bitrate bei mobiler Nutzung</string>
<string name="settings_media_cache">Größe des Medienfile Caches</string>
<string name="settings_music_directory">Zeige Musikverzeichnisse</string>
<string name="settings_music_directory_summary">Zeige den Bereich für Musikverzeichnisse. Der Server muss das Feature unterstützen.</string>
<string name="settings_queue_syncing_title">Warteschlange für diesen User synchronisieren</string>
<string name="settings_queue_syncing_countdown">Timer synchronisieren</string>
<string name="settings_queue_syncing_summary">Der Benutzer kann seine Warteschlange speichern und beim Neustart der Anwendung wiederherstellen.</string>
<string name="settings_podcast">Podcasts anzeigen</string>
<string name="settings_podcast_summary">Zeige den Bereich für Podcasts.</string>
<string name="settings_radio">Radios anzeigen</string>
<string name="settings_radio_summary">Zeige den Bereich für Radios.</string>
<string name="settings_replay_gain">Set replay gain mode</string>
<string name="settings_rounded_corner">Abgerundete Ecken</string>
<string name="settings_rounded_corner_summary">Abgerundete Ecken für alle gerenderten Cover. Anwendungsneustart ist notwendig.</string>
<string name="settings_rounded_corner_size">Eckenradius</string>
<string name="settings_rounded_corner_size_summary">Definiert den Eckenradius.</string>
<string name="settings_scan_title">Sammlung scannen</string>
<string name="settings_summary_replay_gain">Replay-Gain ist ein Feature, das die Lautstärke von Tracks für ein konsistentes Hörerlebnis anpasst. Diese Einstellung funktioniert nur, wenn Tracks die entsprechenden Metadaten haben.</string>
<string name="settings_summary_syncing">Den Zustand der Warteschlange synchronisieren. Das beinhaltet die Tracks in der Warteschlange, den aktuell gespielten Track und die Position innerhalb dieses Tracks. Der Server muss dieses Feature unterstützen.</string>
<string name="settings_summary_transcoding">Priorität des Transkodierungsmodus. \"Direktes Abspielen\" ändert die Bitrate der Dateien nicht.</string>
<string name="settings_summary_transcoding_download">Transkodierte Medien herunterladen. Diese Option deaktiviert den Download-Endpoint und benutzt stattdessen die folgenden Einstellungen. \n\n If \"Transkodierungs-Format\" ist auf \"Direktes Abspielen\" gesetzt, Die Bitrate des Tracks wird nicht geändert.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Lieblingslieder werden automatisch heruntergeladen.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Lieblingslieder für Offline-Modus sychronisieren</string>
<string name="settings_theme">Design</string>
<string name="settings_title_data">Daten</string>
<string name="settings_title_general">Allgemein</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_syncing">Sychronisierung</string>
<string name="settings_title_transcoding">Transkodierung</string>
<string name="settings_title_transcoding_download">Transkodierung Download</string>
<string name="settings_title_ui">Benutzeroberfläche</string>
<string name="settings_transcoded_download">Transcoded download</string>
<string name="settings_version_title">Version</string>
<string name="settings_wifi_only_title">Warnung bei Streamen ohne Wi-Fi</string>
<string name="settings_wifi_only_summary">Um Erlaubnis fragen bevor über das mobile Netzwerk gestreamed wird.</string>
<string name="song_bottom_sheet_add_to_playlist">Zu Playliste hinzufügen</string>
<string name="song_bottom_sheet_add_to_queue">Zur Warteschlange hinzufügen</string>
<string name="song_bottom_sheet_download">Download</string>
<string name="song_bottom_sheet_error_retrieving_album">Fehler beim Abruf des Albums</string>
<string name="song_bottom_sheet_error_retrieving_artist">Fehler beim Abruf des Künstlers</string>
<string name="song_bottom_sheet_go_to_album">Zum Album gehen</string>
<string name="song_bottom_sheet_go_to_artist">Zum Künstler gehen</string>
<string name="song_bottom_sheet_instant_mix">Sofort-Mix</string>
<string name="song_bottom_sheet_play_next">Nächsten Titel spielen</string>
<string name="song_bottom_sheet_rate">Bewerten</string>
<string name="song_bottom_sheet_remove">Entfernen</string>
<string name="song_list_page_downloaded">Heruntergeladen</string>
<string name="song_list_page_most_played">Oft gespielte Tracks</string>
<string name="song_list_page_recently_added">Zuletzt hinzugefügte Tracks</string>
<string name="song_list_page_recently_played">Zuletzt gespielte Tracks</string>
<string name="song_list_page_starred">Lieblingslieder</string>
<string name="song_list_page_top">%1$s\'s Top Tracks</string>
<string name="song_list_page_year">Jahr %1$d</string>
<string name="song_subtitle_formatter">%1$s • %2$s</string>
<string name="starred_sync_dialog_negative_button">Abbrechen</string>
<string name="starred_sync_dialog_neutral_button">Weiter</string>
<string name="starred_sync_dialog_positive_button">Weiter und Herunterladen</string>
<string name="starred_sync_dialog_summary">Das Herunterladen deiner Lieblingslieder kann viel Datenvolumen verbrauchen.</string>
<string name="starred_sync_dialog_title">Lieblingslieder synchronisieren</string>
<string name="undraw_url">https://undraw.co/</string>
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">Besonders möchten wir uns bei unDraw bedanken, durch deren Illustrationen wir diese App so schön machen konnten.</string>
<string name="home_title_radio_station">Radio Stationen</string>
<string name="home_title_last_week">Letzte Woche</string>
<string name="home_title_top_songs">Deine Top Songs</string>
<string name="home_title_new_releases">Neue Releases</string>
<string name="home_title_best_of">Best Of</string>
<string name="home_subtitle_best_of">Top Tracks Deiner Lieblingskünstler</string>
<string name="home_title_newest_podcasts">Neueste Podcasts</string>
<string name="home_title_podcast_channels">Kanäle</string>
<string name="artist_adapter_radio_station_starting">Suche…</string>
<string name="podcast_bottom_sheet_go_to_channel">Zum Kanal gehen</string>
<string name="podcast_bottom_sheet_delete">Löschen</string>
<string name="podcast_bottom_sheet_remove">Entfernen</string>
<string name="podcast_bottom_sheet_download">Download</string>
<string name="podcast_bottom_sheet_add_to_queue">Zur Warteschlange hinzufügen</string>
<string name="podcast_bottom_sheet_play_next">Nächsten Titel spielen</string>
<string name="menu_sort_year">Jahr</string>
<string name="menu_sort_artist">Künstler</string>
<string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Zufall</string>
<string name="description_empty_title">Keine Beschreibung verfügbar</string>
<string name="menu_filter_download">Heruntergeladen</string>
<string name="menu_filter_all">Alle</string>
<string name="menu_group_by_track">Track</string>
<string name="menu_group_by_album">Album</string>
<string name="menu_group_by_artist">Künstler</string>
<string name="menu_group_by_genre">Genre</string>
<string name="menu_group_by_year">Jahr</string>
</resources>

View File

@@ -90,6 +90,35 @@
<item>320</item>
</string-array>
<string-array name="max_bitrate_download_list_titles">
<item>Original</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_download_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_titles">
<item>Direct play</item>
<item>Opus</item>
@@ -120,6 +149,21 @@
<item>flac</item>
</string-array>
<string-array name="audio_transcode_format_download_list_titles">
<item>Direct download</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_download_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="queue_syncing_countdown_titles">
<item>Ten seconds</item>
<item>Five seconds</item>
@@ -152,4 +196,17 @@
<item>track</item>
<item>album</item>
</string-array>
<string-array name="transcoded_download_option_list_titles">
<item>Do not transcode</item>
<item>Server settings</item>
<item>Wi-Fi Transcode format</item>
<item>Mobile Transcode format</item>
</string-array>
<string-array name="transcoded_download_option_list_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
</resources>

View File

@@ -49,9 +49,20 @@
<string name="connection_alert_dialog_positive_button">OK</string>
<string name="connection_alert_dialog_title">Wi-Fi not connected</string>
<string name="connection_alert_dialog_summary">Access to the Subsonic server on connections other than Wi-Fi has been restricted. To prevent this alert dialod from reappearing, disable the connection check in the app settings.</string>
<string name="delete_download_storage_dialog_negative_button">Cancel</string>
<string name="delete_download_storage_dialog_positive_button">Continue</string>
<string name="delete_download_storage_dialog_summary">Please be aware that continuing with this action will result in the permanent deletion of all saved items downloaded from all servers.</string>
<string name="delete_download_storage_dialog_title">Delete saved items</string>
<string name="download_info_empty_subtitle">Once you download a song, you\'ll find it here</string>
<string name="download_info_empty_title">No downloads yet!</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</string>
<string name="download_item_single_subtitle_formatter">%1$s items</string>
<string name="download_title_section">Downloads</string>
<string name="download_storage_internal_dialog_negative_button">Internal</string>
<string name="download_storage_external_dialog_positive_button">External</string>
<string name="download_storage_dialog_summary">Changing the destination of downloaded files from one storage to another will result in the immediate deletion of any previously downloaded files in the other storage.</string>
<string name="download_storage_dialog_sub_summary">For the changes to take effect, restart the app.</string>
<string name="download_storage_dialog_title">Select storage option</string>
<string name="empty_string" />
<string name="error_required">Required</string>
<string name="error_server_prefix">http or https prefix required</string>
@@ -86,8 +97,8 @@
<string name="home_title_starred_tracks_see_all_button">See all</string>
<string name="home_title_internet_radio_station">Internet radio stations</string>
<string name="home_title_podcast_channels_see_all_button">See all</string>
<string name="label_dot_separator"></string>
<string name="label_placeholder">--</string>
<string name="label_dot_separator" translatable="false"></string>
<string name="label_placeholder" translatable="false">--</string>
<string name="library_title_music_folder">Music folders</string>
<string name="library_title_album">Albums</string>
<string name="library_title_album_see_all_button">See all</string>
@@ -172,21 +183,32 @@
<string name="server_unreachable_dialog_summary">The requested server is unavailable. If you choose to continue this dialog will not appear for the next hour.</string>
<string name="settings_about_summary">Tempo is an open source and lightweight music client for Subsonic, designed and built natively for Android.</string>
<string name="settings_about_title">About</string>
<string name="settings_audio_transcode_download_priority_summary">If enabled, Tempo will not force download the track with the transcode settings below.</string>
<string name="settings_audio_transcode_download_priority_title">Prioritize server settings used for streaming in downloads</string>
<string name="settings_audio_transcode_download_summary">If enabled, Tempo will download transcoded tracks.</string>
<string name="settings_audio_transcode_download_title">Download transcoded tracks</string>
<string name="settings_audio_transcode_download_format">Transcode format</string>
<string name="settings_audio_transcode_priority_summary">If enabled, Tempo will not force stream the track with the transcode settings below.</string>
<string name="settings_audio_transcode_priority_title">Prioritize server transcode settings</string>
<string name="settings_audio_transcode_priority_toast">Priority on transcoding of track given to server</string>
<string name="settings_audio_transcode_format_download">Transcode format for downloads</string>
<string name="settings_audio_transcode_format_mobile">Transcode format in mobile</string>
<string name="settings_audio_transcode_format_wifi">Transcode format in Wi-Fi</string>
<string name="settings_covers_cache">Size of artwork cache</string>
<string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers.</string>
<string name="settings_data_saving_mode_title">Limit mobile data usage</string>
<string name="settings_delete_download_storage_title">Delete saved items</string>
<string name="settings_delete_download_storage_summary">Proceeding will result in the irreversible deletion of all saved items.</string>
<string name="settings_download_storage_title">Download storage</string>
<string name="settings_equalizer_summary">Adjust audio settings</string>
<string name="settings_equalizer_title">Equalizer</string>
<string name="settings_github_link">https://github.com/CappielloAntonio/tempo</string>
<string name="settings_github_summary">Follow the development</string>
<string name="settings_github_title">Github</string>
<string name="settings_image_size">Set image resolution</string>
<string name="settings_language">Language</string>
<string name="settings_logout_title">Log out</string>
<string name="settings_max_bitrate_download">Bitrate for downloads</string>
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
<string name="settings_media_cache">Size of media file cache</string>
@@ -208,6 +230,7 @@
<string name="settings_summary_replay_gain">Replay gain is a feature that allows you to adjust the volume level of audio tracks for a consistent listening experience. This setting is only effective if the track contains the necessary metadata.</string>
<string name="settings_summary_syncing">Returns the state of the play queue for this user. This includes the tracks in the play queue, the currently playing track, and the position within this track. The server must support this feature.</string>
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format\" is set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Sync starred tracks for offline use</string>
<string name="settings_theme">Theme</string>
@@ -216,8 +239,10 @@
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_syncing">Syncing</string>
<string name="settings_title_transcoding">Transcoding</string>
<string name="settings_title_transcoding_download">Transcoding Download</string>
<string name="settings_title_ui">UI</string>
<string name="settings_version_summary">3.1.0</string>
<string name="settings_transcoded_download">Transcoded download</string>
<string name="settings_version_summary" translatable="false">3.1.0</string>
<string name="settings_version_title">Version</string>
<string name="settings_wifi_only_title">Stream via Wi-Fi only alert</string>
<string name="settings_wifi_only_summary">Ask for user confirmation before streaming over mobile network.</string>
@@ -249,6 +274,8 @@
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
<string name="home_title_radio_station">Radio stations</string>
<string name="home_title_last_week">Last week</string>
<string name="home_title_top_songs">Your top songs</string>
<string name="home_title_new_releases">New releases</string>
<string name="home_title_best_of">Best of</string>
<string name="home_subtitle_best_of">Top songs of your favorite artists</string>
@@ -268,4 +295,9 @@
<string name="description_empty_title">No description available</string>
<string name="menu_filter_download">Downloaded</string>
<string name="menu_filter_all">All</string>
<string name="menu_group_by_track">Track</string>
<string name="menu_group_by_album">Album</string>
<string name="menu_group_by_artist">Artist</string>
<string name="menu_group_by_genre">Genre</string>
<string name="menu_group_by_year">Year</string>
</resources>

View File

@@ -16,6 +16,12 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_ui">
<ListPreference
app:defaultValue="default"
app:dialogTitle="@string/settings_language"
app:key="language"
app:title="@string/settings_language"/>
<ListPreference
app:defaultValue="default"
app:dialogTitle="@string/settings_theme"
@@ -78,12 +84,6 @@
app:title="@string/settings_image_size"
app:useSimpleSummaryProvider="true" />
<SwitchPreference
android:title="@string/settings_sync_starred_tracks_for_offline_use_title"
android:defaultValue="false"
android:summary="@string/settings_sync_starred_tracks_for_offline_use_summary"
android:key="sync_starred_tracks_for_offline_use" />
<SwitchPreference
android:title="@string/settings_wifi_only_title"
android:defaultValue="false"
@@ -95,6 +95,21 @@
android:defaultValue="false"
android:summary="@string/settings_data_saving_mode_summary"
android:key="data_saving_mode" />
<SwitchPreference
android:title="@string/settings_sync_starred_tracks_for_offline_use_title"
android:defaultValue="false"
android:summary="@string/settings_sync_starred_tracks_for_offline_use_summary"
android:key="sync_starred_tracks_for_offline_use" />
<Preference
android:key="download_storage"
app:title="@string/settings_download_storage_title" />
<Preference
android:key="delete_download_storage"
app:title="@string/settings_delete_download_storage_title"
app:summary="@string/settings_delete_download_storage_summary"/>
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_transcoding">
@@ -145,6 +160,42 @@
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_transcoding_download">
<Preference
app:selectable="false"
app:summary="@string/settings_summary_transcoding_download" />
<SwitchPreference
android:title="@string/settings_audio_transcode_download_title"
android:defaultValue="false"
android:summary="@string/settings_audio_transcode_download_summary"
android:key="audio_transcode_download" />
<SwitchPreference
android:title="@string/settings_audio_transcode_download_priority_title"
android:defaultValue="false"
android:summary="@string/settings_audio_transcode_download_priority_summary"
android:key="audio_transcode_download_priority" />
<ListPreference
app:defaultValue="raw"
app:dialogTitle="@string/settings_audio_transcode_format_download"
app:entries="@array/audio_transcode_format_download_list_titles"
app:entryValues="@array/audio_transcode_format_download_list_values"
app:key="audio_transcode_format_download"
app:title="@string/settings_audio_transcode_format_download"
app:useSimpleSummaryProvider="true" />
<ListPreference
app:defaultValue="0"
app:dialogTitle="@string/settings_max_bitrate_download"
app:entries="@array/max_bitrate_download_list_titles"
app:entryValues="@array/max_bitrate_download_list_values"
app:key="max_bitrate_download"
app:title="@string/settings_max_bitrate_download"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_replay_gain">
<Preference
app:selectable="false"

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en"/> <!-- English -->
<locale android:name="de-DE"/> <!-- German -->
</locale-config>

View File

@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath 'com.android.tools.build:gradle:8.1.0'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0'
}
}

View File

@@ -0,0 +1 @@
Initial release

View File

@@ -0,0 +1,14 @@
Tempo is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
Features
- Subsonic Integration: Tempo seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
- Sleek and Intuitive UI: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history.
- Browse and Search: Easily navigate through your music library using various browsing and searching options, including artists, albums, genres, playlists, decades and more.
- Streaming and Offline Mode: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers.
- Playlist Management: Create, edit, and manage playlists to curate your perfect music collection.
- Gapless Playback: Experience uninterrupted playback with gapless listening mode.
- Chromecast Support: Stream your music to Chromecast devices. The support is currently in a rudimentary state.
- Scrobbling Integration: Optionally integrate Tempo with Last.fm to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- Podcasts and Radio: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
- Transcoding Support: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

View File

@@ -0,0 +1 @@
An open-source and lightweight music client for Subsonic

View File

@@ -0,0 +1 @@
Tempo

View File

@@ -1,6 +1,6 @@
#Mon Jun 19 19:28:49 CEST 2023
#Wed Aug 16 22:52:26 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists