64 Commits
3.4.7 ... 3.4.9

Author SHA1 Message Date
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
antonio
aa68c2c3d8 ci: upload database schemas 2023-08-02 11:30:26 +02:00
antonio
9c0ebca66f feat: created and invoked methods for checking starred/unstarred items from favorites when offline 2023-08-02 10:53:21 +02:00
antonio
888f177597 refactor: update repository references for adding/removing favorites 2023-08-02 10:51:13 +02:00
antonio
c69fbcfa68 feat: centralized star/unstar handling and added offline support 2023-08-02 10:49:21 +02:00
antonio
f044db142c feat: created class for storing information on tracks, albums, and artists starred or unstarred while offline or when the server was unreachable 2023-08-02 10:47:37 +02:00
antonio
9af7bc3ac8 feat: started work on centralizing network state monitoring controls 2023-08-02 10:44:53 +02:00
antonio
cd87fcde26 refactor: removed common starring methods from repository classes 2023-08-02 10:43:29 +02:00
antonio
9e1a6c804f fix: null checking 2023-08-01 21:37:47 +02:00
antonio
4c15f6eb01 fix: set new default color for artist folder images 2023-08-01 10:35:46 +02:00
antonio
4ad2722e81 feat: added fast scrollbar to folder navigation screen. 2023-08-01 10:34:39 +02:00
antonio
b267b904cc feat: added the ability to request download for an unplayed podcast episode 2023-07-31 11:39:35 +02:00
antonio
1ebe9ff8ba fix: resized splash icon 2023-07-30 18:13:32 +02:00
antonio
623a4956a5 fix: limit the number of playlists' tracks queued 2023-07-30 16:58:26 +02:00
antonio
6572b846c8 fix: fixed boolean evaluation in the code 2023-07-30 16:57:18 +02:00
antonio
fe3ba9fb89 fix: null checking 2023-07-30 16:12:32 +02:00
antonio
c4b9db303a feat: improved stability and added cover image for playlist section 2023-07-30 15:51:03 +02:00
antonio
aed52fdbf8 clean: code cleanup 2023-07-30 12:45:51 +02:00
antonio
f9573b3eab feat: updated podcast channel UI 2023-07-30 12:23:49 +02:00
antonio
7a8880ee68 clean: code cleanup 2023-07-30 12:22:38 +02:00
antonio
68aae32d06 feat: add filter to display podcast episodes not yet downloaded 2023-07-30 12:22:02 +02:00
antonio
4b07f37378 fix: fixed the issue with opening podcast channels from the catalog view. 2023-07-29 23:46:47 +02:00
antonio
4d573c6b9d fix: resolved issue with scrobbling the last track 2023-07-29 23:45:34 +02:00
antonio
10dcb2380c gradle: dependency update 2023-07-29 17:21:05 +02:00
100 changed files with 4570 additions and 622 deletions

View File

@@ -28,8 +28,8 @@ android {
tempo {
dimension "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 14
versionName '3.4.7'
versionCode 15
versionName '3.4.9'
}
notquitemy {
@@ -71,10 +71,10 @@ 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.recyclerview:recyclerview:1.3.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'

View File

@@ -0,0 +1,746 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "1f4e50f90f58fb9cb53c89747d142fd9",
"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, `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": "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": []
}
],
"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, '1f4e50f90f58fb9cb53c89747d142fd9')"
]
}
}

View File

@@ -0,0 +1,790 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "ff99e331b4c34a82c560588c4dd5735f",
"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, `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": "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, 'ff99e331b4c34a82c560588c4dd5735f')"
]
}
}

View File

@@ -6,6 +6,9 @@
<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.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"
@@ -40,6 +43,7 @@
</intent-filter>
</service>
<service android:name=".service.DownloaderService"
android:foregroundServiceType="dataSync"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>

View File

@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.database;
import androidx.room.AutoMigration;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
@@ -9,19 +10,21 @@ import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.converter.DateConverters;
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
import com.cappielloantonio.tempo.database.dao.DownloadDao;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.database.dao.QueueDao;
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
import com.cappielloantonio.tempo.database.dao.ServerDao;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.model.Server;
@Database(
version = 1,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class}
// autoMigrations = {@AutoMigration(from = 61, to = 62)}
version = 3,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class},
autoMigrations = {@AutoMigration(from = 2, to = 3)}
)
@TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase {
@@ -47,4 +50,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract DownloadDao downloadDao();
public abstract ChronologyDao chronologyDao();
public abstract FavoriteDao favoriteDao();
}

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

@@ -0,0 +1,26 @@
package com.cappielloantonio.tempo.database.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.cappielloantonio.tempo.model.Favorite;
import java.util.List;
@Dao
public interface FavoriteDao {
@Query("SELECT * FROM favorite")
List<Favorite> getAll();
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(Favorite favorite);
@Delete
void delete(Favorite favorite);
@Query("DELETE FROM favorite")
void deleteAll();
}

View File

