77 Commits
3.3.2 ... 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
antonio
9c6981ed19 gradle: bump up code version 2023-07-18 12:45:03 +02:00
antonio
560ac2df68 fix: fixed search functionality 2023-07-18 10:37:17 +02:00
antonio
260e6e9daa style: temp - icon splash 2023-07-17 11:11:12 +02:00
antonio
38a1368c76 style: icon toolbar 2023-07-17 11:10:56 +02:00
antonio
9bf3139bd2 style: icon launcher 2023-07-17 11:10:44 +02:00
antonio
d389d1d62a style: icon launcher 2023-07-17 11:10:06 +02:00
antonio
e60c0e312c fix: null checking 2023-07-17 10:55:40 +02:00
CappielloAntonio
dc13beca49 Update github_release.yml 2023-07-12 10:16:27 +02:00
antonio
6399488819 Merge remote-tracking branch 'origin/main' 2023-07-12 10:04:45 +02:00
antonio
69c1b93d28 gradle: bump up code version 2023-07-12 10:04:36 +02:00
antonio
50c240793a gradle: update dependencies 2023-07-12 10:03:52 +02:00
CappielloAntonio
80f7783b7d Update github_release.yml 2023-07-12 10:03:17 +02:00
antonio
259c8b3599 refactor: added search2 method as per Subsonic API specifications 2023-07-12 09:52:57 +02:00
antonio
ba1295440b feat: added ReplayGain functionality in main flavor 2023-07-12 09:27:58 +02:00
antonio
9e6178037c feat: added ReplayGain functionality in main flavor 2023-07-03 09:27:35 +02:00
antonio
47302815c1 clean: code cleanup 2023-07-03 09:26:31 +02:00
antonio
c81bfe1457 fix: deleted an irrelevant minus sign 2023-07-03 09:24:20 +02:00
antonio
f413b5d498 feat: added initial version ReplayGain functionality, still in development 2023-07-02 23:38:33 +02:00
antonio
feae1b8bdd feat: added settings to enable and disable ReplayGain 2023-07-02 23:36:52 +02:00
antonio
57bb51fb8d feat: version number is now taken dynamically from the VersionName parameter 2023-07-02 19:59:03 +02:00
antonio
0f1364502a fix: modified list scrolling behavior 2023-07-02 19:55:05 +02:00
antonio
a508ec0af6 feat: make music directory section optional 2023-07-02 19:36:39 +02:00
antonio
a9b264004c fix: fixed directory navigation 2023-07-02 19:23:08 +02:00
antonio
803b61430f build: removed unused dependencies 2023-07-02 12:32:08 +02:00
antonio
25367512c0 build: removed unused dependency 2023-07-02 12:13:50 +02:00
antonio
e3720f4a50 build: disabled jetifier 2023-07-02 12:11:36 +02:00
antonio
1168cc58f4 clean: removed unused repositories 2023-07-02 12:09:37 +02:00
antonio
c711b387bb refactor: extracted common toolbar to better handle two flavors and minimized file duplication 2023-07-01 18:11:44 +02:00
antonio
a3fe0de233 feat: initial groundwork for creating the flavor 2023-07-01 11:37:36 +02:00
antonio
922153837d fix: tap the player bottom sheet header to bring player up 2023-06-30 18:34:33 +02:00
antonio
960dd6c76a fix: shuffle all action 2023-06-30 18:23:12 +02:00
antonio
fcb7135c3d fix: changed the translation label 2023-06-25 22:12:45 +02:00
antonio
3102bff729 gradle: bump up code version 2023-06-24 15:29:40 +02:00
antonio
992b16cc1d clean: explain user why to disable manufacturer optimizations and troubleshooting steps 2023-06-24 15:28:02 +02:00
antonio
2046872d79 gradle: bump up code version 2023-06-24 15:02:52 +02:00
antonio
7312ae20d1 fix: improved error handling during scanning phase 2023-06-24 15:02:13 +02:00
antonio
e8f2385928 gradle: bump up code version 2023-06-24 13:40:55 +02:00
antonio
204f9ff4c7 fix: addConverterFactory to retrofit client 2023-06-24 13:40:06 +02:00
antonio
17e64f3dd0 fix: cull checking 2023-06-24 13:38:45 +02:00
antonio
d09e7e7981 fix: collapse player bottom sheet on logout 2023-06-24 13:38:21 +02:00
antonio
95e9d06e47 gradle: bump up code version 2023-06-24 12:13:02 +02:00
antonio
f147c762d5 fix: reduced the frequency of the optimization prompt dialog and made it possible to disable it 2023-06-24 12:12:14 +02:00
antonio
200ce6555b Gradle: removed old search bar implementation and updated code version in preparation for the tag 2023-06-24 11:17:23 +02:00
antonio
b5e5537691 feat: Removed limit of 5 search suggestions 2023-06-24 11:16:05 +02:00
antonio
3b80725673 feat: replace old searchbar implementation with official material3 2023-06-24 11:15:04 +02:00
antonio
d7c5be834e Merge remote-tracking branch 'origin/main' 2023-06-23 18:28:31 +02:00
antonio
4a5672fc77 build: bump up code version 2023-06-23 18:28:16 +02:00
antonio
34fa2f456e fix: resolved potential issues related to missing or invalid prefixes 2023-06-23 18:23:44 +02:00
antonio
663285611b build: dependencies update 2023-06-23 09:29:10 +02:00
CappielloAntonio
a62cc78e18 Update README.md 2023-06-21 14:50:22 +02:00
126 changed files with 4467 additions and 855 deletions

View File

@@ -34,13 +34,13 @@ jobs:
- name: Build APK
id: build
run: bash ./gradlew assembleRelease
run: bash ./gradlew assembleTempoRelease
- name: Sign APK
id: sign_apk
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/release
releaseDirectory: app/build/outputs/apk/tempo/release
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -98,7 +98,7 @@ jobs:
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
asset_name: app-release.apk
asset_name: app-tempo-release.apk
asset_content_type: application/zip
# - name: Upload AAB

View File

@@ -1,5 +1,3 @@
<h1 align="center"> Tempo </h1>
<br>
<p align="center">
<img alt="Tempo" title="Tempo" src="mockup/svg/horizontal_logo.svg" width="250">
</p>

View File

