27 Commits
3.4.6 ... 3.4.8

Author SHA1 Message Date
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
antonio
fcbe4377aa gradle: bump up code version 2023-07-20 17:46:49 +02:00
antonio
84db4060e6 feat: added server-side track transcoding settings option 2023-07-20 17:20:57 +02:00
antonio
b73a1c532b fix: checking for the presence of a system equalizer 2023-07-20 12:33:10 +02:00
antonio
4fe27067e9 fix: null checking 2023-07-20 09:48:12 +02:00
60 changed files with 3009 additions and 508 deletions

View File

@@ -28,8 +28,8 @@ android {
tempo { tempo {
dimension "default" dimension "default"
applicationId 'com.cappielloantonio.tempo' applicationId 'com.cappielloantonio.tempo'
versionCode 13 versionCode 14
versionName '3.4.6' versionName '3.4.7'
} }
notquitemy { notquitemy {
@@ -74,7 +74,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.room:room-runtime:2.5.2' implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.core:core-splashscreen:1.0.1' 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

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

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 onServerClick(Bundle bundle) {}
default void onServerLongClick(Bundle bundle) {} default void onServerLongClick(Bundle bundle) {}
default void onPodcastEpisodeClick(Bundle bundle) {} default void onPodcastEpisodeClick(Bundle bundle) {}
default void onPodcastEpisodeAltClick(Bundle bundle) {}
default void onPodcastEpisodeLongClick(Bundle bundle) {} default void onPodcastEpisodeLongClick(Bundle bundle) {}
default void onPodcastChannelClick(Bundle bundle) {} default void onPodcastChannelClick(Bundle bundle) {}
default void onPodcastChannelLongClick(Bundle bundle) {} default void onPodcastChannelLongClick(Bundle bundle) {}

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

@@ -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; import retrofit2.Response;
public class AlbumRepository { public class AlbumRepository {
private static final String TAG = "AlbumRepository";
public MutableLiveData<List<AlbumID3>> getAlbums(String type, int size, Integer fromYear, Integer toYear) { public MutableLiveData<List<AlbumID3>> getAlbums(String type, int size, Integer fromYear, Integer toYear) {
MutableLiveData<List<AlbumID3>> listLiveAlbums = new MutableLiveData<>(new ArrayList<>()); MutableLiveData<List<AlbumID3>> listLiveAlbums = new MutableLiveData<>(new ArrayList<>());
@@ -78,40 +76,6 @@ public class AlbumRepository {
return starredAlbums; 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) { public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getMediaAnnotationClient() .getMediaAnnotationClient()

View File

@@ -135,9 +135,6 @@ public class ArtistRepository {
return artist; 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) { public MutableLiveData<ArtistInfo2> getArtistFullInfo(String id) {
MutableLiveData<ArtistInfo2> artistFullInfo = new MutableLiveData<>(null); MutableLiveData<ArtistInfo2> artistFullInfo = new MutableLiveData<>(null);
@@ -161,40 +158,6 @@ public class ArtistRepository {
return artistFullInfo; 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) { public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getMediaAnnotationClient() .getMediaAnnotationClient()

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

@@ -24,9 +24,14 @@ public class GenreRepository {
.enqueue(new Callback<ApiResponse>() { .enqueue(new Callback<ApiResponse>() {
@Override @Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getGenres() != null) { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null && response.body().getSubsonicResponse().getGenres() != null) {
List<Genre> genreList = response.body().getSubsonicResponse().getGenres().getGenres(); List<Genre> genreList = response.body().getSubsonicResponse().getGenres().getGenres();
if (genreList == null || genreList.isEmpty()) {
genres.setValue(Collections.emptyList());
return;
}
if (random) { if (random) {
Collections.shuffle(genreList); Collections.shuffle(genreList);
} }

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 androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App; 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.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Child; 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) { public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getMediaAnnotationClient() .getMediaAnnotationClient()
@@ -173,8 +141,6 @@ public class SongRepository {
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) { public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>(); MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
Log.d(TAG, "onScrolled PAGE: " + page);
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getAlbumSongListClient() .getAlbumSongListClient()
.getSongsByGenre(id, 100, 100 * page) .getSongsByGenre(id, 100, 100 * page)

View File

@@ -48,4 +48,9 @@ public class PodcastClient {
Log.d(TAG, "deletePodcastEpisode()"); Log.d(TAG, "deletePodcastEpisode()");
return podcastService.deletePodcastEpisode(subsonic.getParams(), episodeId); 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") @GET("deletePodcastEpisode")
Call<ApiResponse> deletePodcastEpisode(@QueryMap Map<String, String> params, @Query("id") String id); 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 android.os.Parcelable
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.room.ColumnInfo
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.util.* import java.util.*
@@ -29,11 +28,9 @@ class PodcastEpisode : Parcelable {
var transcodedContentType: String? = null var transcodedContentType: String? = null
var transcodedSuffix: String? = null var transcodedSuffix: String? = null
var duration: Int? = null var duration: Int? = null
@ColumnInfo("bitrate")
@SerializedName("bitRate") @SerializedName("bitRate")
var bitrate: Int? = null var bitrate: Int? = null
var path: String? = null var path: String? = null
@ColumnInfo(name = "is_video")
@SerializedName("isVideo") @SerializedName("isVideo")
var isVideo: Boolean = false var isVideo: Boolean = false
var userRating: Int? = null var userRating: Int? = null

View File

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

View File

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

View File

@@ -9,16 +9,17 @@ import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.databinding.ItemLibraryMusicIndexBinding; 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.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.Artist; import com.cappielloantonio.tempo.subsonic.models.Artist;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
@UnstableApi @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 final ClickCallback click;
private List<Artist> artists; private List<Artist> artists;
@@ -41,10 +42,10 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
holder.item.musicIndexTitleTextView.setText(artist.getName()); holder.item.musicIndexTitleTextView.setText(artist.getName());
CustomGlideRequest.Builder /* CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getName()) .from(holder.itemView.getContext(), artist.getName())
.build() .build()
.into(holder.item.musicIndexCoverImageView); .into(holder.item.musicIndexCoverImageView); */
} }
@Override @Override
@@ -57,6 +58,11 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
notifyDataSetChanged(); 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 { public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryMusicIndexBinding item; ItemLibraryMusicIndexBinding item;

View File

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

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -18,11 +19,14 @@ import com.cappielloantonio.tempo.util.MusicUtil;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAdapter.ViewHolder> { public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAdapter.ViewHolder> {
private final ClickCallback click; private final ClickCallback click;
private List<PodcastEpisode> podcastEpisodes; private List<PodcastEpisode> podcastEpisodes;
private List<PodcastEpisode> podcastEpisodesFull;
public PodcastEpisodeAdapter(ClickCallback click) { public PodcastEpisodeAdapter(ClickCallback click) {
this.click = click; this.click = click;
@@ -50,6 +54,10 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
.from(holder.itemView.getContext(), podcastEpisode.getCoverArtId()) .from(holder.itemView.getContext(), podcastEpisode.getCoverArtId())
.build() .build()
.into(holder.item.podcastCoverImageView); .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 @Override
@@ -58,7 +66,8 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
} }
public void setItems(List<PodcastEpisode> podcastEpisodes) { 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(); notifyDataSetChanged();
} }
@@ -85,22 +94,57 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
item.podcastPlayButton.setOnClickListener(v -> onClick()); item.podcastPlayButton.setOnClickListener(v -> onClick());
item.podcastMoreButton.setOnClickListener(v -> openMore()); item.podcastMoreButton.setOnClickListener(v -> openMore());
item.podcastDownloadRequestButton.setOnClickListener(v -> requestDownload());
} }
public void onClick() { public void onClick() {
Bundle bundle = new Bundle(); PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition());
bundle.putParcelable(Constants.PODCAST_OBJECT, 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() { private boolean openMore() {
Bundle bundle = new Bundle(); PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition());
bundle.putParcelable(Constants.PODCAST_OBJECT, 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

@@ -96,6 +96,9 @@ public class IndexFragment extends Fragment implements ClickCallback {
musicIndexAdapter.setItems(IndexUtil.getArtist(indexes)); 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 @Override

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -51,6 +52,7 @@ public class PlayerControllerFragment extends Fragment {
private Chip playerMediaExtension; private Chip playerMediaExtension;
private TextView playerMediaBitrate; private TextView playerMediaBitrate;
private ImageView playerMediaTranscodingIcon; private ImageView playerMediaTranscodingIcon;
private ImageView playerMediaTranscodingPriorityIcon;
private Chip playerMediaTranscodedExtension; private Chip playerMediaTranscodedExtension;
private TextView playerMediaTranscodedBitrate; private TextView playerMediaTranscodedBitrate;
@@ -71,6 +73,7 @@ public class PlayerControllerFragment extends Fragment {
initCoverLyricsSlideView(); initCoverLyricsSlideView();
initMediaListenable(); initMediaListenable();
initArtistLabelButton(); initArtistLabelButton();
initTranscodingInfo();
return view; return view;
} }
@@ -104,6 +107,7 @@ public class PlayerControllerFragment extends Fragment {
playerMediaExtension = bind.getRoot().findViewById(R.id.player_media_extension); playerMediaExtension = bind.getRoot().findViewById(R.id.player_media_extension);
playerMediaBitrate = bind.getRoot().findViewById(R.id.player_media_bitrate); playerMediaBitrate = bind.getRoot().findViewById(R.id.player_media_bitrate);
playerMediaTranscodingIcon = bind.getRoot().findViewById(R.id.player_media_transcoding_audio); playerMediaTranscodingIcon = bind.getRoot().findViewById(R.id.player_media_transcoding_audio);
playerMediaTranscodingPriorityIcon = bind.getRoot().findViewById(R.id.player_media_server_transcode_priority);
playerMediaTranscodedExtension = bind.getRoot().findViewById(R.id.player_media_transcoded_extension); playerMediaTranscodedExtension = bind.getRoot().findViewById(R.id.player_media_transcoded_extension);
playerMediaTranscodedBitrate = bind.getRoot().findViewById(R.id.player_media_transcoded_bitrate); playerMediaTranscodedBitrate = bind.getRoot().findViewById(R.id.player_media_transcoded_bitrate);
} }
@@ -166,7 +170,6 @@ public class PlayerControllerFragment extends Fragment {
playerMediaBitrate.setVisibility(View.GONE); playerMediaBitrate.setVisibility(View.GONE);
} else { } else {
playerMediaBitrate.setVisibility(View.VISIBLE); playerMediaBitrate.setVisibility(View.VISIBLE);
playerMediaBitrate.setText(bitrate); playerMediaBitrate.setText(bitrate);
} }
} }
@@ -177,19 +180,28 @@ public class PlayerControllerFragment extends Fragment {
: "Original"; : "Original";
if (transcodingExtension.equals("raw") && transcodingBitrate.equals("Original")) { if (transcodingExtension.equals("raw") && transcodingBitrate.equals("Original")) {
playerMediaTranscodingPriorityIcon.setVisibility(View.GONE);
playerMediaTranscodingIcon.setVisibility(View.GONE); playerMediaTranscodingIcon.setVisibility(View.GONE);
playerMediaTranscodedBitrate.setVisibility(View.GONE); playerMediaTranscodedBitrate.setVisibility(View.GONE);
playerMediaTranscodedExtension.setVisibility(View.GONE); playerMediaTranscodedExtension.setVisibility(View.GONE);
} else { } else {
playerMediaTranscodingPriorityIcon.setVisibility(View.GONE);
playerMediaTranscodingIcon.setVisibility(View.VISIBLE); playerMediaTranscodingIcon.setVisibility(View.VISIBLE);
playerMediaTranscodedBitrate.setVisibility(View.VISIBLE); playerMediaTranscodedBitrate.setVisibility(View.VISIBLE);
playerMediaTranscodedExtension.setVisibility(View.VISIBLE); playerMediaTranscodedExtension.setVisibility(View.VISIBLE);
playerMediaTranscodedExtension.setText(transcodingExtension); playerMediaTranscodedExtension.setText(transcodingExtension);
playerMediaTranscodedBitrate.setText(transcodingBitrate); playerMediaTranscodedBitrate.setText(transcodingBitrate);
} }
if (mediaMetadata.extras != null && mediaMetadata.extras.getString("uri", "").contains(Constants.DOWNLOAD_URI)) { if (mediaMetadata.extras != null && mediaMetadata.extras.getString("uri", "").contains(Constants.DOWNLOAD_URI)) {
playerMediaTranscodingPriorityIcon.setVisibility(View.GONE);
playerMediaTranscodingIcon.setVisibility(View.GONE);
playerMediaTranscodedBitrate.setVisibility(View.GONE);
playerMediaTranscodedExtension.setVisibility(View.GONE);
}
if (Preferences.isServerPrioritized() && mediaMetadata.extras != null && !mediaMetadata.extras.getString("uri", "").contains(Constants.DOWNLOAD_URI)) {
playerMediaTranscodingPriorityIcon.setVisibility(View.VISIBLE);
playerMediaTranscodingIcon.setVisibility(View.GONE); playerMediaTranscodingIcon.setVisibility(View.GONE);
playerMediaTranscodedBitrate.setVisibility(View.GONE); playerMediaTranscodedBitrate.setVisibility(View.GONE);
playerMediaTranscodedExtension.setVisibility(View.GONE); playerMediaTranscodedExtension.setVisibility(View.GONE);
@@ -293,6 +305,13 @@ public class PlayerControllerFragment extends Fragment {
}); });
} }
private void initTranscodingInfo() {
playerMediaTranscodingPriorityIcon.setOnLongClickListener(view -> {
Toast.makeText(requireActivity(), R.string.settings_audio_transcode_priority_toast, Toast.LENGTH_SHORT).show();
return true;
});
}
private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) { private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) {
playbackSpeedButton.setOnClickListener(view -> { playbackSpeedButton.setOnClickListener(view -> {
float currentSpeed = Preferences.getPlaybackSpeed(); float currentSpeed = Preferences.getPlaybackSpeed();

View File

@@ -19,6 +19,7 @@ import androidx.media3.session.SessionToken;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentPlaylistPageBinding; import com.cappielloantonio.tempo.databinding.FragmentPlaylistPageBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.glide.CustomGlideRequest;
@@ -146,13 +147,13 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
if (bind != null) { if (bind != null) {
bind.playlistPagePlayButton.setOnClickListener(v -> { bind.playlistPagePlayButton.setOnClickListener(v -> {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(100, songs.size())), 0);
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
}); });
bind.playlistPageShuffleButton.setOnClickListener(v -> { bind.playlistPageShuffleButton.setOnClickListener(v -> {
Collections.shuffle(songs); Collections.shuffle(songs);
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(100, songs.size())), 0);
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
}); });
} }
@@ -160,10 +161,39 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
} }
private void initBackCover() { private void initBackCover() {
CustomGlideRequest.Builder playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
.from(requireContext(), playlistPageViewModel.getPlaylist().getCoverArtId()) if (bind != null) {
.build() Collections.shuffle(songs);
.into(bind.playlistCoverImageView);
// 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() { private void initSongsView() {

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.PopupMenu;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -17,7 +18,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentPodcastChannelPageBinding; import com.cappielloantonio.tempo.databinding.FragmentPodcastChannelPageBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
@@ -28,11 +28,10 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.UIUtil; import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.PodcastChannelPageViewModel; import com.cappielloantonio.tempo.viewmodel.PodcastChannelPageViewModel;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@UnstableApi @UnstableApi
public class PodcastChannelPageFragment extends Fragment implements ClickCallback { public class PodcastChannelPageFragment extends Fragment implements ClickCallback {
@@ -84,28 +83,26 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
} }
private void initAppBar() { private void initAppBar() {
activity.setSupportActionBar(bind.animToolbar); activity.setSupportActionBar(bind.toolbar);
if (activity.getSupportActionBar() != null)
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
bind.collapsingToolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle())); if (activity.getSupportActionBar() != null) {
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
bind.collapsingToolbar.setExpandedTitleColor(getResources().getColor(R.color.white, null)); 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() { private void initPodcastChannelInfo() {
String normalizePodcastChannelDescription = MusicUtil.forceReadableString(podcastChannelPageViewModel.getPodcastChannel().getDescription()); String normalizePodcastChannelDescription = MusicUtil.forceReadableString(podcastChannelPageViewModel.getPodcastChannel().getDescription());
if (bind != null) if (bind != null) {
bind.podcastChannelDescriptionTextView.setVisibility(!normalizePodcastChannelDescription.trim().isEmpty() ? View.VISIBLE : View.GONE); 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.podcastChannelDescriptionTextView.setText(normalizePodcastChannelDescription);
bind.podcastEpisodesFilterImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.filter_podcast_episode_popup_menu));
}
} }
private void initPodcastChannelEpisodesView() { private void initPodcastChannelEpisodesView() {
@@ -116,22 +113,21 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
bind.podcastEpisodesRecyclerView.setAdapter(podcastEpisodeAdapter); bind.podcastEpisodesRecyclerView.setAdapter(podcastEpisodeAdapter);
podcastChannelPageViewModel.getPodcastChannelEpisodes().observe(getViewLifecycleOwner(), channels -> { podcastChannelPageViewModel.getPodcastChannelEpisodes().observe(getViewLifecycleOwner(), channels -> {
if (channels == null) { if (channels == null) {
if (bind != null) if (bind != null) {
bind.podcastChannelPageEpisodesPlaceholder.placeholder.setVisibility(View.VISIBLE); bind.podcastEpisodesRecyclerView.setVisibility(View.GONE);
if (bind != null) bind.podcastChannelPageEpisodesSector.setVisibility(View.GONE); }
} else { } else {
if (bind != null) if (bind != null) {
bind.podcastChannelPageEpisodesPlaceholder.placeholder.setVisibility(View.GONE); bind.podcastEpisodesRecyclerView.setVisibility(View.VISIBLE);
}
if (!channels.isEmpty() && channels.get(0) != null && channels.get(0).getEpisodes() != null) { 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.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); 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 @Override
public void onPodcastEpisodeClick(Bundle bundle) { public void onPodcastEpisodeClick(Bundle bundle) {
MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT)); MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT));
@@ -155,4 +170,14 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
public void onPodcastEpisodeLongClick(Bundle bundle) { public void onPodcastEpisodeLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.podcastEpisodeBottomSheetDialog, 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

@@ -15,6 +15,7 @@ import androidx.annotation.OptIn;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.BuildConfig;
@@ -70,6 +71,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
checkEqualizer();
findPreference("version").setSummary(BuildConfig.VERSION_NAME); findPreference("version").setSummary(BuildConfig.VERSION_NAME);
findPreference("logout").setOnPreferenceClickListener(preference -> { findPreference("logout").setOnPreferenceClickListener(preference -> {
@@ -93,12 +96,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
return true; return true;
}); });
findPreference("equalizer").setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
someActivityResultLauncher.launch(intent);
return true;
});
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof Boolean) { if (newValue instanceof Boolean) {
if ((Boolean) newValue) { if ((Boolean) newValue) {
@@ -130,6 +127,23 @@ public class SettingsFragment extends PreferenceFragmentCompat {
} }
} }
private void checkEqualizer() {
Preference equalizer = findPreference("equalizer");
if (equalizer == null) return;
Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
equalizer.setOnPreferenceClickListener(preference -> {
someActivityResultLauncher.launch(intent);
return true;
});
} else {
equalizer.setVisible(false);
}
}
private void getScanStatus() { private void getScanStatus() {
settingViewModel.getScanStatus(new ScanCallback() { settingViewModel.getScanStatus(new ScanCallback() {
@Override @Override

View File

@@ -91,7 +91,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
artistAlbum.setText(MusicUtil.getReadableString(albumBottomSheetViewModel.getAlbum().getArtist())); artistAlbum.setText(MusicUtil.getReadableString(albumBottomSheetViewModel.getAlbum().getArtist()));
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); 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 -> { favoriteToggle.setOnClickListener(v -> {
albumBottomSheetViewModel.setFavorite(); albumBottomSheetViewModel.setFavorite();
dismissBottomSheet(); dismissBottomSheet();

View File

@@ -79,7 +79,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
nameArtist.setSelected(true); nameArtist.setSelected(true);
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); 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 -> { favoriteToggle.setOnClickListener(v -> {
artistBottomSheetViewModel.setFavorite(); artistBottomSheetViewModel.setFavorite();
dismissBottomSheet(); dismissBottomSheet();

View File

@@ -87,7 +87,7 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
artistSong.setText(MusicUtil.getReadableString(songBottomSheetViewModel.getSong().getArtist())); artistSong.setText(MusicUtil.getReadableString(songBottomSheetViewModel.getSong().getArtist()));
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); 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 -> { favoriteToggle.setOnClickListener(v -> {
songBottomSheetViewModel.setFavorite(requireContext()); songBottomSheetViewModel.setFavorite(requireContext());
dismissBottomSheet(); dismissBottomSheet();

View File

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

View File

@@ -41,8 +41,11 @@ public class MusicUtil {
if (params.containsKey("c") && params.get("c") != null) if (params.containsKey("c") && params.get("c") != null)
uri.append("&c=").append(params.get("c")); uri.append("&c=").append(params.get("c"));
uri.append("&maxBitRate=").append(getBitratePreference()); if (!Preferences.isServerPrioritized())
uri.append("&format=").append(getTranscodingFormatPreference()); uri.append("&maxBitRate=").append(getBitratePreference());
if (!Preferences.isServerPrioritized())
uri.append("&format=").append(getTranscodingFormatPreference());
uri.append("&id=").append(id); uri.append("&id=").append(id);
Log.d(TAG, "getStreamUri: " + uri); Log.d(TAG, "getStreamUri: " + uri);

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

@@ -32,6 +32,7 @@ object Preferences {
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility" private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility" private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
private const val REPLAY_GAIN_MODE = "replay_gain_mode" private const val REPLAY_GAIN_MODE = "replay_gain_mode"
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
@JvmStatic @JvmStatic
fun getServer(): String? { fun getServer(): String? {
@@ -104,7 +105,7 @@ object Preferences {
} }
@JvmStatic @JvmStatic
fun askForOptimization(): Boolean? { fun askForOptimization(): Boolean {
return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true) return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true)
} }
@@ -252,4 +253,9 @@ object Preferences {
fun getReplayGainMode(): String? { fun getReplayGainMode(): String? {
return App.getInstance().preferences.getString(REPLAY_GAIN_MODE, "disabled") return App.getInstance().preferences.getString(REPLAY_GAIN_MODE, "disabled")
} }
@JvmStatic
fun isServerPrioritized(): Boolean {
return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_PRIORITY, false)
}
} }

View File

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

View File

@@ -7,11 +7,14 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.NetworkUtil;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -19,6 +22,7 @@ import java.util.List;
public class AlbumBottomSheetViewModel extends AndroidViewModel { public class AlbumBottomSheetViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository; private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository; private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private AlbumID3 album; private AlbumID3 album;
@@ -27,6 +31,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
albumRepository = new AlbumRepository(); albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository(); artistRepository = new ArtistRepository();
favoriteRepository = new FavoriteRepository();
} }
public AlbumID3 getAlbum() { public AlbumID3 getAlbum() {
@@ -47,11 +52,51 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
public void setFavorite() { public void setFavorite() {
if (album.getStarred() != null) { if (album.getStarred() != null) {
artistRepository.unstar(album.getId()); if (NetworkUtil.isOffline()) {
album.setStarred(null); removeFavoriteOffline();
} else {
removeFavoriteOnline();
}
} else { } else {
artistRepository.star(album.getId()); if (NetworkUtil.isOffline()) {
album.setStarred(new Date()); 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.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel; 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.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.util.NetworkUtil;
import java.util.Date; import java.util.Date;
public class ArtistBottomSheetViewModel extends AndroidViewModel { public class ArtistBottomSheetViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository; private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private ArtistID3 artist; private ArtistID3 artist;
public ArtistBottomSheetViewModel(@NonNull Application application) { public ArtistBottomSheetViewModel(@NonNull Application application) {
super(application); super(application);
albumRepository = new AlbumRepository(); artistRepository = new ArtistRepository();
favoriteRepository = new FavoriteRepository();
} }
public ArtistID3 getArtist() { public ArtistID3 getArtist() {
@@ -31,11 +36,51 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
public void setFavorite() { public void setFavorite() {
if (artist.getStarred() != null) { if (artist.getStarred() != null) {
albumRepository.unstar(artist.getId()); if (NetworkUtil.isOffline()) {
artist.setStarred(null); removeFavoriteOffline();
} else {
removeFavoriteOnline();
}
} else { } else {
albumRepository.star(artist.getId()); if (NetworkUtil.isOffline()) {
artist.setStarred(new Date()); 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,20 +8,24 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.ChronologyRepository; 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.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
public class HomeViewModel extends AndroidViewModel { public class HomeViewModel extends AndroidViewModel {
@@ -31,6 +35,7 @@ public class HomeViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository; private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository; private final ArtistRepository artistRepository;
private final ChronologyRepository chronologyRepository; private final ChronologyRepository chronologyRepository;
private final FavoriteRepository favoriteRepository;
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null); private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null); private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
@@ -57,6 +62,9 @@ public class HomeViewModel extends AndroidViewModel {
albumRepository = new AlbumRepository(); albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository(); artistRepository = new ArtistRepository();
chronologyRepository = new ChronologyRepository(); chronologyRepository = new ChronologyRepository();
favoriteRepository = new FavoriteRepository();
setOfflineFavorite();
} }
public LiveData<List<Child>> getDiscoverSongSample(LifecycleOwner owner) { public LiveData<List<Child>> getDiscoverSongSample(LifecycleOwner owner) {
@@ -239,4 +247,109 @@ public class HomeViewModel extends AndroidViewModel {
public void refreshRecentlyPlayedAlbumList(LifecycleOwner owner) { public void refreshRecentlyPlayedAlbumList(LifecycleOwner owner) {
albumRepository.getAlbums("recent", 20, null, null).observe(owner, recentlyPlayedAlbumSample::postValue); 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.lifecycle.MutableLiveData;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Queue; import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.QueueRepository; import com.cappielloantonio.tempo.repository.QueueRepository;
import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; 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.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import java.util.Collections; import java.util.Collections;
@@ -36,6 +39,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
private final SongRepository songRepository; private final SongRepository songRepository;
private final ArtistRepository artistRepository; private final ArtistRepository artistRepository;
private final QueueRepository queueRepository; private final QueueRepository queueRepository;
private final FavoriteRepository favoriteRepository;
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null); private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
@@ -50,6 +54,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
songRepository = new SongRepository(); songRepository = new SongRepository();
artistRepository = new ArtistRepository(); artistRepository = new ArtistRepository();
queueRepository = new QueueRepository(); queueRepository = new QueueRepository();
favoriteRepository = new FavoriteRepository();
} }
public LiveData<List<Queue>> getQueueSong() { public LiveData<List<Queue>> getQueueSong() {
@@ -59,22 +64,62 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
public void setFavorite(Context context, Child media) { public void setFavorite(Context context, Child media) {
if (media != null) { if (media != null) {
if (media.getStarred() != null) { if (media.getStarred() != null) {
songRepository.unstar(media.getId()); if (NetworkUtil.isOffline()) {
media.setStarred(null); removeFavoriteOffline(media);
} else {
removeFavoriteOnline(media);
}
} else { } else {
songRepository.star(media.getId()); if (NetworkUtil.isOffline()) {
media.setStarred(new Date()); setFavoriteOffline(media);
} else {
if (Preferences.isStarredSyncEnabled()) { setFavoriteOnline(context, media);
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownload(media),
new Download(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() { public LiveData<String> getLiveLyrics() {
return lyricsLiveData; return lyricsLiveData;
} }

View File

@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.repository.PodcastRepository; import com.cappielloantonio.tempo.repository.PodcastRepository;
import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; import com.cappielloantonio.tempo.subsonic.models.PodcastChannel;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import java.util.List; import java.util.List;
@@ -33,4 +34,8 @@ public class PodcastChannelPageViewModel extends AndroidViewModel {
public void setPodcastChannel(PodcastChannel podcastChannel) { public void setPodcastChannel(PodcastChannel podcastChannel) {
this.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.lifecycle.MutableLiveData;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import java.util.Collections; import java.util.Collections;
@@ -30,6 +33,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
private final SongRepository songRepository; private final SongRepository songRepository;
private final AlbumRepository albumRepository; private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository; private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private Child song; private Child song;
@@ -41,6 +45,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
songRepository = new SongRepository(); songRepository = new SongRepository();
albumRepository = new AlbumRepository(); albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository(); artistRepository = new ArtistRepository();
favoriteRepository = new FavoriteRepository();
} }
public Child getSong() { public Child getSong() {
@@ -53,18 +58,58 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
public void setFavorite(Context context) { public void setFavorite(Context context) {
if (song.getStarred() != null) { if (song.getStarred() != null) {
songRepository.unstar(song.getId()); if (NetworkUtil.isOffline()) {
song.setStarred(null); removeFavoriteOffline(song);
} else { } else {
songRepository.star(song.getId()); removeFavoriteOnline(song);
song.setStarred(new Date());
if (Preferences.isStarredSyncEnabled()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownload(song),
new Download(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,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="M480,796.92Q455.25,796.92 437.63,779.29Q420,761.67 420,736.92Q420,712.17 437.63,694.55Q455.25,676.92 480,676.92Q504.75,676.92 522.37,694.55Q540,712.17 540,736.92Q540,761.67 522.37,779.29Q504.75,796.92 480,796.92ZM425.39,600.77L425.39,143.08L534.61,143.08L534.61,600.77L425.39,600.77Z"/>
</vector>

View File

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

View File

@@ -47,13 +47,30 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/index_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:paddingBottom="@dimen/global_padding_bottom"
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> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout> </LinearLayout>

View File

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

View File

@@ -1,74 +1,45 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout" android:id="@+id/layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/appbar" android:id="@+id/toolbar"
android:layout_width="match_parent" 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 <androidx.coordinatorlayout.widget.CoordinatorLayout
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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="18dp"
android:paddingBottom="@dimen/global_padding_bottom">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/podcast_channel_page_bio_sector" android:id="@+id/podcast_channel_page_info_sector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:background="?attr/colorSurface"
android:paddingBottom="22dp"> app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<TextView <TextView
android:id="@+id/podcast_channel_description_label"
style="@style/TitleLarge" style="@style/TitleLarge"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="8dp" 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 <TextView
android:id="@+id/podcast_channel_description_text_view" android:id="@+id/podcast_channel_description_text_view"
@@ -77,63 +48,61 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingEnd="16dp" /> android:paddingEnd="16dp"
</LinearLayout> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_channel_description_label" />
<include <View
android:id="@+id/podcast_channel_page_description_placeholder" android:id="@+id/upper_button_divider"
layout="@layout/item_placehoder_biography" style="@style/Divider"
android:visibility="gone" /> android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
<View app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/upper_button_divider" app:layout_constraintStart_toStartOf="parent"
style="@style/Divider" app:layout_constraintTop_toBottomOf="@+id/podcast_channel_description_text_view" />
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">
<TextView <TextView
android:id="@+id/podcast_episodes_section_label"
style="@style/TitleLarge" style="@style/TitleLarge"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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_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:paddingStart="16dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:text="@string/podcast_channel_page_title_no_episode_available" android:paddingBottom="12dp"
android:visibility="gone" /> android:text="@string/podcast_channel_page_title_episode_section"
</LinearLayout> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider" />
<include <Button
android:id="@+id/podcast_channel_page_episodes_placeholder" android:id="@+id/podcast_episodes_filter_image_view"
layout="@layout/item_placeholder_horizontal" style="@style/Widget.Material3.Button.TonalButton.Icon"
android:visibility="gone" /> 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.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<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

@@ -41,6 +41,13 @@
android:src="@drawable/ic_transcode" android:src="@drawable/ic_transcode"
android:visibility="gone" /> android:visibility="gone" />
<ImageView
android:id="@+id/player_media_server_transcode_priority"
android:layout_width="24dp"
android:layout_height="20dp"
android:src="@drawable/ic_server_transcode_priority"
android:visibility="gone" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/player_media_transcoded_extension" android:id="@+id/player_media_transcoded_extension"
style="@style/Widget.Material3.Chip.Suggestion" style="@style/Widget.Material3.Chip.Suggestion"

View File

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

View File

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

@@ -109,6 +109,7 @@
<string name="menu_search_button">Search</string> <string name="menu_search_button">Search</string>
<string name="menu_settings_button">Settings</string> <string name="menu_settings_button">Settings</string>
<string name="player_playback_speed">%1$.2fx</string> <string name="player_playback_speed">%1$.2fx</string>
<string name="player_server_priority">Server Priority</string>
<string name="playlist_catalogue_title">Playlist Catalogue</string> <string name="playlist_catalogue_title">Playlist Catalogue</string>
<string name="playlist_catalogue_title_expanded">Browse Playlists</string> <string name="playlist_catalogue_title_expanded">Browse Playlists</string>
<string name="playlist_chooser_dialog_empty">No playlists created</string> <string name="playlist_chooser_dialog_empty">No playlists created</string>
@@ -133,6 +134,7 @@
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS Url</string> <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_channel_editor_dialog_title">Podcast Channel</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</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_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_title">No podcasts found!</string>
<string name="podcast_info_empty_button">Click to hide the section\nThe effects will be visible on restart</string> <string name="podcast_info_empty_button">Click to hide the section\nThe effects will be visible on restart</string>
@@ -170,10 +172,13 @@
<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="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_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_about_title">About</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_mobile">Transcode format in mobile</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_audio_transcode_format_wifi">Transcode format in Wi-Fi</string>
<string name="settings_covers_cache">Size of artwork cache</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_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_data_saving_mode_title">Limit mobile data usage</string>
<string name="settings_equalizer_summary">Adjust audio settings</string> <string name="settings_equalizer_summary">Adjust audio settings</string>
<string name="settings_equalizer_title">Equalizer</string> <string name="settings_equalizer_title">Equalizer</string>
@@ -242,7 +247,7 @@
<string name="starred_sync_dialog_title">Sync starred tracks</string> <string name="starred_sync_dialog_title">Sync starred tracks</string>
<string name="undraw_url">https://undraw.co/</string> <string name="undraw_url">https://undraw.co/</string>
<string name="undraw_page">unDraw</string> <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="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_radio_station">Radio stations</string>
<string name="home_title_new_releases">New releases</string> <string name="home_title_new_releases">New releases</string>
<string name="home_title_best_of">Best of</string> <string name="home_title_best_of">Best of</string>
@@ -261,6 +266,6 @@
<string name="menu_sort_name">Name</string> <string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Random</string> <string name="menu_sort_random">Random</string>
<string name="description_empty_title">No description available</string> <string name="description_empty_title">No description available</string>
<!-- TODO: Remove or change this placeholder text --> <string name="menu_filter_download">Downloaded</string>
<string name="hello_blank_fragment">Hello blank fragment</string> <string name="menu_filter_all">All</string>
</resources> </resources>

View File

@@ -102,6 +102,12 @@
app:selectable="false" app:selectable="false"
app:summary="@string/settings_summary_transcoding" /> app:summary="@string/settings_summary_transcoding" />
<SwitchPreference
android:title="@string/settings_audio_transcode_priority_title"
android:defaultValue="false"
android:summary="@string/settings_audio_transcode_priority_summary"
android:key="audio_transcode_priority" />
<ListPreference <ListPreference
app:defaultValue="raw" app:defaultValue="raw"
app:dialogTitle="@string/settings_audio_transcode_format_wifi" app:dialogTitle="@string/settings_audio_transcode_format_wifi"

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( override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo, oldPosition: Player.PositionInfo,
newPosition: 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( override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo, oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo, newPosition: Player.PositionInfo,