52 Commits
3.4.9 ... 3.5.4

Author SHA1 Message Date
antonio
8f6e775ca9 gradle: bump up code version 2023-08-30 17:53:44 +02:00
antonio
bad0fa6c23 fix: fixed a bug in the paginated song lists 2023-08-30 17:52:44 +02:00
antonio
d4caa6f209 build: dependencies update 2023-08-29 08:03:47 +02:00
antonio
723bdf9771 clean: code cleanup 2023-08-28 12:26:04 +02:00
antonio
49fbd85bb4 clean: code cleanup 2023-08-28 12:25:36 +02:00
CappielloAntonio
f040fbf0cf Merge pull request #50 from GallowsDove/main
feat: show albums instead of the artist in artist's top songs
2023-08-28 12:17:54 +02:00
antonio
efb2213ab7 fix: files without embedded cover art now display the image in the music player notification 2023-08-28 12:15:40 +02:00
GallowsDove
742ac6b17d feat: show albums instead of the artist in artist's top songs. 2023-08-28 11:28:59 +02:00
antonio
ae7761cb96 fix: null checking 2023-08-28 10:51:16 +02:00
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
59 changed files with 1280 additions and 198 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 15
versionName '3.4.9'
versionCode 19
versionName '3.5.4'
}
notquitemy {
@@ -72,27 +72,28 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.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.navigation:navigation-fragment-ktx:2.7.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.1'
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,54 +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:foregroundServiceType="dataSync"
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

@@ -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,8 @@
package com.cappielloantonio.tempo.interfaces;
import androidx.annotation.Keep;
@Keep
public interface PlaylistCallback {
default void onDismiss() {}
}

View File

@@ -105,7 +105,9 @@ public class AlbumRepository {
List<Child> tracks = new ArrayList<>();
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) {
tracks.addAll(response.body().getSubsonicResponse().getAlbum().getSongs());
if (response.body().getSubsonicResponse().getAlbum().getSongs() != null) {
tracks.addAll(response.body().getSubsonicResponse().getAlbum().getSongs());
}
}
albumTracks.setValue(tracks);

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

@@ -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

@@ -103,6 +103,7 @@ public class DownloaderManager {
public void removeAll() {
DownloadService.sendRemoveAllDownloads(context, DownloaderService.class, false);
deleteAllDatabase();
DownloadUtil.eraseDownloadFolder(context);
}
private void loadDownloads() {

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

@@ -261,7 +261,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
switch (view) {
case Constants.DOWNLOAD_TYPE_TRACK:
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(grouped));
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
click.onMediaClick(bundle);
break;

View File

@@ -25,13 +25,15 @@ import java.util.List;
@UnstableApi
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> {
private final ClickCallback click;
private final boolean isCoverVisible;
private final boolean showCoverArt;
private final boolean showAlbum;
private List<Child> songs;
public SongHorizontalAdapter(ClickCallback click, boolean isCoverVisible) {
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum) {
this.click = click;
this.isCoverVisible = isCoverVisible;
this.showCoverArt = showCoverArt;
this.showAlbum = showAlbum;
this.songs = Collections.emptyList();
}
@@ -47,7 +49,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
Child song = songs.get(position);
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.searchResultSongSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(this.showAlbum ? song.getAlbum() : song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration() != null ? song.getDuration() : 0, false)));
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
@@ -56,16 +58,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
holder.item.searchResultDowanloadIndicatorImageView.setVisibility(View.GONE);
}
if (isCoverVisible) CustomGlideRequest.Builder
if (showCoverArt) CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId())
.build()
.into(holder.item.songCoverImageView);
if (isCoverVisible) holder.item.trackNumberTextView.setVisibility(View.INVISIBLE);
if (showCoverArt) holder.item.trackNumberTextView.setVisibility(View.INVISIBLE);
if (!isCoverVisible) holder.item.songCoverImageView.setVisibility(View.INVISIBLE);
if (!showCoverArt) holder.item.songCoverImageView.setVisibility(View.INVISIBLE);
if (!isCoverVisible && (position > 0 && songs.get(position - 1) != null && songs.get(position - 1).getDiscNumber() != null && songs.get(position).getDiscNumber() != null && songs.get(position - 1).getDiscNumber() < songs.get(position).getDiscNumber())) {
if (!showCoverArt && (position > 0 && songs.get(position - 1) != null && songs.get(position - 1).getDiscNumber() != null && songs.get(position).getDiscNumber() != null && songs.get(position - 1).getDiscNumber() < songs.get(position).getDiscNumber())) {
holder.item.differentDiskDivider.setVisibility(View.VISIBLE);
}
}
@@ -114,7 +116,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