@@ -7,11 +7,8 @@ android {
buildToolsVersion '33.0.0'
defaultConfig {
applicationId 'com.cappielloantonio.tempo'
minSdkVersion 24
targetSdkVersion 34
versionCode 5
versionName '3.3.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -25,6 +22,24 @@ android {
}
}
flavorDimensions "default"
productFlavors {
tempo {
dimension "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 14
versionName '3.4.7'
}
notquitemy {
dimension "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
versionCode 1
versionName "1.0.0"
}
}
buildTypes {
release {
shrinkResources true
@@ -54,51 +69,34 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// AndroidX
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.core:core-splashscreen:1.0.1'
// Google GMS
implementation 'com.google.android.gms:play-services-cast-framework:21.3.0'
// Android Material
implementation 'com.google.android.material:material:1.9.0'
// SearchBar
implementation 'com.paulrybitskyi.persistentsearchview:persistentsearchview:1.1.4'
implementation 'com.arthurivanets.adapster:adapster:1.0.13'
// Glide
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'com.github.bumptech.glide:annotations:4.15.1'
// Media3
implementation 'androidx.media3:media3-session:1.0.2'
implementation 'androidx.media3:media3-common:1.0.2'
implementation 'androidx.media3:media3-exoplayer:1.0.2'
implementation 'androidx.media3:media3-ui:1.0.2'
implementation 'androidx.media3:media3-cast:1.0.2'
implementation 'androidx.media3:media3-session:1.1.0'
implementation 'androidx.media3:media3-common:1.1.0'
implementation 'androidx.media3:media3-exoplayer:1.1.0'
implementation 'androidx.media3:media3-ui:1.1.0'
tempoImplementation 'androidx.media3:media3-cast:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
annotationProcessor 'androidx.room:room-compiler:2.5.1'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.6'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Crash Report
// debugImplementation 'com.balsikandar.android:crashreporter:1.1.0'
// DB debug
// debugImplementation 'com.amitshekhar.android:debug-db:1.0.6'
}

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

View File

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

@@ -12,8 +12,8 @@ import java.util.List;
@Dao
public interface RecentSearchDao {
@Query("SELECT * FROM recent_search GROUP BY search ORDER BY search DESC LIMIT :limit")
List<String> getRecent(int limit);
@Query("SELECT * FROM recent_search ORDER BY search DESC")
List<String> getRecent();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(RecentSearch search);

View File

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

View File

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

View File

@@ -0,0 +1,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

@@ -0,0 +1,9 @@
package com.cappielloantonio.tempo.model
import androidx.annotation.Keep
@Keep
data class ReplayGain(
var trackGain: Float = 0f,
var albumGain: Float = 0f,
)

View File

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

View File

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

View File

@@ -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>() {
@Override
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();
if (genreList == null || genreList.isEmpty()) {
genres.setValue(Collections.emptyList());
return;
}
if (random) {
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

@@ -17,8 +17,12 @@ public class ScanRepository {
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull retrofit2.Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null) {
if (response.body().getSubsonicResponse().getError() != null) {
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getMessage()));
} else if (response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
}
}
}
@@ -36,8 +40,12 @@ public class ScanRepository {
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull retrofit2.Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null) {
if (response.body().getSubsonicResponse().getError() != null) {
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getMessage()));
} else if (response.body().getSubsonicResponse().getScanStatus() != null) {
callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount());
}
}
}

View File

@@ -1,5 +1,7 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
@@ -11,6 +13,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
import java.util.ArrayList;
@@ -24,7 +27,28 @@ import retrofit2.Response;
public class SearchingRepository {
private final RecentSearchDao recentSearchDao = AppDatabase.getInstance().recentSearchDao();
public MutableLiveData<SearchResult3> search(String query) {
public MutableLiveData<SearchResult2> search2(String query) {
MutableLiveData<SearchResult2> result = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getSearchingClient()
.search3(query, 20, 20, 20)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
result.setValue(response.body().getSubsonicResponse().getSearchResult2());
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return result;
}
public MutableLiveData<SearchResult3> search3(String query) {
MutableLiveData<SearchResult3> result = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
@@ -78,6 +102,8 @@ public class SearchingRepository {
LinkedHashSet<String> hashSet = new LinkedHashSet<>(newSuggestions);
ArrayList<String> suggestionsWithoutDuplicates = new ArrayList<>(hashSet);
Log.d("suggestionsWithoutDuplicates", suggestionsWithoutDuplicates.toString());
suggestions.setValue(suggestionsWithoutDuplicates);
}
}
@@ -103,10 +129,10 @@ public class SearchingRepository {
thread.start();
}
public List<String> getRecentSearchSuggestion(int limit) {
public List<String> getRecentSearchSuggestion() {
List<String> recent = new ArrayList<>();
RecentThreadSafe suggestionsThread = new RecentThreadSafe(recentSearchDao, limit);
RecentThreadSafe suggestionsThread = new RecentThreadSafe(recentSearchDao);
Thread thread = new Thread(suggestionsThread);
thread.start();
@@ -152,17 +178,15 @@ public class SearchingRepository {
private static class RecentThreadSafe implements Runnable {
private final RecentSearchDao recentSearchDao;
private final int limit;
private List<String> recent = new ArrayList<>();
public RecentThreadSafe(RecentSearchDao recentSearchDao, int limit) {
public RecentThreadSafe(RecentSearchDao recentSearchDao) {
this.recentSearchDao = recentSearchDao;
this.limit = limit;
}
@Override
public void run() {
recent = recentSearchDao.getRecent(limit);
recent = recentSearchDao.getRecent();
}
public List<String> getRecent() {

View File

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

View File

@@ -22,7 +22,7 @@ public class SystemRepository {
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull retrofit2.Response<ApiResponse> response) {
if (response.body() != null) {
if (response.body().getSubsonicResponse().getStatus().equals(ResponseStatus.FAILED)) {
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getCode().getValue() + " - " + response.body().getSubsonicResponse().getError().getMessage()));
callback.onError(new Exception(response.body().getSubsonicResponse().getError().getCode() + " - " + response.body().getSubsonicResponse().getError().getMessage()));
} else if (response.body().getSubsonicResponse().getStatus().equals(ResponseStatus.OK)) {
String password = response.raw().request().url().queryParameter("p");
String token = response.raw().request().url().queryParameter("t");

View File

@@ -16,6 +16,7 @@ class RetrofitClient(subsonic: Subsonic) {
init {
retrofit = Retrofit.Builder()
.baseUrl(subsonic.url)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()))
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.client(getOkHttpClient())
.build()

View File

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

View File

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

View File

@@ -4,6 +4,6 @@ import androidx.annotation.Keep
@Keep
class Error {
var code: ErrorCode? = null
var code: Int? = null
var message: String? = null
}

View File

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

View File

@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.activity;
import android.content.Context;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
@@ -141,6 +142,10 @@ public class MainActivity extends BaseActivity {
handler.postDelayed(runnable, 100);
}
public void expandBottomSheet() {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
public void setBottomSheetDraggableState(Boolean isDraggable) {
bottomSheetBehavior.setDraggable(isDraggable);
}
@@ -247,6 +252,7 @@ public class MainActivity extends BaseActivity {
}
private void goToLogin() {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
setBottomNavigationBarVisibility(false);
setBottomSheetVisibility(false);
@@ -333,7 +339,9 @@ public class MainActivity extends BaseActivity {
private void checkConnectionType() {
if (Preferences.isWifiOnly()) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager.getActiveNetworkInfo().getType() != ConnectivityManager.TYPE_WIFI) {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.getType() != ConnectivityManager.TYPE_WIFI) {
ConnectionAlertDialog dialog = new ConnectionAlertDialog();
dialog.show(getSupportFragmentManager(), null);
}

View File

@@ -2,15 +2,12 @@ package com.cappielloantonio.tempo.ui.activity.base;
import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@@ -19,11 +16,11 @@ import androidx.media3.exoplayer.offline.DownloadService;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.service.DownloaderService;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.util.UIUtil;
import com.google.android.gms.cast.framework.CastContext;
import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog;
import com.cappielloantonio.tempo.util.Flavors;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.elevation.SurfaceColors;
import com.google.common.util.concurrent.ListenableFuture;
@@ -36,8 +33,10 @@ public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initializeCastContext();
Flavors.initializeCastContext(this);
initializeDownloader();
checkBatteryOptimization();
checkPermission();
}
@Override
@@ -47,13 +46,6 @@ public class BaseActivity extends AppCompatActivity {
initializeBrowser();
}
@Override
protected void onResume() {
super.onResume();
checkBatteryOptimization();
checkPermission();
}
@Override
protected void onStop() {
releaseBrowser();
@@ -61,7 +53,7 @@ public class BaseActivity extends AppCompatActivity {
}
private void checkBatteryOptimization() {
if (detectBatteryOptimization()) {
if (detectBatteryOptimization() && Preferences.askForOptimization()) {
showBatteryOptimizationDialog();
}
}
@@ -81,19 +73,8 @@ public class BaseActivity extends AppCompatActivity {
}
private void showBatteryOptimizationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.activity_battery_optimizations_summary)
.setTitle(R.string.activity_battery_optimizations_title)
.setNegativeButton(R.string.activity_negative_button, null)
.setPositiveButton(R.string.activity_neutral_button, (dialog, id) -> openPowerSettings())
.show();
}
private void openPowerSettings() {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(intent);
BatteryOptimizationDialog dialog = new BatteryOptimizationDialog();
dialog.show(getSupportFragmentManager(), null);
}
private void initializeBrowser() {
@@ -116,10 +97,6 @@ public class BaseActivity extends AppCompatActivity {
}
}
private void initializeCastContext() {
if (UIUtil.isCastApiAvailable(this)) CastContext.getSharedInstance(this);
}
private void setNavigationBarColor() {
getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 10));
}

View File

@@ -76,7 +76,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
Bundle bundle = new Bundle();
if (children.get(getBindingAdapterPosition()).isDir()) {
bundle.putParcelable(Constants.MUSIC_DIRECTORY_OBJECT, children.get(getBindingAdapterPosition()));
bundle.putString(Constants.MUSIC_DIRECTORY_ID, children.get(getBindingAdapterPosition()).getId());
click.onMusicDirectoryClick(bundle);
} else {
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(children));

View File

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

View File

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

View File

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

View File

@@ -76,7 +76,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
}
public void setItems(List<Child> songs) {
this.songs = songs;
this.songs = songs != null ? songs : Collections.emptyList();
notifyDataSetChanged();
}

View File

@@ -0,0 +1,57 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.DialogFragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogBatteryOptimizationBinding;
import com.cappielloantonio.tempo.util.Preferences;
@OptIn(markerClass = UnstableApi.class)
public class BatteryOptimizationDialog extends DialogFragment {
private static final String TAG = "BatteryOptimizationDialog";
private DialogBatteryOptimizationBinding bind;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogBatteryOptimizationBinding.inflate(getLayoutInflater());
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(bind.getRoot())
.setTitle(R.string.activity_battery_optimizations_title)
.setNeutralButton(R.string.battery_optimization_neutral_button, (dialog, id) -> Preferences.dontAskForOptimization())
.setNegativeButton(R.string.battery_optimization_negative_button, null)
.setPositiveButton(R.string.battery_optimization_positive_button, (dialog, id) -> openPowerSettings());
AlertDialog popup = builder.create();
popup.setCancelable(false);
popup.setCanceledOnTouchOutside(false);
return popup;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void openPowerSettings() {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(intent);
}
}

View File

@@ -116,6 +116,11 @@ public class ServerSignupDialog extends DialogFragment {
return false;
}
if (!server.matches("^https?://(.*)")) {
bind.serverTextView.setError(getString(R.string.error_server_prefix));
return false;
}
return true;
}

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R;
@@ -20,17 +21,12 @@ import com.cappielloantonio.tempo.databinding.FragmentDirectoryBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Artist;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.Objects;
@UnstableApi
public class DirectoryFragment extends Fragment implements ClickCallback {
private static final String TAG = "DirectoryFragment";
@@ -52,9 +48,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
initAppBar();
initButtons();
initDirectoryListView();
init();
return view;
}
@@ -77,17 +71,6 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
bind = null;
}
private void init() {
Artist artist = getArguments().getParcelable(Constants.MUSIC_INDEX_OBJECT);
if (artist != null) {
directoryViewModel.setMusicDirectoryId(artist.getId());
directoryViewModel.setMusicDirectoryName(artist.getName());
}
directoryViewModel.loadMusicDirectory(getViewLifecycleOwner());
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
@@ -96,39 +79,10 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (bind != null)
if (bind != null) {
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
if (bind != null)
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.directoryInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
bind.toolbar.setTitle(directory.getName());
}
});
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
bind.directoryTitleLabel.setText(directory.getName());
}
});
}
private void initButtons() {
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null && directory.getParentId() != null && !Objects.equals(directory.getParentId(), "-1")) {
bind.directoryBackImageView.setVisibility(View.VISIBLE);
} else {
bind.directoryBackImageView.setVisibility(View.GONE);
}
});
bind.directoryBackImageView.setOnClickListener(v -> directoryViewModel.goBack());
bind.directoryBackImageView.setOnClickListener(v -> activity.navController.navigateUp());
}
}
private void initDirectoryListView() {
@@ -137,13 +91,18 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
musicDirectoryAdapter = new MusicDirectoryAdapter(this);
bind.directoryRecyclerView.setAdapter(musicDirectoryAdapter);
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.directoryInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
bind.toolbar.setTitle(directory.getName());
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
musicDirectoryAdapter.setItems(directory.getChildren());
} else {
musicDirectoryAdapter.setItems(Collections.emptyList());
}
bind.directoryTitleLabel.setText(directory.getName());
musicDirectoryAdapter.setItems(directory.getChildren());
});
}
@@ -162,11 +121,6 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
@Override
public void onMusicDirectoryClick(Bundle bundle) {
Child child = bundle.getParcelable(Constants.MUSIC_DIRECTORY_OBJECT);
if (child != null) {
directoryViewModel.setMusicDirectoryId(child.getId());
directoryViewModel.setMusicDirectoryName(child.getTitle());
}
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
}
}