@@ -0,0 +1,197 @@
package com.cappielloantonio.tempo.helper.recyclerview;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class FastScrollbar extends LinearLayout {
private static final int BUBBLE_ANIMATION_DURATION = 100;
private static final int TRACK_SNAP_RANGE = 5;
private TextView bubble;
private View handle;
private RecyclerView recyclerView;
private int height;
private boolean isInitialized = false;
private ObjectAnimator currentAnimator = null;
private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
updateBubbleAndHandlePosition();
}
};
public interface BubbleTextGetter {
String getTextToShowInBubble(int pos);
}
public FastScrollbar(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public FastScrollbar(final Context context) {
super(context);
init(context);
}
public FastScrollbar(final Context context, final AttributeSet attrs) {
super(context, attrs);
init(context);
}
protected void init(Context context) {
if (isInitialized) return;
isInitialized = true;
setOrientation(HORIZONTAL);
setClipChildren(false);
}
public void setViewsToUse(@LayoutRes int layoutResId, @IdRes int bubbleResId, @IdRes int handleResId) {
final LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(layoutResId, this, true);
bubble = findViewById(bubbleResId);
if (bubble != null) bubble.setVisibility(INVISIBLE);
handle = findViewById(handleResId);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
height = h;
updateBubbleAndHandlePosition();
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (event.getX() < handle.getX() - ViewCompat.getPaddingStart(handle)) return false;
if (currentAnimator != null) currentAnimator.cancel();
if (bubble != null && bubble.getVisibility() == INVISIBLE) showBubble();
handle.setSelected(true);
case MotionEvent.ACTION_MOVE:
final float y = event.getY();
setBubbleAndHandlePosition(y);
setRecyclerViewPosition(y);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
handle.setSelected(false);
hideBubble();
return true;
}
return super.onTouchEvent(event);
}
public void setRecyclerView(final RecyclerView recyclerView) {
if (this.recyclerView != recyclerView) {
if (this.recyclerView != null)
this.recyclerView.removeOnScrollListener(onScrollListener);
this.recyclerView = recyclerView;
if (this.recyclerView == null) return;
recyclerView.addOnScrollListener(onScrollListener);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (recyclerView != null) {
recyclerView.removeOnScrollListener(onScrollListener);
recyclerView = null;
}
}
private void setRecyclerViewPosition(float y) {
if (recyclerView != null) {
final int itemCount = recyclerView.getAdapter().getItemCount();
float proportion;
if (handle.getY() == 0) proportion = 0f;
else if (handle.getY() + handle.getHeight() >= height - TRACK_SNAP_RANGE)
proportion = 1f;
else proportion = y / (float) height;
final int targetPos = getValueInRange(0, itemCount - 1, (int) (proportion * (float) itemCount));
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0);
final String bubbleText = ((BubbleTextGetter) recyclerView.getAdapter()).getTextToShowInBubble(targetPos);
if (bubble != null) {
bubble.setText(bubbleText);
if (TextUtils.isEmpty(bubbleText)) {
hideBubble();
} else if (bubble.getVisibility() == View.INVISIBLE) {
showBubble();
}
}
}
}
private int getValueInRange(int min, int max, int value) {
int minimum = Math.max(min, value);
return Math.min(minimum, max);
}
private void updateBubbleAndHandlePosition() {
if (bubble == null || handle.isSelected()) return;
final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
float proportion = (float) verticalScrollOffset / ((float) verticalScrollRange - height);
setBubbleAndHandlePosition(height * proportion);
}
private void setBubbleAndHandlePosition(float y) {
final int handleHeight = handle.getHeight();
handle.setY(getValueInRange(0, height - handleHeight, (int) (y - handleHeight / 2)));
if (bubble != null) {
int bubbleHeight = bubble.getHeight();
bubble.setY(getValueInRange(0, height - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight)));
}
}
private void showBubble() {
if (bubble == null) return;
bubble.setVisibility(VISIBLE);
if (currentAnimator != null) currentAnimator.cancel();
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION);
currentAnimator.start();
}
private void hideBubble() {
if (bubble == null) return;
if (currentAnimator != null) currentAnimator.cancel();
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION);
currentAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
bubble.setVisibility(INVISIBLE);
currentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
bubble.setVisibility(INVISIBLE);
currentAnimator = null;
}
});
currentAnimator.start();
}
}

View File

@@ -20,6 +20,7 @@ public interface ClickCallback {
default void onServerClick(Bundle bundle) {}
default void onServerLongClick(Bundle bundle) {}
default void onPodcastEpisodeClick(Bundle bundle) {}
default void onPodcastEpisodeAltClick(Bundle bundle) {}
default void onPodcastEpisodeLongClick(Bundle bundle) {}
default void onPodcastChannelClick(Bundle bundle) {}
default void onPodcastChannelLongClick(Bundle bundle) {}

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

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

@@ -0,0 +1,32 @@
package com.cappielloantonio.tempo.model
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.annotation.Nullable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
@Entity(tableName = "favorite")
data class Favorite(
@PrimaryKey
@ColumnInfo(name = "timestamp")
var timestamp: Long,
@ColumnInfo(name = "songId")
val songId: String?,
@ColumnInfo(name = "albumId")
val albumId: String?,
@ColumnInfo(name = "artistId")
val artistId: String?,
@ColumnInfo(name = "toStar")
val toStar: Boolean,
) : Parcelable {
override fun toString(): String = (songId ?: "null") + (albumId ?: "null") + (artistId ?: "null")
}

View File

@@ -21,8 +21,6 @@ import retrofit2.Callback;
import retrofit2.Response;
public class AlbumRepository {
private static final String TAG = "AlbumRepository";
public MutableLiveData<List<AlbumID3>> getAlbums(String type, int size, Integer fromYear, Integer toYear) {
MutableLiveData<List<AlbumID3>> listLiveAlbums = new MutableLiveData<>(new ArrayList<>());
@@ -78,40 +76,6 @@ public class AlbumRepository {
return starredAlbums;
}
public void star(String id) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.star(null, id, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void unstar(String id) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.unstar(null, id, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()

View File

@@ -135,9 +135,6 @@ public class ArtistRepository {
return artist;
}
/*
* Metodo che mi restituisce le informazioni complete dell'artista (bio, immagini prese da last.fm, artisti simili...)
*/
public MutableLiveData<ArtistInfo2> getArtistFullInfo(String id) {
MutableLiveData<ArtistInfo2> artistFullInfo = new MutableLiveData<>(null);
@@ -161,40 +158,6 @@ public class ArtistRepository {
return artistFullInfo;
}
public void star(String id) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.star(null, null, id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void unstar(String id) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.unstar(null, null, id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()

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

@@ -0,0 +1,140 @@
package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class FavoriteRepository {
private final FavoriteDao favoriteDao = AppDatabase.getInstance().favoriteDao();
public void star(String id, String albumId, String artistId, StarCallback starCallback) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.star(id, albumId, artistId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful()) {
starCallback.onSuccess();
} else {
starCallback.onError();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
starCallback.onError();
}
});
}
public void unstar(String id, String albumId, String artistId, StarCallback starCallback) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.unstar(id, albumId, artistId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful()) {
starCallback.onSuccess();
} else {
starCallback.onError();
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
starCallback.onError();
}
});
}
public List<Favorite> getFavorites() {
List<Favorite> favorites = new ArrayList<>();
GetAllThreadSafe getAllThreadSafe = new GetAllThreadSafe(favoriteDao);
Thread thread = new Thread(getAllThreadSafe);
thread.start();
try {
thread.join();
favorites = getAllThreadSafe.getFavorites();
} catch (InterruptedException e) {
e.printStackTrace();
}
return favorites;
}
private static class GetAllThreadSafe implements Runnable {
private final FavoriteDao favoriteDao;
private List<Favorite> favorites = new ArrayList<>();
public GetAllThreadSafe(FavoriteDao favoriteDao) {
this.favoriteDao = favoriteDao;
}
@Override
public void run() {
favorites = favoriteDao.getAll();
}
public List<Favorite> getFavorites() {
return favorites;
}
}
public void starLater(String id, String albumId, String artistId, boolean toStar) {
InsertThreadSafe insert = new InsertThreadSafe(favoriteDao, new Favorite(System.currentTimeMillis(), id, albumId, artistId, toStar));
Thread thread = new Thread(insert);
thread.start();
}
private static class InsertThreadSafe implements Runnable {
private final FavoriteDao favoriteDao;
private final Favorite favorite;
public InsertThreadSafe(FavoriteDao favoriteDao, Favorite favorite) {
this.favoriteDao = favoriteDao;
this.favorite = favorite;
}
@Override
public void run() {
favoriteDao.insert(favorite);
}
}
public void delete(Favorite favorite) {
DeleteThreadSafe delete = new DeleteThreadSafe(favoriteDao, favorite);
Thread thread = new Thread(delete);
thread.start();
}
private static class DeleteThreadSafe implements Runnable {
private final FavoriteDao favoriteDao;
private final Favorite favorite;
public DeleteThreadSafe(FavoriteDao favoriteDao, Favorite favorite) {
this.favoriteDao = favoriteDao;
this.favorite = favorite;
}
@Override
public void run() {
favoriteDao.delete(favorite);
}
}
}

View File

@@ -133,4 +133,21 @@ public class PodcastRepository {
}
});
}
public void downloadPodcastEpisode(String episodeId) {
App.getSubsonicClientInstance(false)
.getPodcastClient()
.downloadPodcastEpisode(episodeId)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
}