@@ -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

@@ -181,7 +181,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, false);
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));

View File

@@ -164,7 +164,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private void initTopSongsView() {
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
songHorizontalAdapter = new SongHorizontalAdapter(this, true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true);
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {

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;
@@ -393,7 +392,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private void initStarredTracksView() {
bind.starredTracksRecyclerView.setHasFixedSize(true);
starredSongAdapter = new SongHorizontalAdapter(this, true);
starredSongAdapter = new SongHorizontalAdapter(this, true, false);
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
@@ -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

@@ -200,7 +200,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));

View File

@@ -112,7 +112,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false);
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
}

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;
@@ -28,8 +30,12 @@ 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";
@@ -75,63 +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;
});
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;
});
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
dialog.show(activity.getSupportFragmentManager(), null);
return true;
});
actionLogout();
actionScan();
actionSyncStarredTracks();
actionChangeDownloadStorage();
actionDeleteDownloadStorage();
}
@Override
@@ -187,6 +146,94 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
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

@@ -166,7 +166,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songListRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false);
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
isLoading = false;

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
@@ -153,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,7 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
@@ -33,6 +34,7 @@ public class MappingUtil {
public static MediaItem mapMediaItem(Child media) {
Uri uri = getUri(media);
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(media.getCoverArtId(), Preferences.getImageSize()));
Bundle bundle = new Bundle();
bundle.putString("id", media.getId());
@@ -78,6 +80,7 @@ public class MappingUtil {
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.setArtworkUri(artworkUri)
.setExtras(bundle)
.build()
)
@@ -157,6 +160,7 @@ public class MappingUtil {
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
Uri uri = getUri(podcastEpisode);
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize()));
Bundle bundle = new Bundle();
bundle.putString("id", podcastEpisode.getId());
@@ -202,6 +206,7 @@ public class MappingUtil {
.setReleaseYear(podcastEpisode.getYear() != null ? podcastEpisode.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(podcastEpisode.getAlbum()))
.setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist()))
.setArtworkUri(artworkUri)
.setExtras(bundle)
.build()
)
@@ -223,9 +228,9 @@ public class MappingUtil {
}
private static Uri getUri(PodcastEpisode podcastEpisode) {
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getId())
? 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) {

View File

@@ -32,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)
@@ -68,7 +68,7 @@ public class MusicUtil {
uri.append("download");
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)
@@ -99,7 +99,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)
@@ -271,8 +271,12 @@ public class MusicUtil {
return toLimit;
}
public static int getPlayableMediaPosition(int initialPosition) {
return Math.min(initialPosition, Constants.PRE_PLAYABLE_MEDIA);
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

@@ -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

@@ -1,5 +1,8 @@
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;
@@ -14,4 +17,48 @@ public class Util {
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

@@ -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

@@ -71,11 +71,17 @@ public class SongListPageViewModel extends AndroidViewModel {
public void getSongsByPage(LifecycleOwner owner) {
switch (title) {
case Constants.MEDIA_BY_GENRE:
int page = (songList.getValue() != null ? songList.getValue().size() : 0) / 100;
int songCount = songList.getValue() != null ? songList.getValue().size() : 0;
if (songCount > 0 && songCount % 100 != 0) return;
int page = songCount / 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

@@ -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"
@@ -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

@@ -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"

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

@@ -181,9 +181,15 @@
<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>
@@ -198,7 +204,9 @@
<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>
@@ -220,6 +228,7 @@
<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>
@@ -228,6 +237,7 @@
<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>

View File

@@ -206,6 +206,7 @@
<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>

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"

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

@@ -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