View File

@@ -3,9 +3,6 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -28,7 +25,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Objects;
@@ -43,18 +40,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
private MaterialToolbar materialToolbar;
@Nullable
@Override
@@ -97,22 +83,11 @@ public class DownloadFragment extends Fragment implements ClickCallback {
bind = null;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.action_downloadFragment_to_searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.action_downloadFragment_to_settingsFragment);
return true;
}
return false;
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
materialToolbar = bind.getRoot().findViewById(R.id.toolbar);
activity.setSupportActionBar(materialToolbar);
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
}
private void initDownloadedSongView() {

View File

@@ -2,9 +2,6 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -18,7 +15,9 @@ import com.cappielloantonio.tempo.databinding.FragmentHomeBinding;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.fragment.pager.HomePager;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.Objects;
@@ -30,18 +29,9 @@ public class HomeFragment extends Fragment {
private FragmentHomeBinding bind;
private MainActivity activity;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
private MaterialToolbar materialToolbar;
private AppBarLayout appBarLayout;
private TabLayout tabLayout;
@Nullable
@Override
@@ -73,22 +63,18 @@ public class HomeFragment extends Fragment {
bind = null;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.action_homeFragment_to_searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.action_homeFragment_to_settingsFragment);
return true;
}
return false;
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
appBarLayout = bind.getRoot().findViewById(R.id.toolbar_fragment);
materialToolbar = bind.getRoot().findViewById(R.id.toolbar);
activity.setSupportActionBar(materialToolbar);
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
tabLayout = new TabLayout(requireContext());
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.setTabMode(TabLayout.MODE_FIXED);
appBarLayout.addView(tabLayout);
}
private void initHomePager() {
@@ -106,13 +92,13 @@ public class HomeFragment extends Fragment {
bind.homeViewPager.setOffscreenPageLimit(3);
bind.homeViewPager.setUserInputEnabled(false);
new TabLayoutMediator(bind.homeTabLayout, bind.homeViewPager,
new TabLayoutMediator(tabLayout, bind.homeViewPager,
(tab, position) -> {
tab.setText(pager.getPageTitle(position));
// tab.setIcon(pager.getPageIcon(position));
}
).attach();
bind.homeTabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE);
tabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE);
}
}

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -138,6 +139,15 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
return true;
});
bind.discoveryTextViewClickable.setOnClickListener(v -> {
homeViewModel.getRandomShuffleSample().observe(getViewLifecycleOwner(), songs -> {
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
}
});
});
bind.similarTracksTextViewRefreshable.setOnLongClickListener(v -> {
homeViewModel.refreshSimilarSongSample(getViewLifecycleOwner());
return true;

View File

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

View File

@@ -2,9 +2,6 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -29,8 +26,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicFolderAdapter;
import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.LibraryViewModel;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.material.appbar.MaterialToolbar;
import java.util.Objects;
@@ -46,21 +44,9 @@ public class LibraryFragment extends Fragment implements ClickCallback {
private AlbumAdapter albumAdapter;
private ArtistAdapter artistAdapter;
private GenreAdapter genreAdapter;
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
private MaterialToolbar materialToolbar;
@Nullable
@Override
@@ -100,19 +86,6 @@ public class LibraryFragment extends Fragment implements ClickCallback {
bind = null;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.action_libraryFragment_to_searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.action_libraryFragment_to_settingsFragment);
return true;
}
return false;
}
private void init() {
bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment));
bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment));
@@ -142,11 +115,18 @@ public class LibraryFragment extends Fragment implements ClickCallback {
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
materialToolbar = bind.getRoot().findViewById(R.id.toolbar);
activity.setSupportActionBar(materialToolbar);
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
}
private void initMusicFolderView() {
if (!Preferences.isMusicDirectorySectionVisible()) {
bind.libraryMusicFolderSector.setVisibility(View.GONE);
return;
}
bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.musicFolderRecyclerView.setHasFixedSize(true);

View File

@@ -27,6 +27,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerVerticalPager;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
@@ -57,6 +58,7 @@ public class PlayerBottomSheetFragment extends Fragment {
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
customizeBottomSheetBackground();
customizeBottomSheetAction();
initViewPager();
setHeaderBookmarksButton();
@@ -87,6 +89,10 @@ public class PlayerBottomSheetFragment extends Fragment {
bind.playerHeaderLayout.getRoot().setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 2));
}
private void customizeBottomSheetAction() {
bind.playerHeaderLayout.getRoot().setOnClickListener(view -> ((MainActivity) requireActivity()).expandBottomSheet());
}
private void initViewPager() {
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setAdapter(new PlayerControllerVerticalPager(this));

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
@@ -51,6 +52,7 @@ public class PlayerControllerFragment extends Fragment {
private Chip playerMediaExtension;
private TextView playerMediaBitrate;
private ImageView playerMediaTranscodingIcon;
private ImageView playerMediaTranscodingPriorityIcon;
private Chip playerMediaTranscodedExtension;
private TextView playerMediaTranscodedBitrate;
@@ -71,6 +73,7 @@ public class PlayerControllerFragment extends Fragment {
initCoverLyricsSlideView();
initMediaListenable();
initArtistLabelButton();
initTranscodingInfo();
return view;
}
@@ -104,6 +107,7 @@ public class PlayerControllerFragment extends Fragment {
playerMediaExtension = bind.getRoot().findViewById(R.id.player_media_extension);
playerMediaBitrate = bind.getRoot().findViewById(R.id.player_media_bitrate);
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);
playerMediaTranscodedBitrate = bind.getRoot().findViewById(R.id.player_media_transcoded_bitrate);
}
@@ -166,7 +170,6 @@ public class PlayerControllerFragment extends Fragment {
playerMediaBitrate.setVisibility(View.GONE);
} else {
playerMediaBitrate.setVisibility(View.VISIBLE);
playerMediaBitrate.setText(bitrate);
}
}
@@ -177,19 +180,28 @@ public class PlayerControllerFragment extends Fragment {
: "Original";
if (transcodingExtension.equals("raw") && transcodingBitrate.equals("Original")) {
playerMediaTranscodingPriorityIcon.setVisibility(View.GONE);
playerMediaTranscodingIcon.setVisibility(View.GONE);
playerMediaTranscodedBitrate.setVisibility(View.GONE);
playerMediaTranscodedExtension.setVisibility(View.GONE);
} else {
playerMediaTranscodingPriorityIcon.setVisibility(View.GONE);
playerMediaTranscodingIcon.setVisibility(View.VISIBLE);
playerMediaTranscodedBitrate.setVisibility(View.VISIBLE);
playerMediaTranscodedExtension.setVisibility(View.VISIBLE);
playerMediaTranscodedExtension.setText(transcodingExtension);
playerMediaTranscodedBitrate.setText(transcodingBitrate);
}
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);
playerMediaTranscodedBitrate.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) {
playbackSpeedButton.setOnClickListener(view -> {
float currentSpeed = Preferences.getPlaybackSpeed();

View File

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

View File

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

View File

@@ -2,9 +2,15 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -28,17 +34,15 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.paulrybitskyi.persistentsearchview.adapters.model.SuggestionItem;
import com.paulrybitskyi.persistentsearchview.listeners.OnSuggestionChangeListener;
import com.paulrybitskyi.persistentsearchview.utils.SuggestionCreationUtil;
import java.util.Collections;
@UnstableApi
public class SearchFragment extends Fragment implements ClickCallback {
private static final String TAG = "SearchFragment";
private FragmentSearchBinding bind;
private MainActivity activity;
private SearchViewModel searchViewModel;
@@ -60,6 +64,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
initSearchResultView();
initSearchView();
inputFocus();
return view;
}
@@ -70,12 +75,6 @@ public class SearchFragment extends Fragment implements ClickCallback {
initializeMediaBrowser();
}
@Override
public void onResume() {
super.onResume();
inputFocus();
}
@Override
public void onStop() {
releaseMediaBrowser();
@@ -118,69 +117,100 @@ public class SearchFragment extends Fragment implements ClickCallback {
}
private void initSearchView() {
if (isQueryValid(searchViewModel.getQuery())) {
search(searchViewModel.getQuery());
}
setRecentSuggestions();
bind.persistentSearchView.setInputQuery(searchViewModel.getQuery());
setSuggestions();
bind.searchView
.getEditText()
.setOnEditorActionListener((textView, actionId, keyEvent) -> {
String query = bind.searchView.getText().toString();
bind.persistentSearchView.setOnSearchQueryChangeListener((searchView, oldQuery, newQuery) -> {
if (!newQuery.trim().equals("") && newQuery.trim().length() > 1) {
searchViewModel.getSearchSuggestion(newQuery).observe(getViewLifecycleOwner(), suggestions -> searchView.setSuggestions(SuggestionCreationUtil.asRegularSearchSuggestions(MusicUtil.getReadableStrings(suggestions)), false));
} else {
setSuggestions();
}
});
if (isQueryValid(query)) {
search(query);
return true;
}
bind.persistentSearchView.setOnSuggestionChangeListener(new OnSuggestionChangeListener() {
@Override
public void onSuggestionPicked(SuggestionItem suggestion) {
search(suggestion.getItemModel().getText());
}
return false;
});
@Override
public void onSuggestionRemoved(SuggestionItem suggestion) {
}
});
bind.searchView
.getEditText()
.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
bind.persistentSearchView.setOnSearchConfirmedListener((searchView, query) -> {
if (isQueryValid(query)) {
searchView.collapse();
search(query);
} else {
Toast.makeText(requireContext(), getString(R.string.search_info_minimum_characters), Toast.LENGTH_SHORT).show();
}
});
}
bind.persistentSearchView.setOnSuggestionChangeListener(new OnSuggestionChangeListener() {
@Override
public void onSuggestionPicked(SuggestionItem suggestion) {
search(suggestion.getItemModel().getText());
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if (start + count > 1) {
setSearchSuggestions(charSequence.toString());
} else {
setRecentSuggestions();
}
}
@Override
public void onSuggestionRemoved(SuggestionItem suggestion) {
searchViewModel.deleteRecentSearch(suggestion.getItemModel().getText());
}
});
@Override
public void afterTextChanged(Editable editable) {
bind.persistentSearchView.setOnClearInputBtnClickListener(v -> searchViewModel.setQuery(""));
}
});
}
private void setSuggestions() {
bind.persistentSearchView.setSuggestions(SuggestionCreationUtil.asRecentSearchSuggestions(searchViewModel.getRecentSearchSuggestion()), false);
public void setRecentSuggestions() {
bind.searchViewSuggestionContainer.removeAllViews();
for (String suggestion : searchViewModel.getRecentSearchSuggestion()) {
View view = LayoutInflater.from(bind.searchViewSuggestionContainer.getContext()).inflate(R.layout.item_search_suggestion, bind.searchViewSuggestionContainer, false);
ImageView leadingImageView = view.findViewById(R.id.search_suggestion_icon);
TextView titleView = view.findViewById(R.id.search_suggestion_title);
ImageView tailingImageView = view.findViewById(R.id.search_suggestion_delete_icon);
leadingImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_history, null));
titleView.setText(suggestion);
view.setOnClickListener(v -> search(suggestion));
tailingImageView.setOnClickListener(v -> {
searchViewModel.deleteRecentSearch(suggestion);
setRecentSuggestions();
});
bind.searchViewSuggestionContainer.addView(view);
}
}
public void setSearchSuggestions(String query) {
searchViewModel.getSearchSuggestion(query).observe(getViewLifecycleOwner(), suggestions -> {
bind.searchViewSuggestionContainer.removeAllViews();
for (String suggestion : suggestions) {
View view = LayoutInflater.from(bind.searchViewSuggestionContainer.getContext()).inflate(R.layout.item_search_suggestion, bind.searchViewSuggestionContainer, false);
ImageView leadingImageView = view.findViewById(R.id.search_suggestion_icon);
TextView titleView = view.findViewById(R.id.search_suggestion_title);
ImageView tailingImageView = view.findViewById(R.id.search_suggestion_delete_icon);
leadingImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_search, null));
titleView.setText(suggestion);
tailingImageView.setVisibility(View.GONE);
view.setOnClickListener(v -> search(suggestion));
bind.searchViewSuggestionContainer.addView(view);
}
});
}
public void search(String query) {
searchViewModel.setQuery(query);
bind.persistentSearchView.setInputQuery(query);
bind.searchBar.setText(query);
bind.searchView.hide();
performSearch(query);
}
private void performSearch(String query) {
searchViewModel.search(query).observe(getViewLifecycleOwner(), result -> {
searchViewModel.search3(query).observe(getViewLifecycleOwner(), result -> {
if (bind != null) {
if (result.getArtists() != null) {
bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE);
@@ -212,11 +242,12 @@ public class SearchFragment extends Fragment implements ClickCallback {
}
private boolean isQueryValid(String query) {
Log.d(TAG, "isQueryValid()");
return !query.equals("") && query.trim().length() > 2;
}
private void inputFocus() {
bind.persistentSearchView.expand();
bind.searchView.show();
}
private void initializeMediaBrowser() {

View File

@@ -15,8 +15,10 @@ import androidx.annotation.OptIn;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.helper.ThemeHelper;
import com.cappielloantonio.tempo.interfaces.ScanCallback;
@@ -69,6 +71,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
public void onResume() {
super.onResume();
checkEqualizer();
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
findPreference("logout").setOnPreferenceClickListener(preference -> {
activity.quit();
return true;
@@ -90,12 +96,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
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) -> {
if (newValue instanceof Boolean) {
if ((Boolean) newValue) {
@@ -127,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() {
settingViewModel.getScanStatus(new ScanCallback() {
@Override

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
artistSong.setText(MusicUtil.getReadableString(songBottomSheetViewModel.getSong().getArtist()));
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(Boolean.TRUE.equals(songBottomSheetViewModel.getSong().getStarred()));
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
songBottomSheetViewModel.setFavorite(requireContext());
dismissBottomSheet();

View File

@@ -17,6 +17,7 @@ object Constants {
const val MUSIC_FOLDER_OBJECT = "MUSIC_FOLDER_OBJECT"
const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT"
const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT"
const val MUSIC_DIRECTORY_ID = "MUSIC_DIRECTORY_ID"
const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED"
const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED"
@@ -43,6 +44,9 @@ object Constants {
const val PLAYLIST_ORDER_BY_NAME = "ORDER_BY_NAME"
const val PLAYLIST_ORDER_BY_RANDOM = "ORDER_BY_RANDOM"
const val PODCAST_FILTER_BY_DOWNLOAD = "PODCAST_FILTER_BY_DOWNLOAD"
const val PODCAST_FILTER_BY_ALL = "PODCAST_FILTER_BY_ALL"
const val MEDIA_TYPE_MUSIC = "music"
const val MEDIA_TYPE_PODCAST = "podcast"
const val MEDIA_TYPE_AUDIOBOOK = "audiobook"

View File

@@ -41,8 +41,11 @@ public class MusicUtil {
if (params.containsKey("c") && params.get("c") != null)
uri.append("&c=").append(params.get("c"));
uri.append("&maxBitRate=").append(getBitratePreference());
uri.append("&format=").append(getTranscodingFormatPreference());
if (!Preferences.isServerPrioritized())
uri.append("&maxBitRate=").append(getBitratePreference());
if (!Preferences.isServerPrioritized())
uri.append("&format=").append(getTranscodingFormatPreference());
uri.append("&id=").append(id);
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

@@ -10,6 +10,7 @@ object Preferences {
private const val TOKEN = "token"
private const val SALT = "salt"
private const val LOW_SECURITY = "low_security"
private const val BATTERY_OPTIMIZATION = "battery_optimization"
private const val SERVER_ID = "server_id"
private const val PLAYBACK_SPEED = "playback_speed"
private const val SKIP_SILENCE = "skip_silence"
@@ -29,6 +30,9 @@ object Preferences {
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
private const val PODCAST_SECTION_VISIBILITY = "podcast_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 REPLAY_GAIN_MODE = "replay_gain_mode"
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
@JvmStatic
fun getServer(): String? {
@@ -100,6 +104,16 @@ object Preferences {
App.getInstance().preferences.edit().putString(SERVER_ID, serverId).apply()
}
@JvmStatic
fun askForOptimization(): Boolean {
return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true)
}
@JvmStatic
fun dontAskForOptimization() {
App.getInstance().preferences.edit().putBoolean(BATTERY_OPTIMIZATION, false).apply()
}
@JvmStatic
fun getPlaybackSpeed(): Float {
return App.getInstance().preferences.getFloat(PLAYBACK_SPEED, 1f)
@@ -229,4 +243,19 @@ object Preferences {
fun setRadioSectionHidden() {
App.getInstance().preferences.edit().putBoolean(RADIO_SECTION_VISIBILITY, false).apply()
}
@JvmStatic
fun isMusicDirectorySectionVisible(): Boolean {
return App.getInstance().preferences.getBoolean(MUSIC_DIRECTORY_SECTION_VISIBILITY, true)
}
@JvmStatic
fun getReplayGainMode(): String? {
return App.getInstance().preferences.getString(REPLAY_GAIN_MODE, "disabled")
}
@JvmStatic
fun isServerPrioritized(): Boolean {
return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_PRIORITY, false)
}
}

View File

@@ -0,0 +1,120 @@
package com.cappielloantonio.tempo.util;
import androidx.annotation.OptIn;
import androidx.media3.common.Metadata;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import com.cappielloantonio.tempo.model.ReplayGain;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@OptIn(markerClass = UnstableApi.class)
public class ReplayGainUtil {
private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"};
public static void setReplayGain(ExoPlayer player, Tracks tracks) {
List<Metadata> metadata = getMetadata(tracks);
List<ReplayGain> gains = getReplayGains(metadata);
applyReplayGain(player, gains);
}
private static List<Metadata> getMetadata(Tracks tracks) {
List<Metadata> metadata = new ArrayList<>();
if (tracks != null && !tracks.getGroups().isEmpty()) {
for (int i = 0; i < tracks.getGroups().size(); i++) {
Tracks.Group group = tracks.getGroups().get(i);
if (group != null && group.getMediaTrackGroup() != null) {
for (int j = 0; j < group.getMediaTrackGroup().length; j++) {
metadata.add(group.getTrackFormat(j).metadata);
}
}
}
}
return metadata;
}
private static List<ReplayGain> getReplayGains(List<Metadata> metadata) {
List<ReplayGain> gains = new ArrayList<>();
if (metadata != null) {
for (int i = 0; i < metadata.size(); i++) {
Metadata singleMetadata = metadata.get(i);
if (singleMetadata != null) {
for (int j = 0; j < singleMetadata.length(); j++) {
Metadata.Entry entry = singleMetadata.get(j);
if (checkReplayGain(entry)) {
ReplayGain replayGain = setReplayGains(entry);
gains.add(replayGain);
}
}
}
}
}
return gains;
}
private static boolean checkReplayGain(Metadata.Entry entry) {
for (String tag : tags) {
if (entry.toString().contains(tag)) {
return true;
}
}
return false;
}
private static ReplayGain setReplayGains(Metadata.Entry entry) {
ReplayGain replayGain = new ReplayGain();
if (entry.toString().contains(tags[0])) {
replayGain.setTrackGain(parseReplayGainTag(entry));
}
if (entry.toString().contains(tags[1])) {
replayGain.setAlbumGain(parseReplayGainTag(entry));
}
if (entry.toString().contains(tags[2])) {
replayGain.setTrackGain(parseReplayGainTag(entry) / 256f);
}
if (entry.toString().contains(tags[3])) {
replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f);
}
return replayGain;
}
private static Float parseReplayGainTag(Metadata.Entry entry) {
try {
return Float.parseFloat(entry.toString().replaceAll("[^\\d.]", ""));
} catch (NumberFormatException exception) {
return 0f;
}
}
private static void applyReplayGain(ExoPlayer player, List<ReplayGain> gains) {
if (Objects.equals(Preferences.getReplayGainMode(), "disabled") || gains.size() == 0) {
setReplayGain(player, 0f);
} else if (Objects.equals(Preferences.getReplayGainMode(), "track")) {
setReplayGain(player, gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(0).getAlbumGain());
} else if (Objects.equals(Preferences.getReplayGainMode(), "album")) {
setReplayGain(player, gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(0).getTrackGain());
}
}
private static void setReplayGain(ExoPlayer player, float gain) {
player.setVolume((float) Math.pow(10f, gain / 20f));
}
}

View File

@@ -7,9 +7,6 @@ import android.graphics.drawable.InsetDrawable;
import androidx.recyclerview.widget.DividerItemDecoration;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
public class UIUtil {
public static int getSpanCount(int itemCount, int maxSpan) {
int itemSize = itemCount == 0 ? 1 : itemCount;
@@ -21,10 +18,6 @@ public class UIUtil {
}
}
public static boolean isCastApiAvailable(Context context) {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
}
public static DividerItemDecoration getDividerItemDecoration(Context context) {
int[] ATTRS = new int[]{android.R.attr.listDivider};

View File

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

View File

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

View File

@@ -4,9 +4,7 @@ import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.repository.DirectoryRepository;
import com.cappielloantonio.tempo.subsonic.models.Directory;
@@ -14,34 +12,13 @@ import com.cappielloantonio.tempo.subsonic.models.Directory;
public class DirectoryViewModel extends AndroidViewModel {
private final DirectoryRepository directoryRepository;
private MutableLiveData<String> id = new MutableLiveData<>(null);
private MutableLiveData<String> name = new MutableLiveData<>(null);
private MutableLiveData<Directory> directory = new MutableLiveData<>(null);
public DirectoryViewModel(@NonNull Application application) {
super(application);
directoryRepository = new DirectoryRepository();
}
public LiveData<Directory> getDirectory() {
return directory;
}
public void setMusicDirectoryId(String id) {
this.id.setValue(id);
}
public void setMusicDirectoryName(String name) {
this.name.setValue(name);
}
public void loadMusicDirectory(LifecycleOwner owner) {
this.id.observe(owner, id -> directoryRepository.getMusicDirectory(id).observe(owner, directory -> this.directory.setValue(directory)));
}
public void goBack() {
this.id.setValue(this.directory.getValue().getParentId());
public LiveData<Directory> loadMusicDirectory(String id) {
return directoryRepository.getMusicDirectory(id);
}
}

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.repository.SearchingRepository;
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
import java.util.ArrayList;
@@ -38,8 +39,12 @@ public class SearchViewModel extends AndroidViewModel {
}
}
public LiveData<SearchResult3> search(String title) {
return searchingRepository.search(title);
public LiveData<SearchResult2> search2(String title) {
return searchingRepository.search2(title);
}
public LiveData<SearchResult3> search3(String title) {
return searchingRepository.search3(title);
}
public void insertNewSearch(String search) {
@@ -56,7 +61,7 @@ public class SearchViewModel extends AndroidViewModel {
public List<String> getRecentSearchSuggestion() {
ArrayList<String> suggestions = new ArrayList<>();
suggestions.addAll(searchingRepository.getRecentSearchSuggestion(5));
suggestions.addAll(searchingRepository.getRecentSearchSuggestion());
return suggestions;
}

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>
android:fillColor="@color/titleTextColor"
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
</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,840Q342,840 239.5,748.5Q137,657 122,520L204,520Q218,624 296.5,692Q375,760 480,760Q597,760 678.5,678.5Q760,597 760,480Q760,363 678.5,281.5Q597,200 480,200Q411,200 351,232Q291,264 250,320L360,320L360,400L120,400L120,160L200,160L200,254Q251,190 324.5,155Q398,120 480,120Q555,120 620.5,148.5Q686,177 734.5,225.5Q783,274 811.5,339.5Q840,405 840,480Q840,555 811.5,620.5Q783,686 734.5,734.5Q686,783 620.5,811.5Q555,840 480,840ZM592,648L440,496L440,280L520,280L520,464L648,592L592,648Z"/>
</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="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

@@ -0,0 +1,25 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/battery_optimization_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/activity_battery_optimizations_summary" />
<TextView
android:id="@+id/battery_optimization_secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:autoLink="web"
android:text="@string/activity_battery_optimizations_conclusion" />
</LinearLayout>

View File

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

View File

@@ -188,6 +188,7 @@
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:clipToPadding="false"
android:paddingTop="8dp" />
</LinearLayout>

View File

@@ -4,40 +4,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
<fragment
android:id="@+id/toolbar_fragment"
android:name="com.cappielloantonio.tempo.ui.fragment.ToolbarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
android:layout_height="wrap_content" />
<ProgressBar
android:id="@+id/loading_progress_bar"
@@ -121,6 +92,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingBottom="8dp" />
</LinearLayout>

View File

@@ -6,47 +6,11 @@
android:layout_height="match_parent"
tools:context=".ui.fragment.HomeFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
<fragment
android:id="@+id/toolbar_fragment"
android:name="com.cappielloantonio.tempo.ui.fragment.ToolbarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/homeTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout>
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/homeViewPager"

View File

@@ -113,16 +113,35 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/discovery_text_view_refreshable"
style="@style/TitleLarge"
<!-- Label and button -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp"
android:paddingStart="16dp"
android:paddingStart="8dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_title_discovery" />
android:paddingEnd="8dp">
<TextView
android:id="@+id/discovery_text_view_refreshable"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/home_title_discovery" />
<TextView
android:id="@+id/discovery_text_view_clickable"
style="@style/TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/home_title_discovery_shuffle_all_button" />
</LinearLayout>
<!-- slideview -->
<androidx.viewpager2.widget.ViewPager2

View File

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

View File

@@ -4,40 +4,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
<fragment
android:id="@+id/toolbar_fragment"
android:name="com.cappielloantonio.tempo.ui.fragment.ToolbarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_library_nested_scroll_view"
@@ -75,6 +46,7 @@
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingTop="8dp"
@@ -308,6 +280,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />

View File

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

View File

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

View File

@@ -1,54 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.paulrybitskyi.persistentsearchview.PersistentSearchView
android:id="@+id/persistentSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
app:areSuggestionsDisabled="false"
app:cardBackgroundColor="?attr/colorSurface"
app:cardCornerRadius="0dp"
app:cardElevation="0dp"
app:clearInputButtonDrawable="@drawable/ic_close"
app:dividerColor="@color/dividerColor"
app:isClearInputButtonEnabled="true"
app:isDismissableOnTouchOutside="true"
app:isProgressBarEnabled="true"
app:isVoiceInputButtonEnabled="false"
app:leftButtonDrawable="@drawable/ic_search"
app:progressBarColor="?attr/colorOnSurface"
app:queryInputBarIconColor="@color/searchPlaceholderColor"
app:queryInputCursorColor="?attr/colorOnSurface"
app:queryInputHint="@string/search_hint"
app:queryInputHintColor="@color/searchPlaceholderColor"
app:queryInputTextColor="@color/searchPlaceholderColor"
app:shouldDimBehind="false"
app:suggestionRecentSearchIconColor="@color/searchColor"
app:suggestionSearchSuggestionIconColor="@color/searchColor"
app:suggestionSelectedTextColor="?attr/colorPrimary"
app:suggestionTextColor="@color/searchColor"
app:suggestionIconColor="@color/searchColor" />
<View
android:id="@+id/search_bar_divider"
android:layout_width="match_parent"
android:layout_height="0.75dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_below="@+id/persistentSearchView"
android:background="?attr/colorSurfaceVariant" />
<androidx.core.widget.NestedScrollView
android:id="@+id/search_result_nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/search_bar_divider">
android:layout_height="match_parent"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
<!-- Search result -->
<LinearLayout
@@ -56,6 +16,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="64dp"
android:paddingBottom="@dimen/global_padding_bottom">
<!-- Artist -->
@@ -150,4 +111,39 @@
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_hint">
</com.google.android.material.search.SearchBar>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/search_hint"
app:layout_anchor="@id/search_bar">
<!-- Content goes here (ScrollView, RecyclerView, etc.). -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:paddingBottom="@dimen/global_padding_bottom">
<LinearLayout
android:id="@+id/search_view_suggestion_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</ScrollView>
</com.google.android.material.search.SearchView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -41,6 +41,13 @@
android:src="@drawable/ic_transcode"
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
android:id="@+id/player_media_transcoded_extension"
style="@style/Widget.Material3.Chip.Suggestion"

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp">
<ImageView
android:id="@+id/search_suggestion_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_history"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/search_suggestion_title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="12dp"
android:text="@string/label_placeholder"
android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintBottom_toBottomOf="@id/search_suggestion_icon"
app:layout_constraintEnd_toStartOf="@id/search_suggestion_delete_icon"
app:layout_constraintStart_toEndOf="@id/search_suggestion_icon"
app:layout_constraintTop_toTopOf="@id/search_suggestion_icon" />
<ImageView
android:id="@+id/search_suggestion_delete_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="@id/search_suggestion_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/search_suggestion_title"
app:layout_constraintTop_toTopOf="@id/search_suggestion_icon"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

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,36 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>

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

@@ -286,13 +286,16 @@
android:id="@+id/directoryFragment"
android:name="com.cappielloantonio.tempo.ui.fragment.DirectoryFragment"
android:label="DirectoryFragment"
tools:layout="@layout/fragment_directory" />
tools:layout="@layout/fragment_directory">
<action
android:id="@+id/action_directoryFragment_to_directoryFragment"
app:destination="@id/directoryFragment" />
</fragment>
<fragment
android:id="@+id/indexFragment"
android:name="com.cappielloantonio.tempo.ui.fragment.IndexFragment"
android:label="IndexFragment"
tools:layout="@layout/fragment_index">
<action
android:id="@+id/action_indexFragment_to_directoryFragment"
app:destination="@id/directoryFragment" />

View File

@@ -141,4 +141,15 @@
<item>12</item>
<item>6</item>
</string-array>
<string-array name="replay_gain_titles">
<item>Disabled</item>
<item>Track preferred</item>
<item>Album preferred</item>
</string-array>
<string-array name="replay_gain_values">
<item>disabled</item>
<item>track</item>
<item>album</item>
</string-array>
</resources>

View File

@@ -1,9 +1,11 @@
<resources>
<string name="activity_battery_optimizations_summary">Please disable battery optimizations for media playback while the screen is off.</string>
<string name="activity_battery_optimizations_conclusion">If in trouble visit https://dontkillmyapp.com. It provides detailed instructions on how to disable any power-saving features that may affect app\'s performance.</string>
<string name="activity_battery_optimizations_title">Battery Optimizations</string>
<string name="activity_info_offline_mode">Offline mode</string>
<string name="activity_negative_button">Ignore</string>
<string name="activity_neutral_button">Disable</string>
<string name="battery_optimization_negative_button">Ignore</string>
<string name="battery_optimization_positive_button">Disable</string>
<string name="battery_optimization_neutral_button">Don\'t ask again</string>
<string name="album_bottom_sheet_add_to_queue">Add to queue</string>
<string name="album_bottom_sheet_download_all">Download all</string>
<string name="album_bottom_sheet_go_to_artist">Go to artist</string>
@@ -52,6 +54,7 @@
<string name="download_title_section">Downloads</string>
<string name="empty_string" />
<string name="error_required">Required</string>
<string name="error_server_prefix">http or https prefix required</string>
<string name="exo_download_notification_channel_name">Downloads</string>
<string name="filter_info_selection">Select two or more filters</string>
<string name="filter_title">Filter</string>
@@ -74,6 +77,7 @@
<string name="home_title_discovery">Discovery</string>
<string name="home_title_recently_added">Recently added</string>
<string name="home_title_recently_added_see_all_button">See all</string>
<string name="home_title_discovery_shuffle_all_button">Shuffle all</string>
<string name="home_title_starred_albums">★ Starred albums</string>
<string name="home_title_starred_albums_see_all_button">See all</string>
<string name="home_title_starred_artists">★ Starred artists</string>
@@ -105,6 +109,7 @@
<string name="menu_search_button">Search</string>
<string name="menu_settings_button">Settings</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_expanded">Browse Playlists</string>
<string name="playlist_chooser_dialog_empty">No playlists created</string>
@@ -129,6 +134,7 @@
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS Url</string>
<string name="podcast_channel_editor_dialog_title">Podcast Channel</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="podcast_episode_download_request_snackbar">Your request has been sent to the server</string>
<string name="podcast_info_empty_subtitle">Once you add a channel, you\'ll find it here</string>
<string name="podcast_info_empty_title">No podcasts found!</string>
<string name="podcast_info_empty_button">Click to hide the section\nThe effects will be visible on restart</string>
@@ -166,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="settings_about_summary">Tempo is an open source and lightweight music client for Subsonic, designed and built natively for Android.</string>
<string name="settings_about_title">About</string>
<string name="settings_audio_transcode_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_wifi">Transcode format in Wi-Fi</string>
<string name="settings_covers_cache">Size of artwork cache</string>
<string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers</string>
<string name="settings_data_saving_mode_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_equalizer_summary">Adjust audio settings</string>
<string name="settings_equalizer_title">Equalizer</string>
@@ -181,6 +190,8 @@
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
<string name="settings_media_cache">Size of media file cache</string>
<string name="settings_music_directory">Show music directories</string>
<string name="settings_music_directory_summary">If enabled, show the music directory section. Please note that for folder navigation to work properly, the server must support this feature.</string>
<string name="settings_queue_syncing_title">Sync play queue for this user</string>
<string name="settings_queue_syncing_countdown">Sync timer</string>
<string name="settings_queue_syncing_summary">If enabled, the user will have the ability to save their play queue and will have the ability to load state when opening the application.</string>
@@ -188,11 +199,13 @@
<string name="settings_podcast_summary">If enabled, show the podcast section.</string>
<string name="settings_radio">Show radio</string>
<string name="settings_radio_summary">If enabled, show the radio section.</string>
<string name="settings_replay_gain">Set replay gain mode</string>
<string name="settings_rounded_corner">Rounded corners</string>
<string name="settings_rounded_corner_summary">If enabled, sets a curvature angle for all rendered covers. The changes will take effect on restart.</string>
<string name="settings_rounded_corner_size">Corners size</string>
<string name="settings_rounded_corner_size_summary">Sets the magnitude of the curvature angle.</string>
<string name="settings_scan_title">Scan library</string>
<string name="settings_summary_replay_gain">Replay gain is a feature that allows you to adjust the volume level of audio tracks for a consistent listening experience. This setting is only effective if the track contains the necessary metadata.</string>
<string name="settings_summary_syncing">Returns the state of the play queue for this user. This includes the tracks in the play queue, the currently playing track, and the position within this track. The server must support this feature.</string>
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
@@ -200,6 +213,7 @@
<string name="settings_theme">Theme</string>
<string name="settings_title_data">Data</string>
<string name="settings_title_general">General</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_syncing">Syncing</string>
<string name="settings_title_transcoding">Transcoding</string>
<string name="settings_title_ui">UI</string>
@@ -233,7 +247,7 @@
<string name="starred_sync_dialog_title">Sync starred tracks</string>
<string name="undraw_url">https://undraw.co/</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_new_releases">New releases</string>
<string name="home_title_best_of">Best of</string>
@@ -252,4 +266,6 @@
<string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Random</string>
<string name="description_empty_title">No description available</string>
<string name="menu_filter_download">Downloaded</string>
<string name="menu_filter_all">All</string>
</resources>

View File

@@ -51,6 +51,12 @@
android:defaultValue="true"
android:summary="@string/settings_radio_summary"
android:key="radio_section_visibility" />
<SwitchPreference
android:title="@string/settings_music_directory"
android:defaultValue="true"
android:summary="@string/settings_music_directory_summary"
android:key="music_directory_section_visibility" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_data">
@@ -96,6 +102,12 @@
app:selectable="false"
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
app:defaultValue="raw"
app:dialogTitle="@string/settings_audio_transcode_format_wifi"
@@ -133,6 +145,22 @@
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_replay_gain">
<Preference
app:selectable="false"
app:summary="@string/settings_summary_replay_gain" />
<ListPreference
app:defaultValue="disabled"
app:dialogTitle="@string/settings_replay_gain"
app:entries="@array/replay_gain_titles"
app:entryValues="@array/replay_gain_values"
app:key="replay_gain_mode"
app:title="@string/settings_replay_gain"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_syncing">
<Preference
app:selectable="false"
@@ -141,7 +169,7 @@
<SwitchPreference
android:title="@string/settings_queue_syncing_title"
android:defaultValue="false"
android:summary="@string/settings_sync_starred_tracks_for_offline_use_summary"
android:summary="@string/settings_queue_syncing_summary"
android:key="queue_syncing" />
<ListPreference
@@ -160,6 +188,7 @@
app:summary="@string/settings_about_summary" />
<Preference
android:key="version"
app:summary="@string/settings_version_summary"
app:title="@string/settings_version_title" />

View File

@@ -0,0 +1,253 @@
package com.cappielloantonio.tempo.service
import android.annotation.SuppressLint
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Intent
import android.os.Bundle
import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@UnstableApi
class MediaService : MediaLibraryService() {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
companion object {
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
override fun onCreate() {
super.onCreate()
initializeCustomCommands()
initializePlayer()
initializeMediaLibrarySession()
initializePlayerListener()
setPlayer(player)
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onDestroy() {
releasePlayer()
super.onDestroy()
}
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { commandButton ->
// TODO: Aggiungere i comandi personalizzati
// commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
}
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
player.shuffleModeEnabled = true
customLayout = ImmutableList.of(customCommands[1])
session.setCustomLayout(customLayout)
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
player.shuffleModeEnabled = false
customLayout = ImmutableList.of(customCommands[0])
session.setCustomLayout(customLayout)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems = mediaItems.map {
it.buildUpon()
.setUri(it.requestMetadata.mediaUri)
.setMediaMetadata(it.mediaMetadata)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.build()
}
return Futures.immediateFuture(updatedMediaItems)
}
}
private fun initializeCustomCommands() {
customCommands =
listOf(
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
),
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
}
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK)
.build()
}
private fun initializeMediaLibrarySession() {
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
}
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setSessionActivity(sessionActivityPendingIntent)
.build()
if (!customLayout.isEmpty()) {
mediaLibrarySession.setCustomLayout(customLayout)
}
}
private fun initializePlayerListener() {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (mediaItem == null) return
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
MediaManager.setLastPlayedTimestamp(mediaItem)
}
}
override fun onTracksChanged(tracks: Tracks) {
ReplayGainUtil.setReplayGain(player, tracks)
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem,
player.currentPosition
)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED &&
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
) {
MediaManager.scrobble(player.currentMediaItem)
MediaManager.saveChronology(player.currentMediaItem)
}
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.scrobble(oldPosition.mediaItem)
MediaManager.saveChronology(oldPosition.mediaItem)
}
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
}
}
}
})
}
private fun setPlayer(player: Player) {
mediaLibrarySession.player = player
}
private fun releasePlayer() {
player.release()
mediaLibrarySession.release()
}
@SuppressLint("PrivateResource")
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder()
.setDisplayName(
getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.setSessionCommand(sessionCommand)
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
.build()
}
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory() =
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
}

View File

@@ -0,0 +1,65 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentToolbarBinding;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
@UnstableApi
public class ToolbarFragment extends Fragment {
private static final String TAG = "ToolbarFragment";
private FragmentToolbarBinding bind;
private MainActivity activity;
public ToolbarFragment() {
// Required empty public constructor
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentToolbarBinding.inflate(inflater, container, false);
View view = bind.getRoot();
return view;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.settingsFragment);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,9 @@
package com.cappielloantonio.tempo.util;
import android.content.Context;
public class Flavors {
public static void initializeCastContext(Context context) {
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:title="@string/menu_search_button"
app:showAsAction="always" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:title="@string/menu_settings_button"
app:showAsAction="never" />
</menu>

Some files were not shown because too many files have changed in this diff Show More