View File

@@ -6,6 +6,8 @@ import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Child;
@@ -119,40 +121,6 @@ public class SongRepository {
});
}
public void star(String id) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.star(id, null, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void unstar(String id) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.unstar(id, null, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
}
public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
@@ -173,8 +141,6 @@ public class SongRepository {
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
Log.d(TAG, "onScrolled PAGE: " + page);
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getSongsByGenre(id, 100, 100 * page)

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,11 @@ public class DownloaderManager {
}
}
public void removeAll() {
DownloadService.sendRemoveAllDownloads(context, DownloaderService.class, false);
deleteAllDatabase();
}
private void loadDownloads() {
try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
while (loadedDownloads.moveToNext()) {
@@ -108,6 +116,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 +133,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

@@ -48,4 +48,9 @@ public class PodcastClient {
Log.d(TAG, "deletePodcastEpisode()");
return podcastService.deletePodcastEpisode(subsonic.getParams(), episodeId);
}
public Call<ApiResponse> downloadPodcastEpisode(String episodeId) {
Log.d(TAG, "downloadPodcastEpisode()");
return podcastService.downloadPodcastEpisode(subsonic.getParams(), episodeId);
}
}

View File

@@ -27,4 +27,7 @@ public interface PodcastService {
@GET("deletePodcastEpisode")
Call<ApiResponse> deletePodcastEpisode(@QueryMap Map<String, String> params, @Query("id") String id);
@GET("downloadPodcastEpisode")
Call<ApiResponse> downloadPodcastEpisode(@QueryMap Map<String, String> params, @Query("id") String id);
}

View File

@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import java.util.*
@@ -29,11 +28,9 @@ class PodcastEpisode : Parcelable {
var transcodedContentType: String? = null
var transcodedSuffix: String? = null
var duration: Int? = null
@ColumnInfo("bitrate")
@SerializedName("bitRate")
var bitrate: Int? = null
var path: String? = null
@ColumnInfo(name = "is_video")
@SerializedName("isVideo")
var isVideo: Boolean = false
var userRating: Int? = null

View File

@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.activity;
import android.content.Context;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
@@ -338,7 +339,9 @@ public class MainActivity extends BaseActivity {
private void checkConnectionType() {
if (Preferences.isWifiOnly()) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager.getActiveNetworkInfo().getType() != ConnectivityManager.TYPE_WIFI) {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.getType() != ConnectivityManager.TYPE_WIFI) {
ConnectionAlertDialog dialog = new ConnectionAlertDialog();
dialog.show(getSupportFragmentManager(), null);
}

View File

@@ -53,7 +53,7 @@ public class BaseActivity extends AppCompatActivity {
}
private void checkBatteryOptimization() {
if (detectBatteryOptimization() && Boolean.TRUE.equals(Preferences.askForOptimization())) {
if (detectBatteryOptimization() && Preferences.askForOptimization()) {
showBatteryOptimizationDialog();
}
}

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<>(songs));
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

@@ -9,16 +9,17 @@ import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.databinding.ItemLibraryMusicIndexBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.helper.recyclerview.FastScrollbar;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.Artist;
import com.cappielloantonio.tempo.util.Constants;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@UnstableApi
public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.ViewHolder> {
public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.ViewHolder> implements FastScrollbar.BubbleTextGetter {
private final ClickCallback click;
private List<Artist> artists;
@@ -41,10 +42,10 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
holder.item.musicIndexTitleTextView.setText(artist.getName());
CustomGlideRequest.Builder
/* CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getName())
.build()
.into(holder.item.musicIndexCoverImageView);
.into(holder.item.musicIndexCoverImageView); */
}
@Override
@@ -57,6 +58,11 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
notifyDataSetChanged();
}
@Override
public String getTextToShowInBubble(int pos) {
return Character.toString(Objects.requireNonNull(artists.get(pos).getName().toUpperCase()).charAt(0));
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryMusicIndexBinding item;

View File

@@ -128,14 +128,14 @@ public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter<Podcast
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition()));
click.onAlbumClick(bundle);
click.onPodcastChannelClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition()));
click.onAlbumLongClick(bundle);
click.onPodcastChannelLongClick(bundle);
return true;
}

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;
@@ -18,11 +19,14 @@ import com.cappielloantonio.tempo.util.MusicUtil;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAdapter.ViewHolder> {
private final ClickCallback click;
private List<PodcastEpisode> podcastEpisodes;
private List<PodcastEpisode> podcastEpisodesFull;
public PodcastEpisodeAdapter(ClickCallback click) {
this.click = click;
@@ -50,6 +54,10 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
.from(holder.itemView.getContext(), podcastEpisode.getCoverArtId())
.build()
.into(holder.item.podcastCoverImageView);
holder.item.podcastPlayButton.setEnabled(podcastEpisode.getStatus().equals("completed"));
holder.item.podcastMoreButton.setVisibility(podcastEpisode.getStatus().equals("completed") ? View.VISIBLE : View.GONE);
holder.item.podcastDownloadRequestButton.setVisibility(podcastEpisode.getStatus().equals("completed") ? View.GONE : View.VISIBLE);
}
@Override
@@ -58,7 +66,8 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
}
public void setItems(List<PodcastEpisode> podcastEpisodes) {
this.podcastEpisodes = podcastEpisodes;
this.podcastEpisodesFull = podcastEpisodes;
this.podcastEpisodes = podcastEpisodesFull.stream().filter(podcastEpisode -> Objects.equals(podcastEpisode.getStatus(), "completed")).collect(Collectors.toList());
notifyDataSetChanged();
}
@@ -85,22 +94,57 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
item.podcastPlayButton.setOnClickListener(v -> onClick());
item.podcastMoreButton.setOnClickListener(v -> openMore());
item.podcastDownloadRequestButton.setOnClickListener(v -> requestDownload());
}
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition());
click.onPodcastEpisodeClick(bundle);
if (podcastEpisode.getStatus().equals("completed")) {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
click.onPodcastEpisodeClick(bundle);
}
}
private boolean openMore() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition());
click.onPodcastEpisodeLongClick(bundle);
if (podcastEpisode.getStatus().equals("completed")) {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
return true;
click.onPodcastEpisodeLongClick(bundle);
return true;
}
return false;
}
public void requestDownload() {
PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition());
if (!podcastEpisode.getStatus().equals("completed")) {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
click.onPodcastEpisodeAltClick(bundle);
}
}
}
public void sort(String order) {
switch (order) {
case Constants.PODCAST_FILTER_BY_DOWNLOAD:
podcastEpisodes = podcastEpisodesFull.stream().filter(podcastEpisode -> Objects.equals(podcastEpisode.getStatus(), "completed")).collect(Collectors.toList());
break;
case Constants.PODCAST_FILTER_BY_ALL:
podcastEpisodes = podcastEpisodesFull;
break;
}
notifyDataSetChanged();
}
}

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);
}

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

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

@@ -96,6 +96,9 @@ public class IndexFragment extends Fragment implements ClickCallback {
musicIndexAdapter.setItems(IndexUtil.getArtist(indexes));
}
});
bind.fastScrollbar.setRecyclerView(bind.indexRecyclerView);
bind.fastScrollbar.setViewsToUse(R.layout.layout_fast_scrollbar, R.id.fastscroller_bubble, R.id.fastscroller_handle);
}
@Override

View File

@@ -19,6 +19,7 @@ import androidx.media3.session.SessionToken;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentPlaylistPageBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
@@ -146,13 +147,13 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
if (bind != null) {
bind.playlistPagePlayButton.setOnClickListener(v -> {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(100, songs.size())), 0);
activity.setBottomSheetInPeek(true);
});
bind.playlistPageShuffleButton.setOnClickListener(v -> {
Collections.shuffle(songs);
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(100, songs.size())), 0);
activity.setBottomSheetInPeek(true);
});
}
@@ -160,10 +161,39 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
}
private void initBackCover() {
CustomGlideRequest.Builder
.from(requireContext(), playlistPageViewModel.getPlaylist().getCoverArtId())
.build()
.into(bind.playlistCoverImageView);
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
if (bind != null && songs != null && !songs.isEmpty()) {
Collections.shuffle(songs);
// Pic top-left
CustomGlideRequest.Builder
.from(requireContext(), songs.size() > 0 ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId())
.build()
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
.into(bind.playlistCoverImageViewTopLeft);
// Pic top-right
CustomGlideRequest.Builder
.from(requireContext(), songs.size() > 1 ? songs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId())
.build()
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
.into(bind.playlistCoverImageViewTopRight);
// Pic bottom-left
CustomGlideRequest.Builder
.from(requireContext(), songs.size() > 2 ? songs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId())
.build()
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
.into(bind.playlistCoverImageViewBottomLeft);
// Pic bottom-right
CustomGlideRequest.Builder
.from(requireContext(), songs.size() > 3 ? songs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId())
.build()
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
.into(bind.playlistCoverImageViewBottomRight);
}
});
}
private void initSongsView() {

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.fragment.app.Fragment;
@@ -17,7 +18,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentPodcastChannelPageBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
@@ -28,11 +28,10 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.PodcastChannelPageViewModel;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@UnstableApi
public class PodcastChannelPageFragment extends Fragment implements ClickCallback {
@@ -84,28 +83,26 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
}
private void initAppBar() {
activity.setSupportActionBar(bind.animToolbar);
if (activity.getSupportActionBar() != null)
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
activity.setSupportActionBar(bind.toolbar);
bind.collapsingToolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle()));
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.collapsingToolbar.setExpandedTitleColor(getResources().getColor(R.color.white, null));
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.toolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle()));
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.toolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle()));
}
private void initPodcastChannelInfo() {
String normalizePodcastChannelDescription = MusicUtil.forceReadableString(podcastChannelPageViewModel.getPodcastChannel().getDescription());
if (bind != null)
if (bind != null) {
bind.podcastChannelDescriptionTextView.setVisibility(!normalizePodcastChannelDescription.trim().isEmpty() ? View.VISIBLE : View.GONE);
if (getContext() != null && bind != null) CustomGlideRequest.Builder
.from(requireContext(), podcastChannelPageViewModel.getPodcastChannel().getCoverArtId())
.build()
.into(bind.podcastChannelBackdropImageView);
if (bind != null)
bind.podcastChannelDescriptionTextView.setText(normalizePodcastChannelDescription);
bind.podcastEpisodesFilterImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.filter_podcast_episode_popup_menu));
}
}
private void initPodcastChannelEpisodesView() {
@@ -116,22 +113,21 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
bind.podcastEpisodesRecyclerView.setAdapter(podcastEpisodeAdapter);
podcastChannelPageViewModel.getPodcastChannelEpisodes().observe(getViewLifecycleOwner(), channels -> {
if (channels == null) {
if (bind != null)
bind.podcastChannelPageEpisodesPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.podcastChannelPageEpisodesSector.setVisibility(View.GONE);
if (bind != null) {
bind.podcastEpisodesRecyclerView.setVisibility(View.GONE);
}
} else {
if (bind != null)
bind.podcastChannelPageEpisodesPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) {
bind.podcastEpisodesRecyclerView.setVisibility(View.VISIBLE);
}
if (!channels.isEmpty() && channels.get(0) != null && channels.get(0).getEpisodes() != null) {
List<PodcastEpisode> availableEpisode = channels.get(0).getEpisodes().stream().filter(podcastEpisode -> Objects.equals(podcastEpisode.getStatus(), "completed")).collect(Collectors.toList());
List<PodcastEpisode> availableEpisode = channels.get(0).getEpisodes();
if (bind != null) {
if (bind != null && availableEpisode != null) {
bind.podcastEpisodesRecyclerView.setVisibility(availableEpisode.isEmpty() ? View.GONE : View.VISIBLE);
bind.podcastEpisodesAvailabilityTextView.setVisibility(availableEpisode.isEmpty() ? View.VISIBLE : View.GONE);
podcastEpisodeAdapter.setItems(availableEpisode);
}
podcastEpisodeAdapter.setItems(availableEpisode);
}
}
});
@@ -145,6 +141,25 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
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_podcast_filter_download) {
podcastEpisodeAdapter.sort(Constants.PODCAST_FILTER_BY_DOWNLOAD);
return true;
} else if (menuItem.getItemId() == R.id.menu_podcast_filter_all) {
podcastEpisodeAdapter.sort(Constants.PODCAST_FILTER_BY_ALL);
return true;
}
return false;
});
popup.show();
}
@Override
public void onPodcastEpisodeClick(Bundle bundle) {
MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT));
@@ -155,4 +170,14 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
public void onPodcastEpisodeLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.podcastEpisodeBottomSheetDialog, bundle);
}
@Override
public void onPodcastEpisodeAltClick(Bundle bundle) {
PodcastEpisode episode = bundle.getParcelable(Constants.PODCAST_OBJECT);
podcastChannelPageViewModel.requestPodcastEpisodeDownload(episode);
Snackbar.make(requireView(), R.string.podcast_episode_download_request_snackbar, Snackbar.LENGTH_SHORT)
.setAnchorView(activity.bind.bottomNavigation)
.show();
}
}

View File

@@ -21,8 +21,11 @@ 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.viewmodel.SettingViewModel;
@@ -73,6 +76,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
checkEqualizer();
checkStorage();
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
findPreference("logout").setOnPreferenceClickListener(preference -> {
@@ -105,6 +110,28 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
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;
});
}
@Override
@@ -144,6 +171,22 @@ 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 getScanStatus() {
settingViewModel.getScanStatus(new ScanCallback() {
@Override

View File

@@ -91,10 +91,9 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
artistAlbum.setText(MusicUtil.getReadableString(albumBottomSheetViewModel.getAlbum().getArtist()));
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(Boolean.TRUE.equals(albumBottomSheetViewModel.getAlbum().getStarred()));
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
albumBottomSheetViewModel.setFavorite();
dismissBottomSheet();
});
TextView playRadio = view.findViewById(R.id.play_radio_text_view);

View File

@@ -79,10 +79,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
nameArtist.setSelected(true);
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(Boolean.TRUE.equals(artistBottomSheetViewModel.getArtist().getStarred()));
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
artistBottomSheetViewModel.setFavorite();
dismissBottomSheet();
});
TextView playRadio = view.findViewById(R.id.play_radio_text_view);

View File

@@ -87,10 +87,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
artistSong.setText(MusicUtil.getReadableString(songBottomSheetViewModel.getSong().getArtist()));
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(Boolean.TRUE.equals(songBottomSheetViewModel.getSong().getStarred()));
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

@@ -44,6 +44,9 @@ object Constants {
const val PLAYLIST_ORDER_BY_NAME = "ORDER_BY_NAME"
const val PLAYLIST_ORDER_BY_RANDOM = "ORDER_BY_RANDOM"
const val PODCAST_FILTER_BY_DOWNLOAD = "PODCAST_FILTER_BY_DOWNLOAD"
const val PODCAST_FILTER_BY_ALL = "PODCAST_FILTER_BY_ALL"
const val MEDIA_TYPE_MUSIC = "music"
const val MEDIA_TYPE_PODCAST = "podcast"
const val MEDIA_TYPE_AUDIOBOOK = "audiobook"
@@ -73,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

@@ -127,9 +127,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);
}
}
}

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())
? getDownloadUri(podcastEpisode.getId())
: MusicUtil.getStreamUri(podcastEpisode.getId());
}
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;
@@ -54,12 +57,46 @@ 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(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"));
@@ -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;
@@ -208,7 +272,7 @@ public class MusicUtil {
}
public static int getPlayableMediaPosition(int initialPosition) {
return initialPosition > Constants.PLAYABLE_MEDIA_LIMIT ? Constants.PRE_PLAYABLE_MEDIA : initialPosition;
return Math.min(initialPosition, Constants.PRE_PLAYABLE_MEDIA);
}
private static ConnectivityManager getConnectivityManager() {

View File

@@ -0,0 +1,21 @@
package com.cappielloantonio.tempo.util;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import com.cappielloantonio.tempo.App;
public class NetworkUtil {
public static boolean isOffline() {
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo == null || !networkInfo.isConnected();
}
return true;
}
}

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? {
@@ -105,7 +111,7 @@ object Preferences {
}
@JvmStatic
fun askForOptimization(): Boolean? {
fun askForOptimization(): Boolean {
return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true)
}
@@ -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

@@ -1,7 +1,9 @@
package com.cappielloantonio.tempo.util;
import androidx.annotation.OptIn;
import androidx.media3.common.Metadata;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import com.cappielloantonio.tempo.model.ReplayGain;
@@ -10,6 +12,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@OptIn(markerClass = UnstableApi.class)
public class ReplayGainUtil {
private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"};
@@ -23,11 +26,15 @@ public class ReplayGainUtil {
private static List<Metadata> getMetadata(Tracks tracks) {
List<Metadata> metadata = new ArrayList<>();
for (int i = 0; i < tracks.getGroups().size(); i++) {
Tracks.Group group = tracks.getGroups().get(i);
if (tracks != null && !tracks.getGroups().isEmpty()) {
for (int i = 0; i < tracks.getGroups().size(); i++) {
Tracks.Group group = tracks.getGroups().get(i);
for (int j = 0; j < group.getMediaTrackGroup().length; j++) {
metadata.add(group.getTrackFormat(j).metadata);
if (group != null && group.getMediaTrackGroup() != null) {
for (int j = 0; j < group.getMediaTrackGroup().length; j++) {
metadata.add(group.getTrackFormat(j).metadata);
}
}
}
}
@@ -37,13 +44,19 @@ public class ReplayGainUtil {
private static List<ReplayGain> getReplayGains(List<Metadata> metadata) {
List<ReplayGain> gains = new ArrayList<>();
for (int i = 0; i < metadata.size(); i++) {
for (int j = 0; j < metadata.get(i).length(); j++) {
Metadata.Entry entry = metadata.get(i).get(j);
if (metadata != null) {
for (int i = 0; i < metadata.size(); i++) {
Metadata singleMetadata = metadata.get(i);
if (checkReplayGain(entry)) {
ReplayGain replayGain = setReplayGains(entry);
gains.add(replayGain);
if (singleMetadata != null) {
for (int j = 0; j < singleMetadata.length(); j++) {
Metadata.Entry entry = singleMetadata.get(j);
if (checkReplayGain(entry)) {
ReplayGain replayGain = setReplayGains(entry);
gains.add(replayGain);
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.cappielloantonio.tempo.util;
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;
}
}
}

View File

@@ -7,11 +7,14 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.NetworkUtil;
import java.util.Date;
import java.util.List;
@@ -19,6 +22,7 @@ import java.util.List;
public class AlbumBottomSheetViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private AlbumID3 album;
@@ -27,6 +31,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository();
favoriteRepository = new FavoriteRepository();
}
public AlbumID3 getAlbum() {
@@ -47,11 +52,51 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
public void setFavorite() {
if (album.getStarred() != null) {
artistRepository.unstar(album.getId());
album.setStarred(null);
if (NetworkUtil.isOffline()) {
removeFavoriteOffline();
} else {
removeFavoriteOnline();
}
} else {
artistRepository.star(album.getId());
album.setStarred(new Date());
if (NetworkUtil.isOffline()) {
setFavoriteOffline();
} else {
setFavoriteOnline();
}
}
}
private void removeFavoriteOffline() {
favoriteRepository.starLater(null, album.getId(), null, false);
album.setStarred(null);
}
private void removeFavoriteOnline() {
favoriteRepository.unstar(null, album.getId(), null, new StarCallback() {
@Override
public void onError() {
// album.setStarred(new Date());
favoriteRepository.starLater(null, album.getId(), null, false);
}
});
album.setStarred(null);
}
private void setFavoriteOffline() {
favoriteRepository.starLater(null, album.getId(), null, true);
album.setStarred(new Date());
}
private void setFavoriteOnline() {
favoriteRepository.star(null, album.getId(), null, new StarCallback() {
@Override
public void onError() {
// album.setStarred(null);
favoriteRepository.starLater(null, album.getId(), null, true);
}
});
album.setStarred(new Date());
}
}

View File

@@ -5,20 +5,25 @@ import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.util.NetworkUtil;
import java.util.Date;
public class ArtistBottomSheetViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private ArtistID3 artist;
public ArtistBottomSheetViewModel(@NonNull Application application) {
super(application);
albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository();
favoriteRepository = new FavoriteRepository();
}
public ArtistID3 getArtist() {
@@ -31,11 +36,51 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
public void setFavorite() {
if (artist.getStarred() != null) {
albumRepository.unstar(artist.getId());
artist.setStarred(null);
if (NetworkUtil.isOffline()) {
removeFavoriteOffline();
} else {
removeFavoriteOnline();
}
} else {
albumRepository.star(artist.getId());
artist.setStarred(new Date());
if (NetworkUtil.isOffline()) {
setFavoriteOffline();
} else {
setFavoriteOnline();
}
}
}
private void removeFavoriteOffline() {
favoriteRepository.starLater(null, null, artist.getId(), false);
artist.setStarred(null);
}
private void removeFavoriteOnline() {
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
@Override
public void onError() {
// artist.setStarred(new Date());
favoriteRepository.starLater(null, null, artist.getId(), false);
}
});
artist.setStarred(null);
}
private void setFavoriteOffline() {
favoriteRepository.starLater(null, null, artist.getId(), true);
artist.setStarred(new Date());
}
private void setFavoriteOnline() {
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
@Override
public void onError() {
// artist.setStarred(null);
favoriteRepository.starLater(null, null, artist.getId(), true);
}
});
artist.setStarred(new Date());
}
}

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

@@ -8,20 +8,24 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.ChronologyRepository;
import com.cappielloantonio.tempo.repository.PodcastRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
public class HomeViewModel extends AndroidViewModel {
@@ -31,6 +35,7 @@ public class HomeViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository;
private final ChronologyRepository chronologyRepository;
private final FavoriteRepository favoriteRepository;
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
@@ -57,6 +62,9 @@ public class HomeViewModel extends AndroidViewModel {
albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository();
chronologyRepository = new ChronologyRepository();
favoriteRepository = new FavoriteRepository();
setOfflineFavorite();
}
public LiveData<List<Child>> getDiscoverSongSample(LifecycleOwner owner) {
@@ -239,4 +247,109 @@ public class HomeViewModel extends AndroidViewModel {
public void refreshRecentlyPlayedAlbumList(LifecycleOwner owner) {
albumRepository.getAlbums("recent", 20, null, null).observe(owner, recentlyPlayedAlbumSample::postValue);
}
public void setOfflineFavorite() {
ArrayList<Favorite> favorites = getFavorites();
ArrayList<Favorite> favoritesToSave = getFavoritesToSave(favorites);
ArrayList<Favorite> favoritesToDelete = getFavoritesToDelete(favorites, favoritesToSave);
manageFavoriteToSave(favoritesToSave);
manageFavoriteToDelete(favoritesToDelete);
}
private ArrayList<Favorite> getFavorites() {
return new ArrayList<>(favoriteRepository.getFavorites());
}
private ArrayList<Favorite> getFavoritesToSave(ArrayList<Favorite> favorites) {
HashMap<String, Favorite> filteredMap = new HashMap<>();
for (Favorite favorite : favorites) {
String key = favorite.toString();
if (!filteredMap.containsKey(key) || favorite.getTimestamp() > filteredMap.get(key).getTimestamp()) {
filteredMap.put(key, favorite);
}
}
return new ArrayList<>(filteredMap.values());
}
private ArrayList<Favorite> getFavoritesToDelete(ArrayList<Favorite> favorites, ArrayList<Favorite> favoritesToSave) {
ArrayList<Favorite> favoritesToDelete = new ArrayList<>();
for (Favorite favorite : favorites) {
if (!favoritesToSave.contains(favorite)) {
favoritesToDelete.add(favorite);
}
}
return favoritesToDelete;
}
private void manageFavoriteToSave(ArrayList<Favorite> favoritesToSave) {
for (Favorite favorite : favoritesToSave) {
if (favorite.getToStar()) {
favoriteToStar(favorite);
} else {
favoriteToUnstar(favorite);
}
}
}
private void manageFavoriteToDelete(ArrayList<Favorite> favoritesToDelete) {
for (Favorite favorite : favoritesToDelete) {
favoriteRepository.delete(favorite);
}
}
private void favoriteToStar(Favorite favorite) {
if (favorite.getSongId() != null) {
favoriteRepository.star(favorite.getSongId(), null, null, new StarCallback() {
@Override
public void onSuccess() {
favoriteRepository.delete(favorite);
}
});
} else if (favorite.getAlbumId() != null) {
favoriteRepository.star(null, favorite.getAlbumId(), null, new StarCallback() {
@Override
public void onSuccess() {
favoriteRepository.delete(favorite);
}
});
} else if (favorite.getArtistId() != null) {
favoriteRepository.star(null, null, favorite.getArtistId(), new StarCallback() {
@Override
public void onSuccess() {
favoriteRepository.delete(favorite);
}
});
}
}
private void favoriteToUnstar(Favorite favorite) {
if (favorite.getSongId() != null) {
favoriteRepository.unstar(favorite.getSongId(), null, null, new StarCallback() {
@Override
public void onSuccess() {
favoriteRepository.delete(favorite);
}
});
} else if (favorite.getAlbumId() != null) {
favoriteRepository.unstar(null, favorite.getAlbumId(), null, new StarCallback() {
@Override
public void onSuccess() {
favoriteRepository.delete(favorite);
}
});
} else if (favorite.getArtistId() != null) {
favoriteRepository.unstar(null, null, favorite.getArtistId(), new StarCallback() {
@Override
public void onSuccess() {
favoriteRepository.delete(favorite);
}
});
}
}
}

View File

@@ -11,9 +11,11 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.QueueRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
@@ -22,6 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.Collections;
@@ -36,6 +39,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
private final SongRepository songRepository;
private final ArtistRepository artistRepository;
private final QueueRepository queueRepository;
private final FavoriteRepository favoriteRepository;
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
@@ -50,6 +54,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
songRepository = new SongRepository();
artistRepository = new ArtistRepository();
queueRepository = new QueueRepository();
favoriteRepository = new FavoriteRepository();
}
public LiveData<List<Queue>> getQueueSong() {
@@ -59,22 +64,62 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
public void setFavorite(Context context, Child media) {
if (media != null) {
if (media.getStarred() != null) {
songRepository.unstar(media.getId());
media.setStarred(null);
if (NetworkUtil.isOffline()) {
removeFavoriteOffline(media);
} else {
removeFavoriteOnline(media);
}
} else {
songRepository.star(media.getId());
media.setStarred(new Date());
if (Preferences.isStarredSyncEnabled()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownload(media),
new Download(media)
);
if (NetworkUtil.isOffline()) {
setFavoriteOffline(media);
} else {
setFavoriteOnline(context, media);
}
}
}
}
private void removeFavoriteOffline(Child media) {
favoriteRepository.starLater(media.getId(), null, null, false);
media.setStarred(null);
}
private void removeFavoriteOnline(Child media) {
favoriteRepository.unstar(media.getId(), null, null, new StarCallback() {
@Override
public void onError() {
// media.setStarred(new Date());
favoriteRepository.starLater(media.getId(), null, null, false);
}
});
media.setStarred(null);
}
private void setFavoriteOffline(Child media) {
favoriteRepository.starLater(media.getId(), null, null, true);
media.setStarred(new Date());
}
private void setFavoriteOnline(Context context, Child media) {
favoriteRepository.star(media.getId(), null, null, new StarCallback() {
@Override
public void onError() {
// media.setStarred(null);
favoriteRepository.starLater(media.getId(), null, null, true);
}
});
media.setStarred(new Date());
if (Preferences.isStarredSyncEnabled()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownload(media),
new Download(media)
);
}
}
public LiveData<String> getLiveLyrics() {
return lyricsLiveData;
}

View File

@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.repository.PodcastRepository;
import com.cappielloantonio.tempo.subsonic.models.PodcastChannel;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import java.util.List;
@@ -33,4 +34,8 @@ public class PodcastChannelPageViewModel extends AndroidViewModel {
public void setPodcastChannel(PodcastChannel podcastChannel) {
this.podcastChannel = podcastChannel;
}
public void requestPodcastEpisodeDownload(PodcastEpisode podcastEpisode) {
podcastRepository.downloadPodcastEpisode(podcastEpisode.getId());
}
}

View File

@@ -10,15 +10,18 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.Collections;
@@ -30,6 +33,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
private final SongRepository songRepository;
private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private Child song;
@@ -41,6 +45,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
songRepository = new SongRepository();
albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository();
favoriteRepository = new FavoriteRepository();
}
public Child getSong() {
@@ -53,18 +58,58 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
public void setFavorite(Context context) {
if (song.getStarred() != null) {
songRepository.unstar(song.getId());
song.setStarred(null);
} else {
songRepository.star(song.getId());
song.setStarred(new Date());
if (Preferences.isStarredSyncEnabled()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownload(song),
new Download(song)
);
if (NetworkUtil.isOffline()) {
removeFavoriteOffline(song);
} else {
removeFavoriteOnline(song);
}
} else {
if (NetworkUtil.isOffline()) {
setFavoriteOffline(song);
} else {
setFavoriteOnline(context, song);
}
}
}
private void removeFavoriteOffline(Child media) {
favoriteRepository.starLater(media.getId(), null, null, false);
media.setStarred(null);
}
private void removeFavoriteOnline(Child media) {
favoriteRepository.unstar(media.getId(), null, null, new StarCallback() {
@Override
public void onError() {
// media.setStarred(new Date());
favoriteRepository.starLater(media.getId(), null, null, false);
}
});
media.setStarred(null);
}
private void setFavoriteOffline(Child media) {
favoriteRepository.starLater(media.getId(), null, null, true);
media.setStarred(new Date());
}
private void setFavoriteOnline(Context context, Child media) {
favoriteRepository.star(media.getId(), null, null, new StarCallback() {
@Override
public void onError() {
// media.setStarred(null);
favoriteRepository.starLater(media.getId(), null, null, true);
}
});
media.setStarred(new Date());
if (Preferences.isStarredSyncEnabled()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownload(media),
new Download(media)
);
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="44dp"
android:bottomRightRadius="0px"
android:topLeftRadius="44dp"
android:topRightRadius="44dp" />
<solid android:color="?attr/colorPrimary" />
<size
android:width="88dp"
android:height="88dp" />
</shape>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="?attr/colorPrimary" />
<size android:width="4dp" android:height="32dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="?attr/colorPrimaryContainer" />
<size android:width="4dp" android:height="32dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/titleTextColor"
android:pathData="M439,878Q363,870 297.5,835.5Q232,801 184,747.5Q136,694 108.5,625Q81,556 81,479Q81,324 183.5,210.5Q286,97 440,80L440,160Q319,177 240,267.5Q161,358 161,479Q161,600 240,690.5Q319,781 439,798L439,878ZM479,680L278,478L335,421L439,525L439,280L519,280L519,525L622,422L679,480L479,680ZM519,878L519,798Q562,792 601.5,775Q641,758 675,732L733,790Q686,827 632,849.5Q578,872 519,878ZM677,226Q642,200 602.5,183Q563,166 520,160L520,80Q579,86 633,108.5Q687,131 733,168L677,226ZM789,732L733,675Q759,641 775,601.5Q791,562 797,519L879,519Q871,578 849,632.5Q827,687 789,732ZM797,439Q791,396 775,356.5Q759,317 733,283L789,226Q827,271 850,325.5Q873,380 879,439L797,439Z"/>
</vector>

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

@@ -39,8 +39,7 @@
android:paddingBottom="24dp"
android:text="@string/album_catalogue_title_expanded"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/album_list_sort_image_view"

View File

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

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

@@ -306,7 +306,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 +316,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

@@ -47,13 +47,30 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/index_recycler_view"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/index_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.cappielloantonio.tempo.helper.recyclerview.FastScrollbar
android:id="@+id/fast_scrollbar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/global_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View File

@@ -12,35 +12,67 @@
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_playlist_page_nested_scroll_view"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent">
<LinearLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/playlist_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:paddingTop="8dp">
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/playlist_cover_image_view"
android:id="@+id/playlist_cover_image_view_top_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_top_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/playlist_cover_image_view_top_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="64dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_top_left"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/playlist_cover_image_view_bottom_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_bottom_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/playlist_cover_image_view_top_left" />
<ImageView
android:id="@+id/playlist_cover_image_view_bottom_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="64dp"
android:layout_marginBottom="8dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_bottom_left"
app:layout_constraintTop_toTopOf="@id/playlist_cover_image_view_bottom_left" />
<TextView
android:id="@+id/playlist_name_label"
style="@style/LabelExtraLarge"
@@ -55,7 +87,7 @@
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_cover_image_view" />
app:layout_constraintTop_toBottomOf="@+id/playlist_cover_image_view_bottom_left" />
<TextView
android:id="@+id/playlist_song_count_label"
@@ -159,15 +191,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp"
android:paddingBottom="@dimen/global_padding_bottom" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View File

@@ -1,74 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/appbar_header_height">
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorSurface"
app:expandedTitleMarginStart="@dimen/activity_margin_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/podcast_channel_backdrop_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/appbar_header_height"
android:layout_gravity="top"
android:background="@drawable/gradient_backdrop_background_image" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_artist_page_nested_scroll_view"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:layout_height="match_parent">
<LinearLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="18dp"
android:paddingBottom="@dimen/global_padding_bottom">
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/podcast_channel_page_bio_sector"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/podcast_channel_page_info_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="22dp">
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<TextView
android:id="@+id/podcast_channel_description_label"
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:text="@string/podcast_channel_page_title_description_section" />
android:text="@string/podcast_channel_page_title_description_section"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/podcast_channel_description_text_view"
@@ -77,63 +48,61 @@
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp" />
</LinearLayout>
android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_channel_description_label" />
<include
android:id="@+id/podcast_channel_page_description_placeholder"
layout="@layout/item_placehoder_biography"
android:visibility="gone" />
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginHorizontal="16dp" />
<!-- Label and button -->
<LinearLayout
android:id="@+id/podcast_channel_page_episodes_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="22dp">
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_channel_description_text_view" />
<TextView
android:id="@+id/podcast_episodes_section_label"
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:text="@string/podcast_channel_page_title_episode_section" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/podcast_episodes_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp" />
<TextView
android:id="@+id/podcast_episodes_availability_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:text="@string/podcast_channel_page_title_no_episode_available"
android:visibility="gone" />
</LinearLayout>
android:paddingBottom="12dp"
android:text="@string/podcast_channel_page_title_episode_section"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider" />
<include
android:id="@+id/podcast_channel_page_episodes_placeholder"
layout="@layout/item_placeholder_horizontal"
android:visibility="gone" />
<Button
android:id="@+id/podcast_episodes_filter_image_view"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_filter_list"
app:layout_constraintBottom_toBottomOf="@+id/podcast_episodes_section_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/podcast_episodes_section_label" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/podcast_episodes_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View File

@@ -20,9 +20,9 @@
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:maxLines="5"
android:layout_marginStart="12dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@+id/podcast_subtitle_label"
app:layout_constraintEnd_toEndOf="parent"
@@ -36,9 +36,9 @@
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:maxLines="2"
android:layout_marginStart="12dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@id/podcast_upper_divider"
app:layout_constraintEnd_toEndOf="parent"
@@ -100,9 +100,21 @@
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_more_vert"
app:layout_constraintBottom_toBottomOf="@+id/podcast_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/podcast_play_button" />
<Button
android:id="@+id/podcast_download_request_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_podcast_download"
app:layout_constraintBottom_toBottomOf="@+id/podcast_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/podcast_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -15,6 +15,7 @@
android:layout_height="52dp"
android:layout_gravity="center"
android:layout_margin="2dp"
android:background="?attr/colorSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/fastscroller_bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/fast_scrollbar_bubble"
android:gravity="center"
android:textColor="?attr/colorOnPrimary"
android:textSize="48sp"
android:visibility="visible"
tools:text="A" />
<ImageView
android:id="@+id/fastscroller_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:src="@drawable/fast_scrollbar_handle" />
</merge>

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,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_podcast_filter_download"
android:title="@string/menu_filter_download" />
<item
android:id="@+id/menu_podcast_filter_all"
android:title="@string/menu_filter_all" />
</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,290 @@
<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_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_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_logout_title">Abmelden</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_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_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>
@@ -134,6 +145,7 @@
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS Url</string>
<string name="podcast_channel_editor_dialog_title">Podcast Channel</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="podcast_episode_download_request_snackbar">Your request has been sent to the server</string>
<string name="podcast_info_empty_subtitle">Once you add a channel, you\'ll find it here</string>
<string name="podcast_info_empty_title">No podcasts found!</string>
<string name="podcast_info_empty_button">Click to hide the section\nThe effects will be visible on restart</string>
@@ -171,14 +183,23 @@
<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>
@@ -186,6 +207,7 @@
<string name="settings_github_title">Github</string>
<string name="settings_image_size">Set image resolution</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>
@@ -207,6 +229,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>
@@ -215,8 +238,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>
@@ -248,6 +273,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>
@@ -265,4 +292,11 @@
<string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Random</string>
<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

@@ -78,12 +78,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 +89,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 +154,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

@@ -186,6 +186,17 @@ class MediaService : MediaLibraryService() {
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED &&
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
) {
MediaManager.scrobble(player.currentMediaItem)
MediaManager.saveChronology(player.currentMediaItem)
}
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,

File diff suppressed because one or more lines are too long

View File

@@ -259,6 +259,17 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED &&
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
) {
MediaManager.scrobble(player.currentMediaItem)
MediaManager.saveChronology(player.currentMediaItem)
}
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,

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