Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fd4cc5604 | ||
|
|
c73b71345a | ||
|
|
11f0af1ebd | ||
|
|
a532f988b0 | ||
|
|
bec2796771 | ||
|
|
feec7f7399 | ||
|
|
067c2f84dd | ||
|
|
69996a4346 | ||
|
|
80cda129d2 | ||
|
|
dd0ff04155 | ||
|
|
c69558aa7f | ||
|
|
8fefea0d94 | ||
|
|
0ddee6f44e | ||
|
|
feec1ab55a | ||
|
|
49500aecd6 | ||
|
|
894bf100d2 | ||
|
|
7ba810adb8 | ||
|
|
d10f33362e | ||
|
|
82e8fbba2b | ||
|
|
64325fe2a6 | ||
|
|
99217fee48 | ||
|
|
f12e64990c | ||
|
|
ef7bb9a9d8 | ||
|
|
5ac925e151 | ||
|
|
acc5bde33a | ||
|
|
b521f37ec1 | ||
|
|
379fa21571 | ||
|
|
1aa6a441f3 | ||
|
|
705bbaccc1 | ||
|
|
8365e06762 | ||
|
|
139a0b3412 | ||
|
|
ea6b21169e | ||
|
|
a262efe9da | ||
|
|
ed5fb09007 | ||
|
|
d3c6429fd6 | ||
|
|
371fcacc87 | ||
|
|
75666e523f | ||
|
|
592ea7c533 | ||
|
|
b860dfc791 | ||
|
|
96dde8cf31 | ||
|
|
b4a37fed28 | ||
|
|
ad3fee8907 | ||
|
|
94db55598a | ||
|
|
9687f47b6b | ||
|
|
af216d9b38 | ||
|
|
c7879c2bc0 | ||
|
|
7db503a2cf | ||
|
|
d72295a0e6 | ||
|
|
638da4400f | ||
|
|
af9a7aafae | ||
|
|
ab9f7fed64 | ||
|
|
b62ce27487 | ||
|
|
6d9e902fe7 | ||
|
|
a0faf3f7d4 | ||
|
|
32dfb3b40a | ||
|
|
6bbcd300e4 | ||
|
|
c54d9b3c5f | ||
|
|
9ab03fbcd4 | ||
|
|
a852114e9c | ||
|
|
3c9b8c1c6f | ||
|
|
4735b51dc0 | ||
|
|
23927a1d59 | ||
|
|
40b592f7c1 | ||
|
|
4da31f5850 | ||
|
|
f79e96cbfe | ||
|
|
2dbce1e3bb | ||
|
|
cdfbbcbf8a | ||
|
|
890852bef5 | ||
|
|
3b285db21a | ||
|
|
d37be88977 | ||
|
|
e6d27a1487 | ||
|
|
5d23c17d30 | ||
|
|
9c9c1c3b94 | ||
|
|
4e94fd6ad0 | ||
|
|
81a8203cca | ||
|
|
b6b206c288 | ||
|
|
ed2310a2e7 | ||
|
|
bae5d97087 | ||
|
|
3e12ff139b | ||
|
|
7ccd8db085 | ||
|
|
fdc829d06c | ||
|
|
e7f61e34e8 | ||
|
|
141877f111 | ||
|
|
b71a976e61 | ||
|
|
68c95ed6d5 | ||
|
|
7a68cc99af | ||
|
|
7294bc7033 | ||
|
|
f8836be147 | ||
|
|
713999eb83 | ||
|
|
cc4c9787c0 | ||
|
|
ecc5cf3e1b | ||
|
|
ba7d2be03e | ||
|
|
368f349c78 | ||
|
|
639d641c07 | ||
|
|
d9313a1654 | ||
|
|
9dd532285c | ||
|
|
92f10fdb5c | ||
|
|
9d0f43a664 | ||
|
|
453365fcd6 | ||
|
|
6231c5fcc4 | ||
|
|
9821ddabcf | ||
|
|
57777f2f4b | ||
|
|
1f2c309d8c | ||
|
|
c11977870d | ||
|
|
41753cd853 | ||
|
|
003285f567 | ||
|
|
d1c1902cd3 | ||
|
|
b697deac1c | ||
|
|
fb8e6408bb | ||
|
|
836ffddd4f | ||
|
|
c8ff5a7817 | ||
|
|
c14f6ec4c2 | ||
|
|
dc8852eb0d | ||
|
|
586405f9de | ||
|
|
34e68593bc | ||
|
|
c02adfa326 | ||
|
|
021d63d973 | ||
|
|
50f6b15d23 | ||
|
|
6257c6488b | ||
|
|
defd895995 | ||
|
|
82159ee537 | ||
|
|
11f22888d5 | ||
|
|
20d932877e | ||
|
|
35631fb211 | ||
|
|
b02dd18e0b | ||
|
|
b583a8e6e2 | ||
|
|
5775a51731 | ||
|
|
9b94a11ab9 | ||
|
|
9ee1203130 | ||
|
|
711b079963 | ||
|
|
91dbd73d45 | ||
|
|
a2ac0e29e4 | ||
|
|
4271d832f1 | ||
|
|
64147d065c | ||
|
|
fb03c8e67f | ||
|
|
75979960ab | ||
|
|
1411157bde | ||
|
|
eb7289c150 | ||
|
|
c1f803bdd0 | ||
|
|
ba1efedd79 | ||
|
|
ac10997ed7 | ||
|
|
3c6f29bf3a | ||
|
|
f623393f8d | ||
|
|
66ab402a50 | ||
|
|
2076b93e19 | ||
|
|
a5aae3fce8 | ||
|
|
eb7a241e94 | ||
|
|
14ff9097c7 | ||
|
|
79f09b334c | ||
|
|
f5d1e992ef | ||
|
|
aed2c31f5a | ||
|
|
3e31e72734 | ||
|
|
7cf36d829a | ||
|
|
268cf67576 | ||
|
|
5c76158729 | ||
|
|
8c87b31e56 | ||
|
|
01678f2c91 | ||
|
|
b5e33b1707 | ||
|
|
bba388aefa | ||
|
|
46623d70a3 | ||
|
|
452b25057e | ||
|
|
67a6aec132 | ||
|
|
10ddc1c911 | ||
|
|
2a2ebefeb9 | ||
|
|
d5f39e5cb1 | ||
|
|
099be648bf | ||
|
|
6a885b7241 | ||
|
|
15cdff5aa2 | ||
|
|
8bd0e863ae | ||
|
|
94e261d7bf | ||
|
|
06f87d24a7 | ||
|
|
cdd2f40ef9 | ||
|
|
2448b461f0 | ||
|
|
4f85827585 | ||
|
|
fe042f3f83 | ||
|
|
d084958f2d | ||
|
|
ccecba7a19 | ||
|
|
d57fe44bcc | ||
|
|
a32bd9d74c | ||
|
|
39250b56c0 | ||
|
|
1c85e21e4f | ||
|
|
d331e1f9ab | ||
|
|
d7c414f6c0 | ||
|
|
523f861f77 | ||
|
|
8a0a6f383f | ||
|
|
b8b2d73eab | ||
|
|
683b4e753a | ||
|
|
a3f2b02473 | ||
|
|
befe62f8a0 | ||
|
|
621a493c5b | ||
|
|
8952cbfd20 | ||
|
|
a576eff700 | ||
|
|
506e18a137 | ||
|
|
27d2ed2304 | ||
|
|
a670ba9f95 | ||
|
|
2321312427 | ||
|
|
85382d2bb4 | ||
|
|
7745ccefab | ||
|
|
872931f507 | ||
|
|
f335d8735f | ||
|
|
fca0fd7bbb | ||
|
|
a461ed43de | ||
|
|
0f8672fb92 | ||
|
|
972fbd42ee | ||
|
|
062a217946 | ||
|
|
a59f3bb957 | ||
|
|
93cb27d324 | ||
|
|
b4e70da1f5 | ||
|
|
bf46c46fc0 | ||
|
|
af835f2c43 | ||
|
|
ffd0d4da7d | ||
|
|
e9226671c9 | ||
|
|
5fd3f3c080 | ||
|
|
519969eee0 | ||
|
|
1746ed6469 | ||
|
|
2912d442b5 | ||
|
|
a82474cf45 | ||
|
|
56a3d8bb72 | ||
|
|
add64249cd | ||
|
|
d3172ece9b | ||
|
|
f655f89c49 | ||
|
|
911e46c4f0 | ||
|
|
ec4647e2bc | ||
|
|
10f2da9e90 | ||
|
|
6aa0f2253b | ||
|
|
7c5ee940d3 | ||
|
|
d83e04c47b | ||
|
|
b0905d0270 | ||
|
|
8ce0d9ce56 | ||
|
|
ac2a1ce079 | ||
|
|
5d31499fd6 | ||
|
|
4c8217edf6 | ||
|
|
934a07a5ad | ||
|
|
7aae1edf3c | ||
|
|
7cc35e4b96 | ||
|
|
1f8a8d1ce5 | ||
|
|
e0e2f02e0b | ||
|
|
c029e519e9 | ||
|
|
ef661bba37 | ||
|
|
8d38f3358e | ||
|
|
f4e39ec7ca | ||
|
|
72744dcd29 | ||
|
|
6b374d923b | ||
|
|
03a47b06ee | ||
|
|
77d0974782 | ||
|
|
95e070a897 | ||
|
|
c32c77b963 | ||
|
|
9477860bca | ||
|
|
a5f483f518 | ||
|
|
65fcc45bc6 | ||
|
|
2e6946e1f7 | ||
|
|
64707b0928 | ||
|
|
a107a25e43 | ||
|
|
0a989e7811 | ||
|
|
4e63d43679 | ||
|
|
60cb5f398c | ||
|
|
8edeefc1cd | ||
|
|
02eabb82c1 | ||
|
|
999d434fe9 | ||
|
|
2b07c81e54 | ||
|
|
5f4ff38035 | ||
|
|
e52a5fd585 | ||
|
|
599a8e23cb | ||
|
|
81a4e4ff7b | ||
|
|
7d730dcafb | ||
|
|
446e54cb91 | ||
|
|
b9f428fd77 | ||
|
|
94e8978ce6 | ||
|
|
62376871e3 | ||
|
|
40b731534c | ||
|
|
1aea0a18a9 | ||
|
|
b1be594932 | ||
|
|
a9d7fbc083 | ||
|
|
a3e458fcc4 | ||
|
|
c1b8f8519f | ||
|
|
34d4a0b61b | ||
|
|
7c3b6c9702 | ||
|
|
e275aa1699 | ||
|
|
e9961f64a9 | ||
|
|
8e2be4704e | ||
|
|
948cd3068a | ||
|
|
162eb9afad | ||
|
|
30ae98c3de | ||
|
|
b1b10e846d | ||
|
|
56c73c12db | ||
|
|
ad802c16ea | ||
|
|
afc04d22bf | ||
|
|
b78f004f29 | ||
|
|
849b6303e8 | ||
|
|
4a82ba7c86 | ||
|
|
5b7fcca800 | ||
|
|
b549528e23 | ||
|
|
719db8d3f6 | ||
|
|
db98492d4f | ||
|
|
5430c10424 | ||
|
|
5495bcb945 | ||
|
|
7fded5e105 | ||
|
|
dc8bc2fccd | ||
|
|
af2c609405 | ||
|
|
d5d65e244b | ||
|
|
92a31c246c | ||
|
|
9082b76707 | ||
|
|
33041608f8 | ||
|
|
408faf3665 | ||
|
|
be6c675ded | ||
|
|
528854b285 | ||
|
|
87e277fad1 | ||
|
|
014588180e | ||
|
|
e02edb9f9e | ||
|
|
37fa931672 | ||
|
|
a7424cc428 | ||
|
|
837df329e2 | ||
|
|
f6c43e479d | ||
|
|
0b46a9c9b1 | ||
|
|
76de92fe1f | ||
|
|
32550a75ec | ||
|
|
9eca03cfd7 | ||
|
|
fec1ec554b | ||
|
|
db7a8cef77 | ||
|
|
5662c1983a | ||
|
|
8cfefdcc0f | ||
|
|
2ab9dbc72d | ||
|
|
52d0890032 | ||
|
|
b7725c455b | ||
|
|
175e642081 | ||
|
|
e620f53705 | ||
|
|
f56561b898 | ||
|
|
a29a7669e1 | ||
|
|
29f08a5e5d | ||
|
|
f40da562a6 | ||
|
|
affcdea09b | ||
|
|
16ca7460f4 | ||
|
|
2ca9ce797d |
@@ -33,8 +33,8 @@ android {
|
|||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 86
|
versionCode 91
|
||||||
versionName "0.9.55-beta"
|
versionName "0.9.60-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -43,6 +43,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
versionNameSuffix "-debug"
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
@@ -121,7 +127,7 @@ dependencies {
|
|||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||||
|
|
||||||
// Jackson for JSON
|
// Jackson for JSON
|
||||||
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1'
|
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2'
|
||||||
|
|
||||||
// FFMPEG-Kit
|
// FFMPEG-Kit
|
||||||
implementation 'com.arthenica:ffmpeg-kit-min:4.5.1'
|
implementation 'com.arthenica:ffmpeg-kit-min:4.5.1'
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 879 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -75,7 +75,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioTrack(
|
||||||
|
var index:Int,
|
||||||
|
var startOffset:Double,
|
||||||
|
var duration:Double,
|
||||||
|
var title:String,
|
||||||
|
var contentUrl:String,
|
||||||
|
var mimeType:String,
|
||||||
|
var metadata:FileMetadata?,
|
||||||
|
var isLocal:Boolean,
|
||||||
|
var localFileId:String?,
|
||||||
|
var audioProbeResult:AudioProbeResult?,
|
||||||
|
var serverIndex:Int? // Need to know if server track index is different
|
||||||
|
) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val startOffsetMs get() = (startOffset * 1000L).toLong()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val durationMs get() = (duration * 1000L).toLong()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val endOffsetMs get() = startOffsetMs + durationMs
|
||||||
|
@get:JsonIgnore
|
||||||
|
val relPath get() = metadata?.relPath ?: ""
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getBookChapter():BookChapter {
|
||||||
|
return BookChapter(index + 1,startOffset, startOffset + duration, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,13 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import com.audiobookshelf.app.R
|
import androidx.media.utils.MediaConstants
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.fasterxml.jackson.annotation.*
|
import com.fasterxml.jackson.annotation.*
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
data class LibraryItem(
|
|
||||||
var id:String,
|
|
||||||
var ino:String,
|
|
||||||
var libraryId:String,
|
|
||||||
var folderId:String,
|
|
||||||
var path:String,
|
|
||||||
var relPath:String,
|
|
||||||
var mtimeMs:Long,
|
|
||||||
var ctimeMs:Long,
|
|
||||||
var birthtimeMs:Long,
|
|
||||||
var addedAt:Long,
|
|
||||||
var updatedAt:Long,
|
|
||||||
var lastScan:Long?,
|
|
||||||
var scanVersion:String?,
|
|
||||||
var isMissing:Boolean,
|
|
||||||
var isInvalid:Boolean,
|
|
||||||
var mediaType:String,
|
|
||||||
var media:MediaType,
|
|
||||||
var libraryFiles:MutableList<LibraryFile>?,
|
|
||||||
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
|
|
||||||
) : LibraryItemWrapper() {
|
|
||||||
@get:JsonIgnore
|
|
||||||
val title get() = media.metadata.title
|
|
||||||
@get:JsonIgnore
|
|
||||||
val authorName get() = media.metadata.getAuthorDisplayName()
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
fun getCoverUri():Uri {
|
|
||||||
if (media.coverPath == null) {
|
|
||||||
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
fun checkHasTracks():Boolean {
|
|
||||||
return if (mediaType == "podcast") {
|
|
||||||
((media as Podcast).numEpisodes ?: 0) > 0
|
|
||||||
} else {
|
|
||||||
((media as Book).numTracks ?: 0) > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
|
||||||
return MediaMetadataCompat.Builder().apply {
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This auto-detects whether it is a Book or Podcast
|
// This auto-detects whether it is a Book or Podcast
|
||||||
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||||
@JsonSubTypes(
|
@JsonSubTypes(
|
||||||
@@ -110,7 +52,7 @@ class Podcast(
|
|||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
||||||
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,(episodes?.size ?: 0) + 1,null,null,at.title,null,null,null, null, null, at,at.duration,0, null)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +65,7 @@ class Podcast(
|
|||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
override fun addAudioTrack(audioTrack:AudioTrack) {
|
||||||
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
|
val newEpisode = PodcastEpisode("local_ep_" + audioTrack.localFileId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null, null, null,audioTrack,audioTrack.duration,0, null)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -142,9 +84,16 @@ class Podcast(
|
|||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||||
|
@JsonIgnore
|
||||||
|
override fun getLocalCopy(): Podcast {
|
||||||
|
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
||||||
val newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
|
val newEpisode = PodcastEpisode("local_ep_" + episode.id,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,audioTrack.duration,0, episode.id)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -155,10 +104,14 @@ class Podcast(
|
|||||||
return newEpisode
|
return newEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Podcast {
|
fun getNextUnfinishedEpisode(libraryItemId:String, mediaManager: MediaManager):PodcastEpisode? {
|
||||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
val sortedEpisodes = episodes?.sortedByDescending { it.publishedAt }
|
||||||
|
val podcastEpisode = sortedEpisodes?.find { episode ->
|
||||||
|
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemId && it.episodeId == episode.id }
|
||||||
|
progress == null || !progress.isFinished
|
||||||
|
}
|
||||||
|
return podcastEpisode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +135,13 @@ class Book(
|
|||||||
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
||||||
tracks = audioTracks
|
tracks = audioTracks
|
||||||
tracks?.sortBy { it.index }
|
tracks?.sortBy { it.index }
|
||||||
// TODO: Is it necessary to calculate this each time? check if can remove safely
|
|
||||||
var totalDuration = 0.0
|
var totalDuration = 0.0
|
||||||
|
var currentStartOffset = 0.0
|
||||||
tracks?.forEach {
|
tracks?.forEach {
|
||||||
totalDuration += it.duration
|
totalDuration += it.duration
|
||||||
|
it.startOffset = currentStartOffset
|
||||||
|
currentStartOffset += it.duration
|
||||||
}
|
}
|
||||||
duration = totalDuration
|
duration = totalDuration
|
||||||
}
|
}
|
||||||
@@ -218,7 +174,6 @@ class Book(
|
|||||||
}
|
}
|
||||||
duration = totalDuration
|
duration = totalDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
override fun getLocalCopy(): Book {
|
||||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
||||||
@@ -288,6 +243,8 @@ data class PodcastEpisode(
|
|||||||
var title:String?,
|
var title:String?,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var description:String?,
|
var description:String?,
|
||||||
|
var pubDate:String?,
|
||||||
|
var publishedAt:Long?,
|
||||||
var audioFile:AudioFile?,
|
var audioFile:AudioFile?,
|
||||||
var audioTrack:AudioTrack?,
|
var audioTrack:AudioTrack?,
|
||||||
var duration:Double?,
|
var duration:Double?,
|
||||||
@@ -295,25 +252,49 @@ data class PodcastEpisode(
|
|||||||
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat {
|
fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat {
|
||||||
var coverUri:Uri = Uri.EMPTY
|
val coverUri = if (libraryItem is LocalLibraryItem) {
|
||||||
val podcast = if(libraryItem is LocalLibraryItem) {
|
libraryItem.getCoverUri()
|
||||||
coverUri = libraryItem.getCoverUri()
|
|
||||||
libraryItem.media as Podcast
|
|
||||||
} else {
|
} else {
|
||||||
coverUri = (libraryItem as LibraryItem).getCoverUri()
|
(libraryItem as LibraryItem).getCoverUri()
|
||||||
(libraryItem as LibraryItem).media as Podcast
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaMetadataCompat.Builder().apply {
|
val extras = Bundle()
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
if (progress != null) {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
if (progress.isFinished) {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
extras.putInt(
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName())
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
)
|
||||||
|
} else {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||||
|
)
|
||||||
|
extras.putDouble(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}.build()
|
val libraryItemDescription = libraryItem.getMediaDescription(null, ctx)
|
||||||
|
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
|
.setMediaId(id)
|
||||||
|
.setTitle(title)
|
||||||
|
.setIconUri(coverUri)
|
||||||
|
.setSubtitle(libraryItemDescription.title)
|
||||||
|
.setExtras(extras)
|
||||||
|
|
||||||
|
libraryItemDescription.iconBitmap?.let {
|
||||||
|
mediaDescriptionBuilder.setIconBitmap(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaDescriptionBuilder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,36 +344,6 @@ data class Folder(
|
|||||||
var fullPath:String
|
var fullPath:String
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
data class AudioTrack(
|
|
||||||
var index:Int,
|
|
||||||
var startOffset:Double,
|
|
||||||
var duration:Double,
|
|
||||||
var title:String,
|
|
||||||
var contentUrl:String,
|
|
||||||
var mimeType:String,
|
|
||||||
var metadata:FileMetadata?,
|
|
||||||
var isLocal:Boolean,
|
|
||||||
var localFileId:String?,
|
|
||||||
var audioProbeResult:AudioProbeResult?,
|
|
||||||
var serverIndex:Int? // Need to know if server track index is different
|
|
||||||
) {
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
val startOffsetMs get() = (startOffset * 1000L).toLong()
|
|
||||||
@get:JsonIgnore
|
|
||||||
val durationMs get() = (duration * 1000L).toLong()
|
|
||||||
@get:JsonIgnore
|
|
||||||
val endOffsetMs get() = startOffsetMs + durationMs
|
|
||||||
@get:JsonIgnore
|
|
||||||
val relPath get() = metadata?.relPath ?: ""
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
fun getBookChapter():BookChapter {
|
|
||||||
return BookChapter(index + 1,startOffset, startOffset + duration, title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class BookChapter(
|
data class BookChapter(
|
||||||
var id:Int,
|
var id:Int,
|
||||||
@@ -407,18 +358,25 @@ data class BookChapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class MediaProgress(
|
class MediaProgress(
|
||||||
var id:String,
|
var id:String,
|
||||||
var libraryItemId:String,
|
var libraryItemId:String,
|
||||||
var episodeId:String?,
|
var episodeId:String?,
|
||||||
var duration:Double, // seconds
|
var duration:Double, // seconds
|
||||||
var progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
var currentTime:Double,
|
var currentTime:Double,
|
||||||
var isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?
|
var finishedAt:Long?
|
||||||
|
) : MediaProgressWrapper(isFinished, progress)
|
||||||
|
|
||||||
|
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
|
||||||
|
@JsonSubTypes(
|
||||||
|
JsonSubTypes.Type(MediaProgress::class),
|
||||||
|
JsonSubTypes.Type(LocalMediaProgress::class)
|
||||||
)
|
)
|
||||||
|
open class MediaProgressWrapper(var isFinished:Boolean, var progress:Double)
|
||||||
|
|
||||||
// Helper class
|
// Helper class
|
||||||
data class LibraryItemWithEpisode(
|
data class LibraryItemWithEpisode(
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||||
|
|
||||||
|
enum class LockOrientationSetting {
|
||||||
|
NONE, PORTRAIT, LANDSCAPE
|
||||||
|
}
|
||||||
|
|
||||||
data class ServerConnectionConfig(
|
data class ServerConnectionConfig(
|
||||||
var id:String,
|
var id:String,
|
||||||
var index:Int,
|
var index:Int,
|
||||||
@@ -20,7 +26,9 @@ data class DeviceSettings(
|
|||||||
var disableAutoRewind:Boolean,
|
var disableAutoRewind:Boolean,
|
||||||
var enableAltView:Boolean,
|
var enableAltView:Boolean,
|
||||||
var jumpBackwardsTime:Int,
|
var jumpBackwardsTime:Int,
|
||||||
var jumpForwardTime:Int
|
var jumpForwardTime:Int,
|
||||||
|
var disableShakeToResetSleepTimer:Boolean,
|
||||||
|
var lockOrientation:LockOrientationSetting
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
// Static method to get default device settings
|
// Static method to get default device settings
|
||||||
@@ -29,15 +37,17 @@ data class DeviceSettings(
|
|||||||
disableAutoRewind = false,
|
disableAutoRewind = false,
|
||||||
enableAltView = false,
|
enableAltView = false,
|
||||||
jumpBackwardsTime = 10,
|
jumpBackwardsTime = 10,
|
||||||
jumpForwardTime = 10
|
jumpForwardTime = 10,
|
||||||
|
disableShakeToResetSleepTimer = false,
|
||||||
|
lockOrientation = LockOrientationSetting.NONE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L
|
val jumpBackwardsTimeMs get() = (jumpBackwardsTime ?: default().jumpBackwardsTime) * 1000L
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val jumpForwardTimeMs get() = jumpForwardTime * 1000L
|
val jumpForwardTimeMs get() = (jumpForwardTime ?: default().jumpForwardTime) * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeviceData(
|
data class DeviceData(
|
||||||
@@ -90,7 +100,10 @@ data class LocalFolder(
|
|||||||
JsonSubTypes.Type(LibraryItem::class),
|
JsonSubTypes.Type(LibraryItem::class),
|
||||||
JsonSubTypes.Type(LocalLibraryItem::class)
|
JsonSubTypes.Type(LocalLibraryItem::class)
|
||||||
)
|
)
|
||||||
open class LibraryItemWrapper()
|
open class LibraryItemWrapper(var id:String) {
|
||||||
|
@JsonIgnore
|
||||||
|
open fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class DeviceInfo(
|
data class DeviceInfo(
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Used in Android Auto to represent a podcast episode or an audiobook in progress
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class ItemInProgress(
|
||||||
|
val libraryItemWrapper: LibraryItemWrapper,
|
||||||
|
val episode: PodcastEpisode?,
|
||||||
|
val progressLastUpdate: Long,
|
||||||
|
val isLocal: Boolean
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun makeFromServerObject(serverItem: JSONObject):ItemInProgress {
|
||||||
|
val jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
val libraryItem = jacksonMapper.readValue<LibraryItem>(serverItem.toString())
|
||||||
|
|
||||||
|
var episode:PodcastEpisode? = null
|
||||||
|
if (serverItem.has("recentEpisode")) {
|
||||||
|
episode = jacksonMapper.readValue<PodcastEpisode>(serverItem.get("recentEpisode").toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressLastUpdate:Long = serverItem.getLong("progressLastUpdate")
|
||||||
|
return ItemInProgress(libraryItem, episode, progressLastUpdate, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.R
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class LibraryItem(
|
||||||
|
id:String,
|
||||||
|
var ino:String,
|
||||||
|
var libraryId:String,
|
||||||
|
var folderId:String,
|
||||||
|
var path:String,
|
||||||
|
var relPath:String,
|
||||||
|
var mtimeMs:Long,
|
||||||
|
var ctimeMs:Long,
|
||||||
|
var birthtimeMs:Long,
|
||||||
|
var addedAt:Long,
|
||||||
|
var updatedAt:Long,
|
||||||
|
var lastScan:Long?,
|
||||||
|
var scanVersion:String?,
|
||||||
|
var isMissing:Boolean,
|
||||||
|
var isInvalid:Boolean,
|
||||||
|
var mediaType:String,
|
||||||
|
var media:MediaType,
|
||||||
|
var libraryFiles:MutableList<LibraryFile>?,
|
||||||
|
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
|
||||||
|
) : LibraryItemWrapper(id) {
|
||||||
|
@get:JsonIgnore
|
||||||
|
val title get() = media.metadata.title
|
||||||
|
@get:JsonIgnore
|
||||||
|
val authorName get() = media.metadata.getAuthorDisplayName()
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getCoverUri(): Uri {
|
||||||
|
if (media.coverPath == null) {
|
||||||
|
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun checkHasTracks():Boolean {
|
||||||
|
return if (mediaType == "podcast") {
|
||||||
|
((media as Podcast).numEpisodes ?: 0) > 0
|
||||||
|
} else {
|
||||||
|
((media as Book).numTracks ?: 0) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat {
|
||||||
|
val extras = Bundle()
|
||||||
|
|
||||||
|
if (progress != null) {
|
||||||
|
if (progress.isFinished) {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||||
|
)
|
||||||
|
extras.putDouble(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (mediaType != "podcast") {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaDescriptionCompat.Builder()
|
||||||
|
.setMediaId(id)
|
||||||
|
.setTitle(title)
|
||||||
|
.setIconUri(getCoverUri())
|
||||||
|
.setSubtitle(authorName)
|
||||||
|
.setExtras(extras)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.media.utils.MediaConstants
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
@@ -11,8 +17,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalLibraryItem(
|
class LocalLibraryItem(
|
||||||
var id:String,
|
id:String,
|
||||||
var folderId:String,
|
var folderId:String,
|
||||||
var basePath:String,
|
var basePath:String,
|
||||||
var absolutePath:String,
|
var absolutePath:String,
|
||||||
@@ -29,7 +35,7 @@ data class LocalLibraryItem(
|
|||||||
var serverAddress:String?,
|
var serverAddress:String?,
|
||||||
var serverUserId:String?,
|
var serverUserId:String?,
|
||||||
var libraryItemId:String?
|
var libraryItemId:String?
|
||||||
) : LibraryItemWrapper() {
|
) : LibraryItemWrapper(id) {
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val title get() = media.metadata.title
|
val title get() = media.metadata.title
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
@@ -95,18 +101,55 @@ data class LocalLibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(ctx: Context): MediaMetadataCompat {
|
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx:Context?): MediaDescriptionCompat {
|
||||||
val coverUri = getCoverUri()
|
val coverUri = getCoverUri()
|
||||||
|
|
||||||
return MediaMetadataCompat.Builder().apply {
|
var bitmap:Bitmap? = null
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
if (coverContentUrl != null) {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
ctx?.let {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
|
MediaStore.Images.Media.getBitmap(it.contentResolver, coverUri)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
} else {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
|
val source: ImageDecoder.Source = ImageDecoder.createSource(it.contentResolver, coverUri)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
|
ImageDecoder.decodeBitmap(source)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
|
}
|
||||||
}.build()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val extras = Bundle()
|
||||||
|
if (progress != null) {
|
||||||
|
if (progress.isFinished) {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||||
|
)
|
||||||
|
extras.putDouble(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (mediaType != "podcast") {
|
||||||
|
extras.putInt(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
|
.setMediaId(id)
|
||||||
|
.setTitle(title)
|
||||||
|
.setIconUri(coverUri)
|
||||||
|
.setSubtitle(authorName)
|
||||||
|
.setExtras(extras)
|
||||||
|
|
||||||
|
bitmap?.let {
|
||||||
|
mediaDescriptionBuilder.setIconBitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaDescriptionBuilder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalMediaProgress(
|
class LocalMediaProgress(
|
||||||
var id:String,
|
var id:String,
|
||||||
var localLibraryItemId:String,
|
var localLibraryItemId:String,
|
||||||
var localEpisodeId:String?,
|
var localEpisodeId:String?,
|
||||||
var duration:Double,
|
var duration:Double,
|
||||||
var progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
var currentTime:Double,
|
var currentTime:Double,
|
||||||
var isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?,
|
var finishedAt:Long?,
|
||||||
@@ -22,7 +22,7 @@ data class LocalMediaProgress(
|
|||||||
var serverUserId:String?,
|
var serverUserId:String?,
|
||||||
var libraryItemId:String?,
|
var libraryItemId:String?,
|
||||||
var episodeId:String?
|
var episodeId:String?
|
||||||
) {
|
) : MediaProgressWrapper(isFinished, progress) {
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
|
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import com.audiobookshelf.app.R
|
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
@@ -13,6 +16,7 @@ import com.google.android.gms.cast.MediaInfo
|
|||||||
import com.google.android.gms.cast.MediaQueueItem
|
import com.google.android.gms.cast.MediaQueueItem
|
||||||
import com.google.android.gms.common.images.WebImage
|
import com.google.android.gms.common.images.WebImage
|
||||||
|
|
||||||
|
|
||||||
// TODO: enum or something in kotlin?
|
// TODO: enum or something in kotlin?
|
||||||
val PLAYMETHOD_DIRECTPLAY = 0
|
val PLAYMETHOD_DIRECTPLAY = 0
|
||||||
val PLAYMETHOD_DIRECTSTREAM = 1
|
val PLAYMETHOD_DIRECTSTREAM = 1
|
||||||
@@ -53,6 +57,8 @@ class PlaybackSession(
|
|||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
|
val isPodcastEpisode get() = mediaType == "podcast"
|
||||||
|
@get:JsonIgnore
|
||||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
||||||
@@ -91,6 +97,7 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getTrackStartOffsetMs(index:Int):Long {
|
fun getTrackStartOffsetMs(index:Int):Long {
|
||||||
|
if (index < 0 || index >= audioTracks.size) return 0L
|
||||||
val currentTrack = audioTracks[index]
|
val currentTrack = audioTracks[index]
|
||||||
return (currentTrack.startOffset * 1000L).toLong()
|
return (currentTrack.startOffset * 1000L).toLong()
|
||||||
}
|
}
|
||||||
@@ -104,9 +111,9 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(): Uri {
|
||||||
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
||||||
|
|
||||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
||||||
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
|
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,15 +124,33 @@ class PlaybackSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
|
||||||
val metadataBuilder = MediaMetadataCompat.Builder()
|
val metadataBuilder = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, displayAuthor)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
||||||
|
|
||||||
|
// Local covers get bitmap
|
||||||
|
if (localLibraryItem?.coverContentUrl != null) {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, getCoverUri())
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, getCoverUri())
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
}
|
||||||
|
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
|
||||||
|
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
return metadataBuilder.build()
|
return metadataBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +162,9 @@ class PlaybackSession(
|
|||||||
.setArtist(displayAuthor)
|
.setArtist(displayAuthor)
|
||||||
.setAlbumArtist(displayAuthor)
|
.setAlbumArtist(displayAuthor)
|
||||||
.setSubtitle(displayAuthor)
|
.setSubtitle(displayAuthor)
|
||||||
|
.setAlbumTitle(displayAuthor)
|
||||||
|
.setDescription(displayAuthor)
|
||||||
|
.setArtworkUri(getCoverUri())
|
||||||
|
|
||||||
val contentUri = this.getContentUri(audioTrack)
|
val contentUri = this.getContentUri(audioTrack)
|
||||||
metadataBuilder.setMediaUri(contentUri)
|
metadataBuilder.setMediaUri(contentUri)
|
||||||
@@ -170,7 +198,9 @@ class PlaybackSession(
|
|||||||
|
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
||||||
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ALBUM_TITLE, displayAuthor ?: "")
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
|
||||||
|
|
||||||
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
||||||
return castMetadata
|
return castMetadata
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
|
|||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
|
import org.json.JSONException
|
||||||
|
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
@@ -465,11 +466,17 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
fun probeAudioFile(absolutePath:String):AudioProbeResult? {
|
fun probeAudioFile(absolutePath:String):AudioProbeResult? {
|
||||||
val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||||
Log.d(tag, "FFprobe output ${JSObject(session.output)}")
|
|
||||||
|
|
||||||
val probeObject = JSObject(session.output)
|
var probeObject:JSObject? = null
|
||||||
if (!probeObject.has("streams")) { // Check if output is empty
|
try {
|
||||||
Log.d(tag, "probeAudioFile Probe audio file $absolutePath is empty")
|
probeObject = JSObject(session.output)
|
||||||
|
} catch(error:JSONException) {
|
||||||
|
Log.e(tag, "Failed to parse probe result $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "FFprobe output $probeObject")
|
||||||
|
if (probeObject == null || !probeObject.has("streams")) { // Check if output is empty
|
||||||
|
Log.d(tag, "probeAudioFile Probe audio file $absolutePath failed or invalid")
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
|
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
|
||||||
|
|||||||
@@ -18,16 +18,19 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
var serverLibraryItems = mutableListOf<LibraryItem>()
|
var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
||||||
|
var selectedLibraryItems = mutableListOf<LibraryItem>()
|
||||||
var selectedLibraryId = ""
|
var selectedLibraryId = ""
|
||||||
|
|
||||||
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
|
||||||
var selectedPodcast:Podcast? = null
|
var selectedPodcast:Podcast? = null
|
||||||
var selectedLibraryItemId:String? = null
|
var selectedLibraryItemId:String? = null
|
||||||
var serverPodcastEpisodes = listOf<PodcastEpisode>()
|
var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
||||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
var serverLibraryCategories = listOf<LibraryCategory>()
|
||||||
|
var serverItemsInProgress = listOf<ItemInProgress>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
var serverConfigIdUsed:String? = null
|
var serverConfigIdUsed:String? = null
|
||||||
|
var serverConfigLastPing:Long = 0L
|
||||||
|
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||||
|
|
||||||
var userSettingsPlaybackRate:Float? = null
|
var userSettingsPlaybackRate:Float? = null
|
||||||
|
|
||||||
@@ -64,34 +67,39 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||||
|
|
||||||
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||||
serverPodcastEpisodes = listOf()
|
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||||
serverLibraryCategories = listOf()
|
serverLibraryCategories = listOf()
|
||||||
serverLibraries = listOf()
|
serverLibraries = listOf()
|
||||||
serverLibraryItems = mutableListOf()
|
serverLibraryItems = mutableListOf()
|
||||||
|
selectedLibraryItems = mutableListOf()
|
||||||
selectedLibraryId = ""
|
selectedLibraryId = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
|
||||||
if (serverLibraryCategories.isNotEmpty()) {
|
if (serverItemsInProgress.isNotEmpty()) {
|
||||||
cb(serverLibraryCategories)
|
cb(serverItemsInProgress)
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getLibraryCategories(libraryId) {
|
apiHandler.getAllItemsInProgress { itemsInProgress ->
|
||||||
serverLibraryCategories = it
|
serverItemsInProgress = itemsInProgress
|
||||||
cb(it)
|
cb(serverItemsInProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||||
if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
||||||
cb(serverLibraryItems)
|
cb(selectedLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||||
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
|
if (libraryItemsWithAudio.isNotEmpty()) {
|
||||||
|
selectedLibraryId = libraryId
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedLibraryItems = mutableListOf()
|
||||||
libraryItemsWithAudio.forEach { libraryItem ->
|
libraryItemsWithAudio.forEach { libraryItem ->
|
||||||
|
selectedLibraryItems.add(libraryItem)
|
||||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
serverLibraryItems.add(libraryItem)
|
serverLibraryItems.add(libraryItem)
|
||||||
}
|
}
|
||||||
@@ -113,41 +121,44 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
|
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, ctx:Context, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
|
||||||
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
|
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
|
||||||
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
|
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
|
||||||
|
|
||||||
selectedLibraryItemWrapper = libraryItemWrapper
|
|
||||||
|
|
||||||
libraryItemWrapper?.let {
|
libraryItemWrapper?.let {
|
||||||
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
|
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
|
||||||
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
||||||
serverPodcastEpisodes = listOf()
|
|
||||||
cb(mutableListOf())
|
cb(mutableListOf())
|
||||||
} else {
|
} else {
|
||||||
val podcast = libraryItemWrapper.media as Podcast
|
val podcast = libraryItemWrapper.media as Podcast
|
||||||
serverPodcastEpisodes = podcast.episodes ?: listOf()
|
|
||||||
selectedLibraryItemId = libraryItemWrapper.id
|
selectedLibraryItemId = libraryItemWrapper.id
|
||||||
selectedPodcast = podcast
|
selectedPodcast = podcast
|
||||||
|
|
||||||
val children = podcast.episodes?.map { podcastEpisode ->
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
|
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
|
||||||
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
|
val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}")
|
||||||
|
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
}
|
}
|
||||||
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
|
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
|
||||||
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
||||||
serverPodcastEpisodes = listOf()
|
|
||||||
cb(mutableListOf())
|
cb(mutableListOf())
|
||||||
} else {
|
} else {
|
||||||
val podcast = libraryItemWrapper.media as Podcast
|
val podcast = libraryItemWrapper.media as Podcast
|
||||||
serverPodcastEpisodes = podcast.episodes ?: listOf()
|
podcast.episodes?.forEach { podcastEpisode ->
|
||||||
|
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItemWrapper, podcastEpisode)
|
||||||
|
}
|
||||||
selectedLibraryItemId = libraryItemWrapper.id
|
selectedLibraryItemId = libraryItemWrapper.id
|
||||||
selectedPodcast = podcast
|
selectedPodcast = podcast
|
||||||
|
|
||||||
val children = podcast.episodes?.map { podcastEpisode ->
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
|
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
|
||||||
|
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
}
|
}
|
||||||
@@ -179,16 +190,49 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return successfulPing
|
return successfulPing
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
|
suspend fun authorize(config:ServerConnectionConfig) : MutableList<MediaProgress> {
|
||||||
if (!apiHandler.isOnline()) cb(false)
|
var mediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||||
else {
|
suspendCoroutine<MutableList<MediaProgress>> { cont ->
|
||||||
coroutineScope {
|
apiHandler.authorize(config) {
|
||||||
var hasValidConn = false
|
Log.d(tag, "authorize: Authorized server config ${config.address} result = $it")
|
||||||
|
if (!it.isNullOrEmpty()) {
|
||||||
|
mediaProgress = it
|
||||||
|
}
|
||||||
|
cont.resume(mediaProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mediaProgress
|
||||||
|
}
|
||||||
|
|
||||||
// First check if the current selected config is pingable
|
fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
|
||||||
DeviceManager.serverConnectionConfig?.let {
|
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
||||||
hasValidConn = checkServerConnection(it)
|
|
||||||
Log.d(tag, "checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn")
|
coroutineScope {
|
||||||
|
if (!apiHandler.isOnline()) {
|
||||||
|
serverUserMediaProgress = mutableListOf()
|
||||||
|
cb(false)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
var hasValidConn = false
|
||||||
|
var lookupMediaProgress = true
|
||||||
|
|
||||||
|
if (!serverConfigIdUsed.isNullOrEmpty() && serverConfigLastPing > 0L && System.currentTimeMillis() - serverConfigLastPing < 5000) {
|
||||||
|
Log.d(tag, "checkSetValidServerConnectionConfig last ping less than a 5 seconds ago")
|
||||||
|
hasValidConn = true
|
||||||
|
lookupMediaProgress = false
|
||||||
|
} else {
|
||||||
|
serverUserMediaProgress = mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidConn) {
|
||||||
|
// First check if the current selected config is pingable
|
||||||
|
DeviceManager.serverConnectionConfig?.let {
|
||||||
|
hasValidConn = checkServerConnection(it)
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasValidConn) {
|
if (!hasValidConn) {
|
||||||
@@ -205,31 +249,42 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasValidConn) {
|
||||||
|
serverConfigLastPing = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (lookupMediaProgress) {
|
||||||
|
Log.d(tag, "Has valid conn now get user media progress")
|
||||||
|
DeviceManager.serverConnectionConfig?.let {
|
||||||
|
serverUserMediaProgress = authorize(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cb(hasValidConn)
|
cb(hasValidConn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadServerUserMediaProgress(cb: () -> Unit) {
|
||||||
|
Log.d(tag, "Loading server media progress")
|
||||||
|
if (DeviceManager.serverConnectionConfig == null) {
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceManager.serverConnectionConfig?.let { config ->
|
||||||
|
apiHandler.authorize(config) {
|
||||||
|
Log.d(tag, "loadServerUserMediaProgress: Authorized server config ${config.address} result = $it")
|
||||||
|
if (!it.isNullOrEmpty()) {
|
||||||
|
serverUserMediaProgress = it
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Load currently listening category for local items
|
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||||
fun loadLocalCategory():List<LibraryCategory> {
|
|
||||||
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
|
||||||
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
|
||||||
val cats = mutableListOf<LibraryCategory>()
|
|
||||||
if (localBooks.isNotEmpty()) {
|
|
||||||
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
|
|
||||||
}
|
|
||||||
if (localPodcasts.isNotEmpty()) {
|
|
||||||
cats.add(LibraryCategory("local-podcasts", "Local Podcasts", "podcast", localPodcasts, true))
|
|
||||||
}
|
|
||||||
return cats
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
|
|
||||||
Log.d(tag, "Load android auto items")
|
Log.d(tag, "Load android auto items")
|
||||||
val cats = mutableListOf<LibraryCategory>()
|
|
||||||
|
|
||||||
val localCategories = loadLocalCategory()
|
|
||||||
cats.addAll(localCategories)
|
|
||||||
|
|
||||||
// Check if any valid server connection if not use locally downloaded books
|
// Check if any valid server connection if not use locally downloaded books
|
||||||
checkSetValidServerConnectionConfig { isConnected ->
|
checkSetValidServerConnectionConfig { isConnected ->
|
||||||
@@ -239,39 +294,29 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
loadLibraries { libraries ->
|
loadLibraries { libraries ->
|
||||||
if (libraries.isEmpty()) {
|
if (libraries.isEmpty()) {
|
||||||
Log.w(tag, "No libraries returned from server request")
|
Log.w(tag, "No libraries returned from server request")
|
||||||
cb(cats) // Return download category only
|
cb()
|
||||||
} else {
|
} else {
|
||||||
val library = libraries[0]
|
val library = libraries[0]
|
||||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||||
|
|
||||||
loadLibraryCategories(library.id) { libraryCategories ->
|
loadItemsInProgressForAllLibraries { itemsInProgress ->
|
||||||
|
itemsInProgress.forEach {
|
||||||
// Only using book or podcast library categories for now
|
val libraryItem = it.libraryItemWrapper as LibraryItem
|
||||||
libraryCategories.forEach {
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
|
serverLibraryItems.add(libraryItem)
|
||||||
// Add items in continue listening to serverLibraryItems
|
|
||||||
if (it.id == "continue-listening") {
|
|
||||||
it.entities.forEach { libraryItemWrapper ->
|
|
||||||
val libraryItem = libraryItemWrapper as LibraryItem
|
|
||||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
|
||||||
serverLibraryItems.add(libraryItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
if (it.episode != null) {
|
||||||
if (it.type == library.mediaType) {
|
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
|
||||||
// Log.d(tag, "Using library category ${it.id}")
|
|
||||||
cats.add(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(cats)
|
cb() // Fully loaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Not connected/no internet sent downloaded cats only
|
} else { // Not connected to server
|
||||||
cb(cats)
|
cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,12 +334,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
if (id.startsWith("local")) {
|
if (id.startsWith("local")) {
|
||||||
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
|
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
|
||||||
} else {
|
} else {
|
||||||
val podcastEpisode = serverPodcastEpisodes.find { it.id == id }
|
return podcastEpisodeLibraryItemMap[id]
|
||||||
return if (podcastEpisode != null && selectedLibraryItemWrapper != null) {
|
|
||||||
LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package com.audiobookshelf.app.player
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
@@ -42,13 +45,25 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
|
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
|
||||||
currentIconUri = albumArtUri
|
currentIconUri = albumArtUri
|
||||||
Log.d(tag, "ART $currentIconUri")
|
Log.d(tag, "ART $currentIconUri")
|
||||||
serviceScope.launch {
|
|
||||||
currentBitmap = albumArtUri?.let {
|
if (currentIconUri.toString().startsWith("content://")) {
|
||||||
resolveUriAsBitmap(it)
|
currentBitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(playerNotificationService.contentResolver, currentIconUri)
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(playerNotificationService.contentResolver, currentIconUri!!)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
}
|
}
|
||||||
currentBitmap?.let { callback.onBitmap(it) }
|
currentBitmap
|
||||||
|
} else {
|
||||||
|
serviceScope.launch {
|
||||||
|
currentBitmap = albumArtUri?.let {
|
||||||
|
resolveUriAsBitmap(it)
|
||||||
|
}
|
||||||
|
currentBitmap?.let { callback.onBitmap(it) }
|
||||||
|
}
|
||||||
|
null
|
||||||
}
|
}
|
||||||
null
|
|
||||||
} else {
|
} else {
|
||||||
currentBitmap
|
currentBitmap
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ import android.net.Uri
|
|||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.data.Library
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.data.LibraryCategory
|
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
|
||||||
import com.audiobookshelf.app.data.LocalLibraryItem
|
|
||||||
|
|
||||||
class BrowseTree(
|
class BrowseTree(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
libraryCategories: List<LibraryCategory>,
|
itemsInProgress: List<ItemInProgress>,
|
||||||
libraries: List<Library>
|
libraries: List<Library>
|
||||||
) {
|
) {
|
||||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
@@ -24,8 +21,7 @@ class BrowseTree(
|
|||||||
* @param drawableId - drawable res id
|
* @param drawableId - drawable res id
|
||||||
* @return - uri
|
* @return - uri
|
||||||
*/
|
*/
|
||||||
fun getUriToDrawable(context: Context,
|
fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
|
||||||
@AnyRes drawableId: Int): Uri {
|
|
||||||
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||||
@@ -35,38 +31,26 @@ class BrowseTree(
|
|||||||
init {
|
init {
|
||||||
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
|
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
|
||||||
|
|
||||||
val continueReadingMetadata = MediaMetadataCompat.Builder().apply {
|
val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val librariesMetadata = MediaMetadataCompat.Builder().apply {
|
val librariesMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
// Server continue Listening cat
|
if (!itemsInProgress.isEmpty()) {
|
||||||
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
rootList += continueListeningMetadata
|
||||||
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
|
||||||
val libraryItem = liw as LibraryItem
|
|
||||||
libraryItem.getMediaMetadata()
|
|
||||||
}
|
|
||||||
if (continueListeningMediaMetadata.isNotEmpty()) {
|
|
||||||
rootList += continueReadingMetadata
|
|
||||||
}
|
|
||||||
continueListeningMediaMetadata.forEach {
|
|
||||||
val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf()
|
|
||||||
children += it
|
|
||||||
mediaIdToChildren[CONTINUE_ROOT] = children
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraries.isNotEmpty()) {
|
if (libraries.isNotEmpty()) {
|
||||||
@@ -81,23 +65,6 @@ class BrowseTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
rootList += downloadsMetadata
|
rootList += downloadsMetadata
|
||||||
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
|
||||||
localBooksCat.entities.forEach { libc ->
|
|
||||||
val libraryItem = libc as LocalLibraryItem
|
|
||||||
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
|
||||||
children += libraryItem.getMediaMetadata(context)
|
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat ->
|
|
||||||
localPodcastsCat.entities.forEach { libc ->
|
|
||||||
val libraryItem = libc as LocalLibraryItem
|
|
||||||
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
|
||||||
children += libraryItem.getMediaMetadata(context)
|
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,14 +74,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(cb: () -> Unit) {
|
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
|
||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
listeningTimerTask = null
|
listeningTimerTask = null
|
||||||
listeningTimerRunning = false
|
listeningTimerRunning = false
|
||||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
||||||
if (currentTime > 0) { // Current time should always be > 0 on stop
|
if (currentTime > 0) { // Current time should always be > 0 on stop
|
||||||
sync(true, currentTime) {
|
sync(true, currentTime) {
|
||||||
reset()
|
reset()
|
||||||
@@ -115,7 +115,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finished(cb: () -> Unit) {
|
||||||
|
if (!listeningTimerRunning) return
|
||||||
|
listeningTimerTask?.cancel()
|
||||||
|
listeningTimerTask = null
|
||||||
|
listeningTimerRunning = false
|
||||||
|
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
|
sync(true, currentPlaybackSession?.duration ?: 0.0) {
|
||||||
|
reset()
|
||||||
|
cb()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.R
|
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -21,7 +19,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
|
|
||||||
private var mediaButtonClickCount: Int = 0
|
private var mediaButtonClickCount: Int = 0
|
||||||
var mediaButtonClickTimeout: Long = 1000 //ms
|
var mediaButtonClickTimeout: Long = 1000 //ms
|
||||||
var seekAmount: Long = 20000 //ms
|
|
||||||
|
|
||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
@@ -75,19 +72,19 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
playerNotificationService.seekBackward(seekAmount)
|
playerNotificationService.skipToPrevious()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.skipToNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFastForward() {
|
override fun onFastForward() {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpForward()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRewind() {
|
override fun onRewind() {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpBackward()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekTo(pos: Long) {
|
override fun onSeekTo(pos: Long) {
|
||||||
@@ -179,10 +176,16 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpForward()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
||||||
playerNotificationService.seekBackward(seekAmount)
|
playerNotificationService.jumpBackward()
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
|
playerNotificationService.jumpForward()
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
|
playerNotificationService.jumpBackward()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
||||||
playerNotificationService.closePlayback()
|
playerNotificationService.closePlayback()
|
||||||
@@ -226,22 +229,24 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun handleMessage(msg: Message) {
|
override fun handleMessage(msg: Message) {
|
||||||
super.handleMessage(msg)
|
super.handleMessage(msg)
|
||||||
if (2 == msg.what) {
|
if (2 == msg.what) {
|
||||||
playerNotificationService.seekBackward(seekAmount)
|
playerNotificationService.jumpBackward()
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
}
|
}
|
||||||
else if (msg.what >= 3) {
|
else if (msg.what >= 3) {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpForward()
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example Using a custom action in android auto
|
override fun onCustomAction(action: String?, extras: Bundle?) {
|
||||||
// override fun onCustomAction(action: String?, extras: Bundle?) {
|
super.onCustomAction(action, extras)
|
||||||
// super.onCustomAction(action, extras)
|
|
||||||
//
|
when (action) {
|
||||||
// if ("com.audiobookshelf.app.PLAYBACK_RATE" == action) {
|
CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
|
||||||
//
|
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
||||||
// }
|
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
||||||
// }
|
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
|
const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
|
||||||
|
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
||||||
|
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
||||||
|
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
||||||
@@ -52,6 +52,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
||||||
Log.d(tag, "STATE_ENDED")
|
Log.d(tag, "STATE_ENDED")
|
||||||
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
||||||
|
|
||||||
|
playerNotificationService.handlePlaybackEnded()
|
||||||
}
|
}
|
||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
||||||
Log.d(tag, "STATE_IDLE")
|
Log.d(tag, "STATE_IDLE")
|
||||||
|
|||||||
@@ -29,9 +29,14 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio
|
|||||||
Log.d(tag, "onNotificationCancelled not dismissed by user")
|
Log.d(tag, "onNotificationCancelled not dismissed by user")
|
||||||
|
|
||||||
// When stop button is pressed on the notification I guess it isn't considered "dismissedByUser" so we need to close playback ourselves
|
// When stop button is pressed on the notification I guess it isn't considered "dismissedByUser" so we need to close playback ourselves
|
||||||
if (!PlayerNotificationService.isClosed) {
|
if (!PlayerNotificationService.isClosed && !PlayerNotificationService.isSwitchingPlayer) {
|
||||||
Log.d(tag, "PNS is not closed - closing it now")
|
Log.d(tag, "PNS is not closed - closing it now")
|
||||||
playerNotificationService.closePlayback()
|
playerNotificationService.closePlayback()
|
||||||
|
} else if (PlayerNotificationService.isSwitchingPlayer) {
|
||||||
|
// When switching from cast player to exo player and vice versa the notification is cancelled and posted again
|
||||||
|
// so we don't want to cancel the playback during this switch
|
||||||
|
Log.d(tag, "PNS is switching player")
|
||||||
|
PlayerNotificationService.isSwitchingPlayer = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.audiobookshelf.app.player
|
|||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.hardware.Sensor
|
import android.hardware.Sensor
|
||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@@ -11,8 +13,10 @@ import android.net.Network
|
|||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
@@ -20,7 +24,6 @@ import android.util.Log
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
import com.audiobookshelf.app.BuildConfig
|
import com.audiobookshelf.app.BuildConfig
|
||||||
@@ -30,9 +33,11 @@ import com.audiobookshelf.app.data.DeviceInfo
|
|||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.CustomActionProvider
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||||
import com.google.android.exoplayer2.source.MediaSource
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
@@ -42,7 +47,10 @@ import com.google.android.exoplayer2.upstream.*
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
|
||||||
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
||||||
|
const val PLAYER_CAST = "cast-player";
|
||||||
|
const val PLAYER_EXO = "exo-player";
|
||||||
|
|
||||||
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
@@ -50,6 +58,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
var isStarted = false
|
var isStarted = false
|
||||||
var isClosed = false
|
var isClosed = false
|
||||||
var isUnmeteredNetwork = false
|
var isUnmeteredNetwork = false
|
||||||
|
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientEventEmitter {
|
interface ClientEventEmitter {
|
||||||
@@ -147,6 +156,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
// detach player
|
// detach player
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
try {
|
||||||
|
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||||
|
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||||
|
} catch(error:Exception) {
|
||||||
|
Log.e(tag, "Error unregistering network listening callback $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "onDestroy")
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
mPlayer.release()
|
mPlayer.release()
|
||||||
castPlayer?.release()
|
castPlayer?.release()
|
||||||
@@ -237,6 +254,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
|
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
playerNotificationManager.setUseFastForwardActionInCompactView(true)
|
playerNotificationManager.setUseFastForwardActionInCompactView(true)
|
||||||
playerNotificationManager.setUseRewindActionInCompactView(true)
|
playerNotificationManager.setUseRewindActionInCompactView(true)
|
||||||
|
playerNotificationManager.setSmallIcon(R.drawable.exo_icon_localaudio)
|
||||||
|
|
||||||
// Unknown action
|
// Unknown action
|
||||||
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||||
@@ -245,6 +263,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
|
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||||
|
return PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
if (currentPlaybackSession == null) {
|
if (currentPlaybackSession == null) {
|
||||||
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
|
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
|
||||||
@@ -253,19 +275,42 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
val coverUri = currentPlaybackSession!!.getCoverUri()
|
val coverUri = currentPlaybackSession!!.getCoverUri()
|
||||||
|
|
||||||
|
var bitmap:Bitmap? = null
|
||||||
|
// Local covers get bitmap
|
||||||
|
if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) {
|
||||||
|
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fix for local images crashing on Android 11 for specific devices
|
// Fix for local images crashing on Android 11 for specific devices
|
||||||
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
|
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
|
||||||
ctx.grantUriPermission(
|
try {
|
||||||
"com.android.systemui",
|
ctx.grantUriPermission(
|
||||||
coverUri,
|
"com.android.systemui",
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
coverUri,
|
||||||
)
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
} catch(error:Exception) {
|
||||||
|
Log.e(tag, "Grant uri permission error $error")
|
||||||
|
}
|
||||||
|
|
||||||
return MediaDescriptionCompat.Builder()
|
val extra = Bundle()
|
||||||
.setMediaId(currentPlaybackSession!!.id)
|
extra.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentPlaybackSession!!.displayAuthor)
|
||||||
|
|
||||||
|
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
|
.setExtras(extra)
|
||||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||||
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
.setIconUri(coverUri)
|
||||||
.setIconUri(coverUri).build()
|
|
||||||
|
bitmap?.let {
|
||||||
|
mediaDescriptionBuilder.setIconBitmap(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaDescriptionBuilder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,18 +326,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
||||||
|
|
||||||
// Example adding custom action with icon in android auto
|
|
||||||
// mediaSessionConnector.setCustomActionProviders(object : MediaSessionConnector.CustomActionProvider {
|
|
||||||
// override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
|
||||||
// }
|
|
||||||
// override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
|
||||||
// var icon = R.drawable.exo_icon_rewind
|
|
||||||
// return PlaybackStateCompat.CustomAction.Builder(
|
|
||||||
// "com.audiobookshelf.app.PLAYBACK_RATE", "Playback Rate", icon)
|
|
||||||
// .build()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
mediaSession.setCallback(MediaSessionCallback(this))
|
mediaSession.setCallback(MediaSessionCallback(this))
|
||||||
|
|
||||||
initializeMPlayer()
|
initializeMPlayer()
|
||||||
@@ -307,15 +340,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
1000 * 20 // 20s playback rebuffer
|
1000 * 20 // 20s playback rebuffer
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val seekBackTime = DeviceManager.deviceData.deviceSettings?.jumpBackwardsTimeMs ?: 10000
|
|
||||||
val seekForwardTime = DeviceManager.deviceData.deviceSettings?.jumpForwardTimeMs ?: 10000
|
|
||||||
Log.d(tag, "Seek Back Time $seekBackTime")
|
|
||||||
Log.d(tag, "Seek Forward Time $seekForwardTime")
|
|
||||||
|
|
||||||
mPlayer = ExoPlayer.Builder(this)
|
mPlayer = ExoPlayer.Builder(this)
|
||||||
.setLoadControl(customLoadControl)
|
.setLoadControl(customLoadControl)
|
||||||
.setSeekBackIncrementMs(seekBackTime)
|
.setSeekBackIncrementMs(deviceSettings.jumpBackwardsTimeMs)
|
||||||
.setSeekForwardIncrementMs(seekForwardTime)
|
.setSeekForwardIncrementMs(deviceSettings.jumpForwardTimeMs)
|
||||||
.build()
|
.build()
|
||||||
mPlayer.setHandleAudioBecomingNoisy(true)
|
mPlayer.setHandleAudioBecomingNoisy(true)
|
||||||
mPlayer.addListener(PlayerListener(this))
|
mPlayer.addListener(PlayerListener(this))
|
||||||
@@ -339,20 +367,43 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: When an item isFinished the currentTime should be reset to 0
|
||||||
|
// will reset the time if currentTime is within 5s of duration (for android auto)
|
||||||
|
Log.d(tag, "Prepare Player Session Current Time=${playbackSession.currentTime}, Duration=${playbackSession.duration}")
|
||||||
|
if (playbackSession.duration - playbackSession.currentTime < 5) {
|
||||||
|
Log.d(tag, "Prepare Player Session is finished, so restart it")
|
||||||
|
playbackSession.currentTime = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
isClosed = false
|
isClosed = false
|
||||||
|
val customActionProviders = mutableListOf(
|
||||||
|
JumpBackwardCustomActionProvider(),
|
||||||
|
JumpForwardCustomActionProvider(),
|
||||||
|
)
|
||||||
|
val metadata = playbackSession.getMediaMetadataCompat(ctx)
|
||||||
|
mediaSession.setMetadata(metadata)
|
||||||
|
val mediaItems = playbackSession.getMediaItems()
|
||||||
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
||||||
initialPlaybackRate = playbackRate
|
initialPlaybackRate = playbackRate
|
||||||
|
|
||||||
|
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
||||||
|
customActionProviders.addAll(listOf(
|
||||||
|
SkipBackwardCustomActionProvider(),
|
||||||
|
SkipForwardCustomActionProvider(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray());
|
||||||
|
|
||||||
playbackSession.mediaPlayer = getMediaPlayer()
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
|
|
||||||
if (playbackSession.mediaPlayer == "cast-player" && playbackSession.isLocal) {
|
if (playbackSession.mediaPlayer == PLAYER_CAST && playbackSession.isLocal) {
|
||||||
Log.w(tag, "Cannot cast local media item - switching player")
|
Log.w(tag, "Cannot cast local media item - switching player")
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
switchToPlayer(false)
|
switchToPlayer(false)
|
||||||
playbackSession.mediaPlayer = getMediaPlayer()
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackSession.mediaPlayer == "cast-player") {
|
if (playbackSession.mediaPlayer == PLAYER_CAST) {
|
||||||
// If cast-player is the first player to be used
|
// If cast-player is the first player to be used
|
||||||
mediaSessionConnector.setPlayer(castPlayer)
|
mediaSessionConnector.setPlayer(castPlayer)
|
||||||
playerNotificationManager.setPlayer(castPlayer)
|
playerNotificationManager.setPlayer(castPlayer)
|
||||||
@@ -363,10 +414,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
val metadata = playbackSession.getMediaMetadataCompat()
|
|
||||||
mediaSession.setMetadata(metadata)
|
|
||||||
val mediaItems = playbackSession.getMediaItems()
|
|
||||||
|
|
||||||
if (mediaItems.isEmpty()) {
|
if (mediaItems.isEmpty()) {
|
||||||
Log.e(tag, "Invalid playback session no media items to play")
|
Log.e(tag, "Invalid playback session no media items to play")
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
@@ -413,7 +460,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
||||||
|
|
||||||
currentPlayer.prepare()
|
currentPlayer.prepare()
|
||||||
|
|
||||||
} else if (castPlayer != null) {
|
} else if (castPlayer != null) {
|
||||||
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
||||||
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
||||||
@@ -436,7 +482,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
if (it == null) { // Play request failed
|
if (it == null) { // Play request failed
|
||||||
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
||||||
closePlayback()
|
closePlayback(true)
|
||||||
} else {
|
} else {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
preparePlayer(it, true, null)
|
preparePlayer(it, true, null)
|
||||||
@@ -445,7 +491,37 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
||||||
closePlayback()
|
closePlayback(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handlePlaybackEnded() {
|
||||||
|
Log.d(tag, "handlePlaybackEnded")
|
||||||
|
if (isAndroidAuto && currentPlaybackSession?.isPodcastEpisode == true) {
|
||||||
|
Log.d(tag, "Podcast playback ended on android auto")
|
||||||
|
val libraryItem = currentPlaybackSession?.libraryItem ?: return
|
||||||
|
|
||||||
|
// Need to sync with server to set as finished
|
||||||
|
mediaProgressSyncer.finished {
|
||||||
|
// Need to reload media progress
|
||||||
|
mediaManager.loadServerUserMediaProgress {
|
||||||
|
val podcast = libraryItem.media as Podcast
|
||||||
|
val nextEpisode = podcast.getNextUnfinishedEpisode(libraryItem.id, mediaManager)
|
||||||
|
Log.d(tag, "handlePlaybackEnded nextEpisode=$nextEpisode")
|
||||||
|
nextEpisode?.let { podcastEpisode ->
|
||||||
|
mediaManager.play(libraryItem, podcastEpisode, getPlayItemRequestPayload(false)) {
|
||||||
|
if (it == null) {
|
||||||
|
Log.e(tag, "Failed to play library item")
|
||||||
|
} else {
|
||||||
|
val playbackRate = mediaManager.getSavedPlaybackRate()
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
preparePlayer(it,true, playbackRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,6 +565,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentPlaybackSession == null) {
|
||||||
|
Log.e(tag, "switchToPlayer: No Current playback session")
|
||||||
|
} else {
|
||||||
|
isSwitchingPlayer = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playback session in progress syncer is a copy that is up-to-date so replace current here with that
|
||||||
|
// TODO: bad design here implemented to prevent the session in MediaProgressSyncer from changing while syncing
|
||||||
|
if (mediaProgressSyncer.currentPlaybackSession != null) {
|
||||||
|
currentPlaybackSession = mediaProgressSyncer.currentPlaybackSession?.clone()
|
||||||
|
}
|
||||||
|
|
||||||
currentPlayer = if (useCastPlayer) {
|
currentPlayer = if (useCastPlayer) {
|
||||||
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
|
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
|
||||||
mediaSessionConnector.setPlayer(castPlayer)
|
mediaSessionConnector.setPlayer(castPlayer)
|
||||||
@@ -503,15 +591,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer())
|
clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer())
|
||||||
|
|
||||||
if (currentPlaybackSession == null) {
|
|
||||||
Log.d(tag, "switchToPlayer: No Current playback session")
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}")
|
Log.d(tag, "switchToPlayer: Starting new playback session ${it.displayTitle}")
|
||||||
if (wasPlaying) { // media is paused when switching players
|
if (wasPlaying) { // media is paused when switching players
|
||||||
clientEventEmitter?.onPlayingUpdate(false)
|
clientEventEmitter?.onPlayingUpdate(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Start a new playback session here instead of using the existing
|
||||||
preparePlayer(it, false, null)
|
preparePlayer(it, false, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,10 +634,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return currentPlaybackSession?.totalDurationMs ?: 0L
|
return currentPlaybackSession?.totalDurationMs ?: 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentBookTitle() : String? {
|
|
||||||
return currentPlaybackSession?.displayTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentPlaybackSessionCopy() :PlaybackSession? {
|
fun getCurrentPlaybackSessionCopy() :PlaybackSession? {
|
||||||
return currentPlaybackSession?.clone()
|
return currentPlaybackSession?.clone()
|
||||||
}
|
}
|
||||||
@@ -619,7 +701,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// Streaming from server so check if playback session still exists on server
|
// Streaming from server so check if playback session still exists on server
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
"checkCurrentSessionProgress: Checking if playback session for server stream is still available"
|
"checkCurrentSessionProgress: Checking if playback session ${playbackSession.id} for server stream is still available"
|
||||||
)
|
)
|
||||||
apiHandler.getPlaybackSession(playbackSession.id) {
|
apiHandler.getPlaybackSession(playbackSession.id) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
@@ -656,10 +738,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentPlayer.volume = 1F
|
currentPlayer.volume = 1F
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
Log.d(tag, "CAST Player set on play ${currentPlayer.isLoading} || ${currentPlayer.duration} | ${currentPlayer.currentPosition}")
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPlayer.play()
|
currentPlayer.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,17 +756,42 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun seekPlayer(time: Long) {
|
fun seekPlayer(time: Long) {
|
||||||
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $time")
|
var timeToSeek = time
|
||||||
|
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $timeToSeek")
|
||||||
|
if (timeToSeek < 0) {
|
||||||
|
Log.w(tag, "seekPlayer invalid time ${timeToSeek} - setting to 0")
|
||||||
|
timeToSeek = 0L
|
||||||
|
} else if (timeToSeek > getDuration()) {
|
||||||
|
Log.w(tag, "seekPlayer invalid time ${timeToSeek} - setting to MAX - 2000")
|
||||||
|
timeToSeek = getDuration() - 2000L
|
||||||
|
}
|
||||||
|
|
||||||
if (currentPlayer.mediaItemCount > 1) {
|
if (currentPlayer.mediaItemCount > 1) {
|
||||||
currentPlaybackSession?.currentTime = time / 1000.0
|
currentPlaybackSession?.currentTime = timeToSeek / 1000.0
|
||||||
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
||||||
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
|
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
|
||||||
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(time)
|
currentPlayer.seekTo(timeToSeek)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun skipToPrevious() {
|
||||||
|
currentPlayer.seekToPrevious()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skipToNext() {
|
||||||
|
currentPlayer.seekToNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jumpForward() {
|
||||||
|
seekForward(deviceSettings.jumpForwardTimeMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jumpBackward() {
|
||||||
|
seekBackward(deviceSettings.jumpBackwardsTimeMs)
|
||||||
|
}
|
||||||
|
|
||||||
fun seekForward(amount: Long) {
|
fun seekForward(amount: Long) {
|
||||||
seekPlayer(getCurrentTime() + amount)
|
seekPlayer(getCurrentTime() + amount)
|
||||||
}
|
}
|
||||||
@@ -701,12 +804,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
currentPlayer.setPlaybackSpeed(speed)
|
currentPlayer.setPlaybackSpeed(speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closePlayback() {
|
fun closePlayback(calledOnError:Boolean? = false) {
|
||||||
Log.d(tag, "closePlayback")
|
Log.d(tag, "closePlayback")
|
||||||
if (mediaProgressSyncer.listeningTimerRunning) {
|
if (mediaProgressSyncer.listeningTimerRunning) {
|
||||||
Log.i(tag, "About to close playback so stopping media progress syncer first")
|
Log.i(tag, "About to close playback so stopping media progress syncer first")
|
||||||
mediaProgressSyncer.stop {
|
mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception)
|
||||||
Log.d(tag, "Media Progress syncer stopped and synced")
|
Log.d(tag, "Media Progress syncer stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,7 +834,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getMediaPlayer():String {
|
fun getMediaPlayer():String {
|
||||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDeviceInfo(): DeviceInfo {
|
fun getDeviceInfo(): DeviceInfo {
|
||||||
@@ -745,6 +848,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
||||||
|
|
||||||
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
||||||
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
||||||
}
|
}
|
||||||
@@ -772,8 +878,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
|
private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
|
||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
private val ALL_ROOT = "__ALL__"
|
|
||||||
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
|
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
|
private val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
|
|
||||||
|
|
||||||
@@ -816,56 +923,78 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
||||||
|
|
||||||
var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
|
||||||
|
|
||||||
result.detach()
|
result.detach()
|
||||||
|
|
||||||
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
||||||
Log.d(tag, "Loading podcast episodes")
|
Log.d(tag, "Loading podcast episodes")
|
||||||
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) {
|
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId, ctx) {
|
||||||
result.sendResult(it)
|
result.sendResult(it)
|
||||||
}
|
}
|
||||||
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||||
|
|
||||||
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||||
val children = libraryItems.map { libraryItem ->
|
val children = libraryItems.map { libraryItem ->
|
||||||
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
|
|
||||||
|
|
||||||
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||||
flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
} else {
|
||||||
|
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||||
|
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||||
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag)
|
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
}
|
}
|
||||||
} else if (parentMediaId == "__DOWNLOADS__") { // Load downloads
|
} else if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads
|
||||||
|
|
||||||
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||||
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
localBooks.forEach { localLibraryItem ->
|
localBooks.forEach { localLibraryItem ->
|
||||||
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
|
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
||||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
val description = localLibraryItem.getMediaDescription(progress, ctx)
|
||||||
|
|
||||||
|
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
localPodcasts.forEach { localLibraryItem ->
|
localPodcasts.forEach { localLibraryItem ->
|
||||||
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
|
val mediaDescription = localLibraryItem.getMediaDescription(null, ctx)
|
||||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.sendResult(localBrowseItems)
|
result.sendResult(localBrowseItems)
|
||||||
|
|
||||||
} else { // Load categories
|
} else if (parentMediaId == CONTINUE_ROOT) {
|
||||||
|
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
mediaManager.serverItemsInProgress.forEach { itemInProgress ->
|
||||||
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
val progress: MediaProgressWrapper?
|
||||||
|
val mediaDescription:MediaDescriptionCompat
|
||||||
|
if (itemInProgress.episode != null) {
|
||||||
|
if (itemInProgress.isLocal) {
|
||||||
|
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
|
||||||
|
} else {
|
||||||
|
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id && it.episodeId == itemInProgress.episode.id }
|
||||||
|
}
|
||||||
|
mediaDescription = itemInProgress.episode.getMediaDescription(itemInProgress.libraryItemWrapper, progress, ctx)
|
||||||
|
} else {
|
||||||
|
if (itemInProgress.isLocal) {
|
||||||
|
progress = DeviceManager.dbManager.getLocalMediaProgress(itemInProgress.libraryItemWrapper.id)
|
||||||
|
} else {
|
||||||
|
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id }
|
||||||
|
}
|
||||||
|
mediaDescription = itemInProgress.libraryItemWrapper.getMediaDescription(progress, ctx)
|
||||||
|
}
|
||||||
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
result.sendResult(localBrowseItems)
|
||||||
|
} else {
|
||||||
|
mediaManager.loadAndroidAutoItems {
|
||||||
|
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
||||||
|
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag")
|
Log.d(tag, "Loading Browser Media Item ${item.description.title}")
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
}
|
}
|
||||||
@@ -874,10 +1003,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
result.detach()
|
result.detach()
|
||||||
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
mediaManager.loadAndroidAutoItems {
|
||||||
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
||||||
val children = browseTree[ALL_ROOT]?.map { item ->
|
val children = browseTree[LIBRARIES_ROOT]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
}
|
}
|
||||||
@@ -944,5 +1073,73 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
|
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class JumpBackwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_JUMP_BACKWARD,
|
||||||
|
getContext().getString(R.string.action_jump_backward),
|
||||||
|
R.drawable.exo_icon_rewind
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class JumpForwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_JUMP_FORWARD,
|
||||||
|
getContext().getString(R.string.action_jump_forward),
|
||||||
|
R.drawable.exo_icon_fastforward
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class SkipForwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_SKIP_FORWARD,
|
||||||
|
getContext().getString(R.string.action_skip_forward),
|
||||||
|
R.drawable.skip_next_24
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class SkipBackwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_SKIP_BACKWARD,
|
||||||
|
getContext().getString(R.string.action_skip_backward),
|
||||||
|
R.drawable.skip_previous_24
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.audiobookshelf.app.player
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -203,8 +204,13 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleShake() {
|
fun handleShake() {
|
||||||
Log.d(tag, "HANDLE SHAKE HERE")
|
if (sleepTimerRunning || sleepTimerFinishedAt > 0L) {
|
||||||
if (sleepTimerRunning || sleepTimerFinishedAt > 0L) checkShouldExtendSleepTimer()
|
if (DeviceManager.deviceData.deviceSettings?.disableShakeToResetSleepTimer == true) {
|
||||||
|
Log.d(tag, "Shake to reset sleep timer is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checkShouldExtendSleepTimer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun increaseSleepTime(time: Long) {
|
fun increaseSleepTime(time: Long) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.getcapacitor.annotation.CapacitorPlugin
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsDatabase")
|
@CapacitorPlugin(name = "AbsDatabase")
|
||||||
class AbsDatabase : Plugin() {
|
class AbsDatabase : Plugin() {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.getcapacitor.JSObject
|
|||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -43,11 +44,13 @@ class ApiHandler(var ctx:Context) {
|
|||||||
makeRequest(request, httpClient, cb)
|
makeRequest(request, httpClient, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
fun postRequest(endpoint:String, payload: JSObject, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||||
|
val address = config?.address ?: DeviceManager.serverAddress
|
||||||
|
val token = config?.token ?: DeviceManager.token
|
||||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
val requestBody = payload.toString().toRequestBody(mediaType)
|
val requestBody = payload.toString().toRequestBody(mediaType)
|
||||||
val request = Request.Builder().post(requestBody)
|
val request = Request.Builder().post(requestBody)
|
||||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
.url("${address}$endpoint").addHeader("Authorization", "Bearer ${token}")
|
||||||
.build()
|
.build()
|
||||||
makeRequest(request, null, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
@@ -135,12 +138,16 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val mapper = jacksonMapper
|
val mapper = jacksonMapper
|
||||||
getRequest("/api/libraries", null,null) {
|
getRequest("/api/libraries", null,null) {
|
||||||
val libraries = mutableListOf<Library>()
|
val libraries = mutableListOf<Library>()
|
||||||
if (it.has("value")) {
|
|
||||||
val array = it.getJSONArray("value")
|
var array = JSONArray()
|
||||||
for (i in 0 until array.length()) {
|
if (it.has("libraries")) { // TODO: Server 2.2.9 changed to this
|
||||||
val library = mapper.readValue<Library>(array.get(i).toString())
|
array = it.getJSONArray("libraries")
|
||||||
libraries.add(library)
|
} else if (it.has("value")) {
|
||||||
}
|
array = it.getJSONArray("value")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until array.length()) {
|
||||||
|
libraries.add(mapper.readValue(array.get(i).toString()))
|
||||||
}
|
}
|
||||||
cb(libraries)
|
cb(libraries)
|
||||||
}
|
}
|
||||||
@@ -186,21 +193,16 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
|
||||||
getRequest("/api/libraries/$libraryId/personalized", null, null) {
|
getRequest("/api/me/items-in-progress", null, null) {
|
||||||
val items = mutableListOf<LibraryCategory>()
|
val items = mutableListOf<ItemInProgress>()
|
||||||
if (it.has("value")) {
|
if (it.has("libraryItems")) {
|
||||||
val array = it.getJSONArray("value")
|
val array = it.getJSONArray("libraryItems")
|
||||||
for (i in 0 until array.length()) {
|
for (i in 0 until array.length()) {
|
||||||
val jsobj = array.get(i) as JSONObject
|
val jsobj = array.get(i) as JSONObject
|
||||||
|
|
||||||
val type = jsobj.get("type").toString()
|
val itemInProgress = ItemInProgress.makeFromServerObject(jsobj)
|
||||||
// Only support for podcast and book in android auto
|
items.add(itemInProgress)
|
||||||
if (type == "podcast" || type == "book") {
|
|
||||||
jsobj.put("isLocal", false)
|
|
||||||
val item = jacksonMapper.readValue<LibraryCategory>(jsobj.toString())
|
|
||||||
items.add(item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cb(items)
|
cb(items)
|
||||||
@@ -211,7 +213,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload))
|
val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload))
|
||||||
|
|
||||||
val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId"
|
val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId"
|
||||||
postRequest(endpoint, payload) {
|
postRequest(endpoint, payload, null) {
|
||||||
if (it.has("error")) {
|
if (it.has("error")) {
|
||||||
Log.e(tag, it.getString("error") ?: "Play Library Item Failed")
|
Log.e(tag, it.getString("error") ?: "Play Library Item Failed")
|
||||||
cb(null)
|
cb(null)
|
||||||
@@ -227,7 +229,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
|
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
||||||
|
|
||||||
postRequest("/api/session/$sessionId/sync", payload) {
|
postRequest("/api/session/$sessionId/sync", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
cb(false)
|
cb(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -239,7 +241,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
|
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||||
|
|
||||||
postRequest("/api/session/local", payload) {
|
postRequest("/api/session/local", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
cb(false)
|
cb(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -265,7 +267,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
if (localMediaProgress.isNotEmpty()) {
|
if (localMediaProgress.isNotEmpty()) {
|
||||||
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
|
val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
|
||||||
postRequest("/api/me/sync-local-progress", payload) {
|
postRequest("/api/me/sync-local-progress", payload, null) {
|
||||||
Log.d(tag, "Media Progress Sync payload $payload - response ${it}")
|
Log.d(tag, "Media Progress Sync payload $payload - response ${it}")
|
||||||
|
|
||||||
if (it.toString() == "{}") {
|
if (it.toString() == "{}") {
|
||||||
@@ -319,6 +321,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
||||||
|
Log.d(tag, "getPlaybackSession for $playbackSessionId for server ${DeviceManager.serverAddress}")
|
||||||
val endpoint = "/api/session/$playbackSessionId"
|
val endpoint = "/api/session/$playbackSessionId"
|
||||||
getRequest(endpoint, null, null) {
|
getRequest(endpoint, null, null) {
|
||||||
val err = it.getString("error")
|
val err = it.getString("error")
|
||||||
@@ -343,4 +346,25 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun authorize(config:ServerConnectionConfig, cb: (MutableList<MediaProgress>?) -> Unit) {
|
||||||
|
Log.d(tag, "authorize: Authorizing ${config.address}")
|
||||||
|
postRequest("/api/authorize", JSObject(), config) {
|
||||||
|
val error = it.getString("error")
|
||||||
|
if (!error.isNullOrEmpty()) {
|
||||||
|
Log.d(tag, "authorize: Authorize ${config.address} Failed: $error")
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
val mediaProgressList:MutableList<MediaProgress> = mutableListOf()
|
||||||
|
val user = it.getJSObject("user")
|
||||||
|
val mediaProgress = user?.getJSONArray("mediaProgress") ?: JSONArray()
|
||||||
|
for (i in 0 until mediaProgress.length()) {
|
||||||
|
val mediaProg = jacksonMapper.readValue<MediaProgress>(mediaProgress.getJSONObject(i).toString())
|
||||||
|
mediaProgressList.add(mediaProg)
|
||||||
|
}
|
||||||
|
Log.d(tag, "authorize: Authorize ${config.address} Successful")
|
||||||
|
cb(mediaProgressList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.127451"
|
||||||
|
android:scaleY="1.127451"
|
||||||
|
android:translateX="-1.5294118"
|
||||||
|
android:translateY="-1.5294118">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.127451"
|
||||||
|
android:scaleY="1.127451"
|
||||||
|
android:translateX="-1.5294118"
|
||||||
|
android:translateY="-1.5294118">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 101 KiB |
@@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="78.5885"
|
|
||||||
android:endY="90.9159"
|
|
||||||
android:startX="48.7653"
|
|
||||||
android:startY="61.0927"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#26A69A"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,4 +1,25 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:statusBarColor">@color/background_dark</item>
|
||||||
|
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
||||||
|
<!-- <item name="android:background">@drawable/screen</item>-->
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
<item name="android:background">@drawable/app_widget_background</item>
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
<color name="light_blue_200">#FF81D4FA</color>
|
<color name="light_blue_200">#FF81D4FA</color>
|
||||||
<color name="light_blue_600">#FF039BE5</color>
|
<color name="light_blue_600">#FF039BE5</color>
|
||||||
<color name="light_blue_900">#FF01579B</color>
|
<color name="light_blue_900">#FF01579B</color>
|
||||||
|
<color name="background_dark">#262626</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -6,4 +6,8 @@
|
|||||||
<string name="custom_url_scheme">com.audiobookshelf.app</string>
|
<string name="custom_url_scheme">com.audiobookshelf.app</string>
|
||||||
<string name="add_widget">Add widget</string>
|
<string name="add_widget">Add widget</string>
|
||||||
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
||||||
|
<string name="action_jump_forward">Jump Forward</string>
|
||||||
|
<string name="action_jump_backward">Jump Backward</string>
|
||||||
|
<string name="action_skip_forward">Skip Forward</string>
|
||||||
|
<string name="action_skip_backward">Skip Backward</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:statusBarColor">@color/background_dark</item>
|
||||||
|
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
||||||
<item name="android:background">@drawable/screen</item>
|
<!-- <item name="android:background">@drawable/screen</item>-->
|
||||||
</style>
|
</style>
|
||||||
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
<item name="android:id">@android:id/background</item>
|
<item name="android:id">@android:id/background</item>
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||||
<access origin="*" />
|
<access origin="*" />
|
||||||
|
|
||||||
|
<feature name="CDVOrientation">
|
||||||
|
<param name="android-package" value="cordova.plugins.screenorientation.CDVOrientation"/>
|
||||||
|
</feature>
|
||||||
|
|
||||||
|
|
||||||
</widget>
|
</widget>
|
||||||
@@ -8,8 +8,8 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.google.gms:google-services:4.3.5'
|
classpath 'com.google.gms:google-services:4.3.10'
|
||||||
classpath 'com.android.tools.build:gradle:7.2.0-beta04'
|
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
minSdkVersion = 24
|
minSdkVersion = 24
|
||||||
compileSdkVersion = 31
|
compileSdkVersion = 31
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 31
|
||||||
androidxActivityVersion = '1.2.0'
|
androidxActivityVersion = '1.2.0'
|
||||||
androidxAppCompatVersion = '1.4.1'
|
androidxAppCompatVersion = '1.4.1'
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'absicons';
|
||||||
|
src: url('~static/fonts/absicons/absicons.eot?2jfq33');
|
||||||
|
src: url('~static/fonts/absicons/absicons.eot?2jfq33#iefix') format('embedded-opentype'),
|
||||||
|
url('~static/fonts/absicons/absicons.ttf?2jfq33') format('truetype'),
|
||||||
|
url('~static/fonts/absicons/absicons.woff?2jfq33') format('woff'),
|
||||||
|
url('~static/fonts/absicons/absicons.svg?2jfq33#absicons') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abs-icons {
|
||||||
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
|
font-family: 'absicons' !important;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-list:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-home:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-authors:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-books-1:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-microphone-1:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-radio:before {
|
||||||
|
content: "\e903";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-podcast:before {
|
||||||
|
content: "\e904";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-audiobookshelf:before {
|
||||||
|
content: "\e900";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-database:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-microphone-2:before {
|
||||||
|
content: "\e901";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-columns:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-headphones:before {
|
||||||
|
content: "\e910";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-music:before {
|
||||||
|
content: "\e911";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-video:before {
|
||||||
|
content: "\e914";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-microphone-3:before {
|
||||||
|
content: "\e91e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-book-1:before {
|
||||||
|
content: "\e91f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-books-2:before {
|
||||||
|
content: "\e920";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-file-picture:before {
|
||||||
|
content: "\e927";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-database-1:before {
|
||||||
|
content: "\e964";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rocket:before {
|
||||||
|
content: "\e9a5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-power:before {
|
||||||
|
content: "\e9b5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-star:before {
|
||||||
|
content: "\e9d9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-heart:before {
|
||||||
|
content: "\e9da";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rss:before {
|
||||||
|
content: "\ea9b";
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
@import './defaultStyles.css';
|
@import './defaultStyles.css';
|
||||||
|
@import './absicons.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div v-if="user && currentLibrary">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||||
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
<widgets-download-progress-indicator />
|
<widgets-download-progress-indicator />
|
||||||
|
|
||||||
<!-- Must be connected to a server to cast, only supports media items on server -->
|
<!-- Must be connected to a server to cast, only supports media items on server -->
|
||||||
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
|
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer flex items-center pt-0.5" @click="castClick">
|
||||||
<span class="material-icons text-2xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
<span class="material-icons" :class="isCasting ? 'text-success' : ''">cast</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="user" class="h-7 mx-1.5" to="/search">
|
<nuxt-link v-if="user" class="h-7 mx-1.5" style="padding-top: 3px" to="/search">
|
||||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
<span class="material-icons">search</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<div class="h-7 mx-1.5">
|
<div class="h-7 mx-1.5">
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="playbackSession" id="streamContainer" class="playerContainer fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
|
<div v-if="playbackSession" id="streamContainer" class="playerContainer fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
|
||||||
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
||||||
<div class="top-2 left-4 absolute cursor-pointer">
|
<div class="top-4 left-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showCastBtn" class="top-4 right-16 absolute cursor-pointer">
|
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-4 right-4 absolute cursor-pointer">
|
<div class="top-6 right-4 absolute cursor-pointer">
|
||||||
<ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction">
|
<span class="material-icons text-3xl" @click="showMoreMenuDialog = true">more_vert</span>
|
||||||
<span class="material-icons text-3xl">more_vert</span>
|
|
||||||
</ui-dropdown-menu>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="top-2 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
||||||
@@ -48,7 +46,7 @@
|
|||||||
<div id="playerContent" class="playerContainer w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
<div id="playerContent" class="playerContainer w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
||||||
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<span v-if="!isPodcast && !isLocalPlayMethod" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
<span v-if="!isPodcast && isServerItem && networkConnected" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
<!-- hidden for podcasts but still using this as a placeholder -->
|
<!-- hidden for podcasts but still using this as a placeholder -->
|
||||||
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
|
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
|
||||||
|
|
||||||
@@ -66,23 +64,24 @@
|
|||||||
|
|
||||||
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4" :class="{ 'animate-spin': seekLoading, 'bg-accent': !isLocalPlayMethod, 'bg-success': isLocalPlayMethod }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4" :class="{ 'animate-spin': seekLoading, 'bg-accent': !isLocalPlayMethod, 'bg-success': isLocalPlayMethod }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
||||||
<widgets-spinner-icon v-else class="h-8 w-8" />
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
<div id="playerTrack" class="absolute left-0 w-full px-3">
|
||||||
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="isLoading ? 'animate-pulse' : ''" @touchstart="touchstartTrack" @click="clickTrack">
|
<div ref="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative" :class="{ 'animate-pulse': isLoading }" @touchstart="touchstartTrack" @click="clickTrack">
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
|
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
<div ref="trackCursor" class="h-3.5 w-3.5 rounded-full bg-gray-200 absolute -top-1 pointer-events-none" :class="{ 'opacity-0': lockUi || !showFullscreen }" />
|
||||||
</div>
|
</div>
|
||||||
<div id="timestamp-row" class="flex pt-0.5">
|
<div id="timestamp-row" class="flex pt-0.5">
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
||||||
@@ -95,6 +94,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||||
|
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction">
|
||||||
|
<template v-slot:chapter_track="{ item }">
|
||||||
|
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickMenuAction(item.value)">
|
||||||
|
<div class="flex items-center px-3">
|
||||||
|
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">{{ item.icon }}</span>
|
||||||
|
<span class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template v-slot:lock="{ item }">
|
||||||
|
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickMenuAction(item.value)">
|
||||||
|
<div class="flex items-center px-3">
|
||||||
|
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-opacity-80" :class="{ 'text-red-500': lockUi, 'text-white': !lockUi }">{{ item.icon }}</span>
|
||||||
|
<span class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</modals-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -109,7 +126,8 @@ export default {
|
|||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
sleepTimerRunning: Boolean,
|
sleepTimerRunning: Boolean,
|
||||||
sleepTimeRemaining: Number
|
sleepTimeRemaining: Number,
|
||||||
|
isServerItem: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -139,10 +157,12 @@ export default {
|
|||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
useChapterTrack: false,
|
useChapterTrack: false,
|
||||||
|
lockUi: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
touchTrackStart: false,
|
touchTrackStart: false,
|
||||||
dragPercent: 0,
|
dragPercent: 0,
|
||||||
syncStatus: 0
|
syncStatus: 0,
|
||||||
|
showMoreMenuDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -153,17 +173,24 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
menuItems() {
|
menuItems() {
|
||||||
var items = []
|
var items = [
|
||||||
items.push({
|
{
|
||||||
text: 'Chapter Track',
|
text: 'Chapter Track',
|
||||||
value: 'chapter_track',
|
value: 'chapter_track',
|
||||||
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
||||||
})
|
},
|
||||||
items.push({
|
{
|
||||||
text: 'Close Player',
|
text: this.lockUi ? 'Unlock Player' : 'Lock Player',
|
||||||
value: 'close',
|
value: 'lock',
|
||||||
icon: 'close'
|
icon: this.lockUi ? 'lock' : 'lock_open'
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
text: 'Close Player',
|
||||||
|
value: 'close',
|
||||||
|
icon: 'close'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
jumpForwardIcon() {
|
jumpForwardIcon() {
|
||||||
@@ -179,11 +206,11 @@ export default {
|
|||||||
return this.$store.getters['getJumpBackwardsTime']
|
return this.$store.getters['getJumpBackwardsTime']
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
if (this.showFullscreen) return this.fullscreenBookCoverWidth
|
if (this.showFullscreen) return this.fullscreenBookCoverWidth
|
||||||
return 60
|
return 46 / this.bookCoverAspectRatio
|
||||||
},
|
},
|
||||||
fullscreenBookCoverWidth() {
|
fullscreenBookCoverWidth() {
|
||||||
var heightScale = (this.windowHeight - 200) / 651
|
var heightScale = (this.windowHeight - 200) / 651
|
||||||
@@ -298,6 +325,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return secondsRemaining + 's'
|
return secondsRemaining + 's'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -309,15 +339,18 @@ export default {
|
|||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
touchstartTrack(e) {
|
async touchstartTrack(e) {
|
||||||
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen) return
|
await this.$hapticsImpactMedium()
|
||||||
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
||||||
this.touchTrackStart = true
|
this.touchTrackStart = true
|
||||||
},
|
},
|
||||||
selectChapter(chapter) {
|
async selectChapter(chapter) {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
this.showChapterModal = false
|
this.showChapterModal = false
|
||||||
},
|
},
|
||||||
castClick() {
|
async castClick() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.isLocalPlayMethod) {
|
if (this.isLocalPlayMethod) {
|
||||||
this.$eventBus.$emit('cast-local-item')
|
this.$eventBus.$emit('cast-local-item')
|
||||||
return
|
return
|
||||||
@@ -336,12 +369,14 @@ export default {
|
|||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
},
|
},
|
||||||
jumpNextChapter() {
|
async jumpNextChapter() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
if (!this.nextChapter) return
|
if (!this.nextChapter) return
|
||||||
this.seek(this.nextChapter.start)
|
this.seek(this.nextChapter.start)
|
||||||
},
|
},
|
||||||
jumpChapterStart() {
|
async jumpChapterStart() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
if (!this.currentChapter) {
|
if (!this.currentChapter) {
|
||||||
return this.restart()
|
return this.restart()
|
||||||
@@ -349,9 +384,9 @@ export default {
|
|||||||
|
|
||||||
// If 4 seconds or less into current chapter, then go to previous
|
// If 4 seconds or less into current chapter, then go to previous
|
||||||
if (this.currentTime - this.currentChapter.start <= 4) {
|
if (this.currentTime - this.currentChapter.start <= 4) {
|
||||||
var currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
|
const currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
|
||||||
if (currChapterIndex > 0) {
|
if (currChapterIndex > 0) {
|
||||||
var prevChapter = this.chapters[currChapterIndex - 1]
|
const prevChapter = this.chapters[currChapterIndex - 1]
|
||||||
this.seek(prevChapter.start)
|
this.seek(prevChapter.start)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -361,7 +396,8 @@ export default {
|
|||||||
showSleepTimerModal() {
|
showSleepTimerModal() {
|
||||||
this.$emit('showSleepTimer')
|
this.$emit('showSleepTimer')
|
||||||
},
|
},
|
||||||
setPlaybackSpeed(speed) {
|
async setPlaybackSpeed(speed) {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
||||||
this.currentPlaybackRate = speed
|
this.currentPlaybackRate = speed
|
||||||
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
||||||
@@ -369,11 +405,13 @@ export default {
|
|||||||
restart() {
|
restart() {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
},
|
},
|
||||||
jumpBackwards() {
|
async jumpBackwards() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
|
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
|
||||||
},
|
},
|
||||||
jumpForward() {
|
async jumpForward() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
|
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
|
||||||
},
|
},
|
||||||
@@ -382,19 +420,19 @@ export default {
|
|||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
},
|
},
|
||||||
setChunksReady(chunks, numSegments) {
|
setChunksReady(chunks, numSegments) {
|
||||||
var largestSeg = 0
|
let largestSeg = 0
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
var chunk = chunks[i]
|
const chunk = chunks[i]
|
||||||
if (typeof chunk === 'string') {
|
if (typeof chunk === 'string') {
|
||||||
var chunkRange = chunk.split('-').map((c) => Number(c))
|
const chunkRange = chunk.split('-').map((c) => Number(c))
|
||||||
if (chunkRange.length < 2) continue
|
if (chunkRange.length < 2) continue
|
||||||
if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1]
|
if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1]
|
||||||
} else if (chunk > largestSeg) {
|
} else if (chunk > largestSeg) {
|
||||||
largestSeg = chunk
|
largestSeg = chunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var percentageReady = largestSeg / numSegments
|
const percentageReady = largestSeg / numSegments
|
||||||
var widthReady = Math.round(this.trackWidth * percentageReady)
|
const widthReady = Math.round(this.trackWidth * percentageReady)
|
||||||
if (this.readyTrackWidth === widthReady) {
|
if (this.readyTrackWidth === widthReady) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -446,20 +484,24 @@ export default {
|
|||||||
},
|
},
|
||||||
updateTrack() {
|
updateTrack() {
|
||||||
// Update progress track UI
|
// Update progress track UI
|
||||||
var percentDone = this.currentTime / this.totalDuration
|
let percentDone = this.currentTime / this.totalDuration
|
||||||
var totalPercentDone = percentDone
|
const totalPercentDone = percentDone
|
||||||
var bufferedPercent = this.bufferedTime / this.totalDuration
|
let bufferedPercent = this.bufferedTime / this.totalDuration
|
||||||
var totalBufferedPercent = bufferedPercent
|
const totalBufferedPercent = bufferedPercent
|
||||||
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = this.currentTime - this.currentChapter.start
|
const currChapTime = this.currentTime - this.currentChapter.start
|
||||||
percentDone = currChapTime / this.currentChapterDuration
|
percentDone = currChapTime / this.currentChapterDuration
|
||||||
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
||||||
}
|
}
|
||||||
var ptWidth = Math.round(percentDone * this.trackWidth)
|
const ptWidth = Math.round(percentDone * this.trackWidth)
|
||||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||||
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
||||||
|
|
||||||
|
if (this.$refs.trackCursor) {
|
||||||
|
this.$refs.trackCursor.style.left = ptWidth - 8 + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.useChapterTrack) {
|
if (this.useChapterTrack) {
|
||||||
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
||||||
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
||||||
@@ -487,7 +529,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickTrack(e) {
|
clickTrack(e) {
|
||||||
if (this.isLoading) return
|
if (this.isLoading || this.lockUi) return
|
||||||
if (!this.showFullscreen) {
|
if (!this.showFullscreen) {
|
||||||
// Track not clickable on mini-player
|
// Track not clickable on mini-player
|
||||||
return
|
return
|
||||||
@@ -509,6 +551,7 @@ export default {
|
|||||||
this.seek(time)
|
this.seek(time)
|
||||||
},
|
},
|
||||||
async playPauseClick() {
|
async playPauseClick() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
|
|
||||||
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
|
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
|
||||||
@@ -611,19 +654,24 @@ export default {
|
|||||||
ts.innerText = currTimeStr
|
ts.innerText = currTimeStr
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickMenuAction(action) {
|
async clickMenuAction(action) {
|
||||||
if (action === 'chapter_track') {
|
await this.$hapticsImpactMedium()
|
||||||
this.useChapterTrack = !this.useChapterTrack
|
this.showMoreMenuDialog = false
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (action === 'lock') {
|
||||||
|
this.lockUi = !this.lockUi
|
||||||
|
this.$localStore.setPlayerLock(this.lockUi)
|
||||||
|
} else if (action === 'chapter_track') {
|
||||||
|
this.useChapterTrack = !this.useChapterTrack
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
})
|
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
} else if (action === 'close') {
|
||||||
} else if (action === 'close') {
|
this.closePlayback()
|
||||||
this.closePlayback()
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
forceCloseDropdownMenu() {
|
forceCloseDropdownMenu() {
|
||||||
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
||||||
@@ -705,6 +753,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
||||||
|
this.lockUi = await this.$localStore.getPlayerLock()
|
||||||
|
|
||||||
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
||||||
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
||||||
@@ -715,16 +764,22 @@ export default {
|
|||||||
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
||||||
},
|
},
|
||||||
screenOrientationChange() {
|
screenOrientationChange() {
|
||||||
setTimeout(this.updateScreenSize, 50)
|
setTimeout(() => {
|
||||||
|
this.updateScreenSize()
|
||||||
|
if (this.$refs.track) {
|
||||||
|
this.trackWidth = this.$refs.track.clientWidth
|
||||||
|
this.updateTrack()
|
||||||
|
this.updateReadyTrack()
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
},
|
},
|
||||||
updateScreenSize() {
|
updateScreenSize() {
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
var coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
||||||
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
||||||
},
|
},
|
||||||
minimizePlayerEvt() {
|
minimizePlayerEvt() {
|
||||||
console.log('Minimize Player Evt')
|
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
},
|
},
|
||||||
showProgressSyncIsFailing() {
|
showProgressSyncIsFailing() {
|
||||||
@@ -737,8 +792,12 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.updateScreenSize()
|
this.updateScreenSize()
|
||||||
if (screen.orientation) {
|
if (screen.orientation) {
|
||||||
|
// Not available on ios
|
||||||
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
||||||
|
} else {
|
||||||
|
document.addEventListener('orientationchange', this.screenOrientationChange)
|
||||||
}
|
}
|
||||||
|
window.addEventListener('resize', this.screenOrientationChange)
|
||||||
|
|
||||||
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
|
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
|
||||||
document.body.addEventListener('touchstart', this.touchstart)
|
document.body.addEventListener('touchstart', this.touchstart)
|
||||||
@@ -748,8 +807,12 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (screen.orientation) {
|
if (screen.orientation) {
|
||||||
|
// Not available on ios
|
||||||
screen.orientation.removeEventListener('change', this.screenOrientationChange)
|
screen.orientation.removeEventListener('change', this.screenOrientationChange)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('orientationchange', this.screenOrientationChange)
|
||||||
}
|
}
|
||||||
|
window.removeEventListener('resize', this.screenOrientationChange)
|
||||||
|
|
||||||
if (this.playbackSession) {
|
if (this.playbackSession) {
|
||||||
console.log('[AudioPlayer] Before destroy closing playback')
|
console.log('[AudioPlayer] Before destroy closing playback')
|
||||||
@@ -778,8 +841,8 @@ export default {
|
|||||||
:root {
|
:root {
|
||||||
--cover-image-width: 0px;
|
--cover-image-width: 0px;
|
||||||
--cover-image-height: 0px;
|
--cover-image-height: 0px;
|
||||||
--cover-image-width-collapsed: 60px;
|
--cover-image-width-collapsed: 46px;
|
||||||
--cover-image-height-collapsed: 60px;
|
--cover-image-height-collapsed: 46px;
|
||||||
}
|
}
|
||||||
.bookCoverWrapper {
|
.bookCoverWrapper {
|
||||||
box-shadow: 3px -2px 5px #00000066;
|
box-shadow: 3px -2px 5px #00000066;
|
||||||
@@ -797,10 +860,10 @@ export default {
|
|||||||
#playerTrack {
|
#playerTrack {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: margin;
|
transition-property: margin;
|
||||||
margin-bottom: 4px;
|
bottom: 10px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerTrack {
|
.fullscreen #playerTrack {
|
||||||
margin-bottom: 18px;
|
bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-player #timestamp-row {
|
.ios-player #timestamp-row {
|
||||||
@@ -809,7 +872,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cover-wrapper {
|
.cover-wrapper {
|
||||||
bottom: 44px;
|
bottom: 48px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
height: var(--cover-image-height-collapsed);
|
height: var(--cover-image-height-collapsed);
|
||||||
width: var(--cover-image-width-collapsed);
|
width: var(--cover-image-width-collapsed);
|
||||||
@@ -830,8 +893,8 @@ export default {
|
|||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
bottom: 50px;
|
bottom: 56px;
|
||||||
left: 80px;
|
left: 70px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.title-author-texts .title-text {
|
.title-author-texts .title-text {
|
||||||
@@ -869,7 +932,7 @@ export default {
|
|||||||
width: 140px;
|
width: 140px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
bottom: 45px;
|
bottom: 50px;
|
||||||
}
|
}
|
||||||
#playerControls .jump-icon {
|
#playerControls .jump-icon {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
@@ -906,7 +969,7 @@ export default {
|
|||||||
|
|
||||||
.fullscreen #playerControls {
|
.fullscreen #playerControls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 100px;
|
bottom: 105px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .jump-icon {
|
.fullscreen #playerControls .jump-icon {
|
||||||
margin: 0px 18px;
|
margin: 0px 18px;
|
||||||
@@ -927,4 +990,4 @@ export default {
|
|||||||
.fullscreen #playerControls .play-btn .material-icons {
|
.fullscreen #playerControls .play-btn .material-icons {
|
||||||
font-size: 2.1rem;
|
font-size: 2.1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<app-audio-player ref="audioPlayer" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
|
<app-audio-player ref="audioPlayer" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" :is-server-item="!!serverLibraryItemId" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
|
||||||
|
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
||||||
@@ -15,6 +15,8 @@ import { Dialog } from '@capacitor/dialog'
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
isReady: false,
|
||||||
|
settingsLoaded: false,
|
||||||
audioPlayerReady: false,
|
audioPlayerReady: false,
|
||||||
stream: null,
|
stream: null,
|
||||||
download: null,
|
download: null,
|
||||||
@@ -32,24 +34,14 @@ export default {
|
|||||||
onMediaPlayerChangedListener: null,
|
onMediaPlayerChangedListener: null,
|
||||||
sleepInterval: null,
|
sleepInterval: null,
|
||||||
currentEndOfChapterTime: 0,
|
currentEndOfChapterTime: 0,
|
||||||
serverLibraryItemId: null
|
serverLibraryItemId: null,
|
||||||
}
|
serverEpisodeId: null
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
socketConnected(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
console.log('Socket Connected set listeners')
|
|
||||||
this.setListeners()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.serverLibraryItemId) return []
|
if (!this.serverLibraryItemId) return []
|
||||||
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
||||||
},
|
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -59,7 +51,7 @@ export default {
|
|||||||
selectBookmark(bookmark) {
|
selectBookmark(bookmark) {
|
||||||
this.showBookmarksModal = false
|
this.showBookmarksModal = false
|
||||||
if (!bookmark || isNaN(bookmark.time)) return
|
if (!bookmark || isNaN(bookmark.time)) return
|
||||||
var bookmarkTime = Number(bookmark.time)
|
const bookmarkTime = Number(bookmark.time)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.seek(bookmarkTime)
|
this.$refs.audioPlayer.seek(bookmarkTime)
|
||||||
}
|
}
|
||||||
@@ -68,8 +60,6 @@ export default {
|
|||||||
this.isSleepTimerRunning = false
|
this.isSleepTimerRunning = false
|
||||||
if (currentPosition) {
|
if (currentPosition) {
|
||||||
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
||||||
var currentTime = Math.floor(currentPosition / 1000)
|
|
||||||
// TODO: Was syncing to the server here before
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
||||||
@@ -150,17 +140,12 @@ export default {
|
|||||||
console.log(`[AudioPlayerContainer] PlaybackRate Updated: ${this.playbackSpeed}`)
|
console.log(`[AudioPlayerContainer] PlaybackRate Updated: ${this.playbackSpeed}`)
|
||||||
this.$refs.audioPlayer.setPlaybackSpeed(this.playbackSpeed)
|
this.$refs.audioPlayer.setPlaybackSpeed(this.playbackSpeed)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
setListeners() {
|
// Settings have been loaded (at least once, so it's safe to kickoff onReady)
|
||||||
// if (!this.$server.socket) {
|
if (!this.settingsLoaded) {
|
||||||
// console.error('Invalid server socket not set')
|
this.settingsLoaded = true
|
||||||
// return
|
this.notifyOnReady()
|
||||||
// }
|
}
|
||||||
// this.$server.socket.on('stream_open', this.streamOpen)
|
|
||||||
// this.$server.socket.on('stream_closed', this.streamClosed)
|
|
||||||
// this.$server.socket.on('stream_progress', this.streamProgress)
|
|
||||||
// this.$server.socket.on('stream_ready', this.streamReady)
|
|
||||||
// this.$server.socket.on('stream_reset', this.streamReset)
|
|
||||||
},
|
},
|
||||||
closeStreamOnly() {
|
closeStreamOnly() {
|
||||||
// If user logs out or disconnects from server and not playing local
|
// If user logs out or disconnects from server and not playing local
|
||||||
@@ -173,15 +158,15 @@ export default {
|
|||||||
this.$toast.error(`Cannot cast locally downloaded media`)
|
this.$toast.error(`Cannot cast locally downloaded media`)
|
||||||
} else {
|
} else {
|
||||||
// Change to server library item
|
// Change to server library item
|
||||||
this.playServerLibraryItemAndCast(this.serverLibraryItemId)
|
this.playServerLibraryItemAndCast(this.serverLibraryItemId, this.serverEpisodeId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
playServerLibraryItemAndCast(libraryItemId) {
|
playServerLibraryItemAndCast(libraryItemId, episodeId) {
|
||||||
var playbackRate = 1
|
var playbackRate = 1
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
||||||
}
|
}
|
||||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId: null, playWhenReady: false, playbackRate })
|
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: false, playbackRate })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
const errorMsg = data.error || 'Failed to play'
|
const errorMsg = data.error || 'Failed to play'
|
||||||
@@ -197,12 +182,13 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
var libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
var episodeId = payload.episodeId
|
const episodeId = payload.episodeId
|
||||||
|
|
||||||
// When playing local library item and can also play this item from the server
|
// When playing local library item and can also play this item from the server
|
||||||
// then store the server library item id so it can be used if a cast is made
|
// then store the server library item id so it can be used if a cast is made
|
||||||
var serverLibraryItemId = payload.serverLibraryItemId || null
|
const serverLibraryItemId = payload.serverLibraryItemId || null
|
||||||
|
const serverEpisodeId = payload.serverEpisodeId || null
|
||||||
|
|
||||||
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
@@ -215,8 +201,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.serverLibraryItemId = null
|
this.serverLibraryItemId = null
|
||||||
|
this.serverEpisodeId = null
|
||||||
|
|
||||||
var playbackRate = 1
|
let playbackRate = 1
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
||||||
}
|
}
|
||||||
@@ -234,6 +221,11 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.serverLibraryItemId = serverLibraryItemId
|
this.serverLibraryItemId = serverLibraryItemId
|
||||||
}
|
}
|
||||||
|
if (episodeId && !episodeId.startsWith('local')) {
|
||||||
|
this.serverEpisodeId = episodeId
|
||||||
|
} else {
|
||||||
|
this.serverEpisodeId = serverEpisodeId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -253,6 +245,18 @@ export default {
|
|||||||
onMediaPlayerChanged(data) {
|
onMediaPlayerChanged(data) {
|
||||||
var mediaPlayer = data.value
|
var mediaPlayer = data.value
|
||||||
this.$store.commit('setMediaPlayer', mediaPlayer)
|
this.$store.commit('setMediaPlayer', mediaPlayer)
|
||||||
|
},
|
||||||
|
onReady() {
|
||||||
|
// The UI is reporting elsewhere we are ready
|
||||||
|
this.isReady = true
|
||||||
|
this.notifyOnReady()
|
||||||
|
},
|
||||||
|
notifyOnReady() {
|
||||||
|
// If settings aren't loaded yet, native player will receive incorrect settings
|
||||||
|
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
||||||
|
if (this.settingsLoaded && this.isReady) {
|
||||||
|
AbsAudioPlayer.onReady()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -264,7 +268,7 @@ export default {
|
|||||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||||
|
|
||||||
this.setListeners()
|
this.$eventBus.$on('abs-ui-ready', this.onReady)
|
||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||||
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
||||||
@@ -277,13 +281,7 @@ export default {
|
|||||||
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
||||||
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
||||||
|
|
||||||
// if (this.$server.socket) {
|
this.$eventBus.$off('abs-ui-ready', this.onReady)
|
||||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
|
||||||
// this.$server.socket.off('stream_closed', this.streamClosed)
|
|
||||||
// this.$server.socket.off('stream_progress', this.streamProgress)
|
|
||||||
// this.$server.socket.off('stream_ready', this.streamReady)
|
|
||||||
// this.$server.socket.off('stream_reset', this.streamReset)
|
|
||||||
// }
|
|
||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||||
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ export default {
|
|||||||
text: 'Account',
|
text: 'Account',
|
||||||
to: '/account'
|
to: '/account'
|
||||||
})
|
})
|
||||||
|
items.push({
|
||||||
|
icon: 'equalizer',
|
||||||
|
text: 'User Stats',
|
||||||
|
to: '/stats'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$platform !== 'ios') {
|
if (this.$platform !== 'ios') {
|
||||||
@@ -108,6 +113,13 @@ export default {
|
|||||||
text: 'Local Media',
|
text: 'Local Media',
|
||||||
to: '/localMedia/folders'
|
to: '/localMedia/folders'
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
icon: 'download',
|
||||||
|
iconOutlined: false,
|
||||||
|
text: 'Downloads',
|
||||||
|
to: '/downloads'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
@@ -125,6 +137,7 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
async logout() {
|
async logout() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
await this.$axios.$post('/logout').catch((error) => {
|
await this.$axios.$post('/logout').catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -162,4 +175,4 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -89,22 +89,22 @@ export default {
|
|||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = 100
|
var coverSize = 100
|
||||||
if (window.innerWidth <= 375) coverSize = 90
|
if (window.innerWidth <= 375) coverSize = 90
|
||||||
|
|
||||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
|
||||||
return this.bookWidth * 1.6
|
return this.bookWidth * 1.6
|
||||||
},
|
},
|
||||||
entityWidth() {
|
entityWidth() {
|
||||||
if (this.showBookshelfListView) return this.bookshelfWidth - 16
|
if (this.showBookshelfListView) return this.bookshelfWidth - 16
|
||||||
if (this.isBookEntity) return this.bookWidth
|
if (this.isBookEntity || this.entityName === 'playlists') return this.bookWidth
|
||||||
return this.bookWidth * 2
|
return this.bookWidth * 2
|
||||||
},
|
},
|
||||||
entityHeight() {
|
entityHeight() {
|
||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
return this.$store.getters['getAltViewEnabled']
|
return this.$store.getters['getAltViewEnabled']
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
return this.entityWidth / baseSize
|
return this.entityWidth / baseSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +145,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async fetchEntities(page) {
|
async fetchEntities(page) {
|
||||||
var startIndex = page * this.booksPerFetch
|
const startIndex = page * this.booksPerFetch
|
||||||
|
|
||||||
this.isFetchingEntities = true
|
this.isFetchingEntities = true
|
||||||
|
|
||||||
@@ -153,11 +153,11 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
||||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||||
|
|
||||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
console.error('failed to fetch books', error)
|
console.error('failed to fetch books', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -179,13 +179,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < payload.results.length; i++) {
|
for (let i = 0; i < payload.results.length; i++) {
|
||||||
var index = i + startIndex
|
const index = i + startIndex
|
||||||
this.entities[index] = payload.results[i]
|
this.entities[index] = payload.results[i]
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||||
|
|
||||||
if (this.isBookEntity) {
|
if (this.isBookEntity) {
|
||||||
var localLibraryItem = this.localLibraryItems.find((lli) => lli.libraryItemId == this.entities[index].id)
|
const localLibraryItem = this.localLibraryItems.find((lli) => lli.libraryItemId == this.entities[index].id)
|
||||||
if (localLibraryItem) {
|
if (localLibraryItem) {
|
||||||
this.entityComponentRefs[index].setLocalLibraryItem(localLibraryItem)
|
this.entityComponentRefs[index].setLocalLibraryItem(localLibraryItem)
|
||||||
}
|
}
|
||||||
@@ -356,8 +356,6 @@ export default {
|
|||||||
let searchParams = new URLSearchParams()
|
let searchParams = new URLSearchParams()
|
||||||
if (this.page === 'series-books') {
|
if (this.page === 'series-books') {
|
||||||
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
||||||
searchParams.set('sort', 'book.volumeNumber')
|
|
||||||
searchParams.set('desc', 0)
|
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default {
|
|||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
altViewEnabled() {
|
altViewEnabled() {
|
||||||
return this.$store.getters['getAltViewEnabled']
|
return this.$store.getters['getAltViewEnabled']
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<!-- No progress shown for collapsed series in library -->
|
<!-- No progress shown for collapsed series in library -->
|
||||||
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="showHasLocalDownload" class="absolute right-0 top-0 z-20" :style="{ top: (isPodcast ? 1.75 : 0.375) * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
|
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,6 +164,7 @@ export default {
|
|||||||
return this.media.numTracks
|
return this.media.numTracks
|
||||||
},
|
},
|
||||||
numEpisodes() {
|
numEpisodes() {
|
||||||
|
if (this.isLocal && this.isPodcast && this.media.episodes) return this.media.episodes.length
|
||||||
return this.media.numEpisodes
|
return this.media.numEpisodes
|
||||||
},
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
@@ -239,7 +240,7 @@ export default {
|
|||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
@@ -250,7 +251,7 @@ export default {
|
|||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.recentEpisode) return this.episodeProgress || null
|
||||||
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
|
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
@@ -318,9 +319,6 @@ export default {
|
|||||||
userIsRoot() {
|
userIsRoot() {
|
||||||
return this.store.getters['user/getIsRoot']
|
return this.store.getters['user/getIsRoot']
|
||||||
},
|
},
|
||||||
_socket() {
|
|
||||||
return this.$root.socket || this.$nuxt.$root.socket
|
|
||||||
},
|
|
||||||
titleFontSize() {
|
titleFontSize() {
|
||||||
return 0.75 * this.sizeMultiplier
|
return 0.75 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
@@ -351,6 +349,17 @@ export default {
|
|||||||
if (!this.isAltViewEnabled) return 0
|
if (!this.isAltViewEnabled) return 0
|
||||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||||
return 4.25 * this.sizeMultiplier
|
return 4.25 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
showHasLocalDownload() {
|
||||||
|
if (this.localLibraryItem || this.isLocal) {
|
||||||
|
if (this.recentEpisode && !this.isLocal) {
|
||||||
|
const localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === this.recentEpisode.id)
|
||||||
|
return !!localEpisode
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -392,15 +401,34 @@ export default {
|
|||||||
// Server books may have a local library item
|
// Server books may have a local library item
|
||||||
this.localLibraryItem = localLibraryItem
|
this.localLibraryItem = localLibraryItem
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
async clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectBtnClick()
|
this.selectBtnClick()
|
||||||
} else if (this.recentEpisode) {
|
} else if (this.recentEpisode) {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
if (this.streamIsPlaying) eventBus.$emit('pause-item')
|
if (this.streamIsPlaying) {
|
||||||
else eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
|
eventBus.$emit('pause-item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.localLibraryItem) {
|
||||||
|
const localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === this.recentEpisode.id)
|
||||||
|
if (localEpisode) {
|
||||||
|
// Play episode locally
|
||||||
|
eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.localLibraryItemId,
|
||||||
|
episodeId: localEpisode.id,
|
||||||
|
serverLibraryItemId: this.libraryItemId,
|
||||||
|
serverEpisodeId: this.recentEpisode.id
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
|
||||||
} else {
|
} else {
|
||||||
var router = this.$router || this.$nuxt.$router
|
var router = this.$router || this.$nuxt.$router
|
||||||
if (router) {
|
if (router) {
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ export default {
|
|||||||
var router = this.$router || this.$nuxt.$router
|
var router = this.$router || this.$nuxt.$router
|
||||||
router.push(`/collection/${this.collection.id}`)
|
router.push(`/collection/${this.collection.id}`)
|
||||||
},
|
},
|
||||||
clickEdit() {
|
|
||||||
this.$emit('edit', this.collection)
|
|
||||||
},
|
|
||||||
destroy() {
|
destroy() {
|
||||||
// destroy the vue listeners, etc
|
// destroy the vue listeners, etc
|
||||||
this.$destroy()
|
this.$destroy()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
||||||
{{ displayTitle }}<span v-if="seriesSequence"> #{{ seriesSequence }}</span>
|
<span v-if="seriesSequence">#{{ seriesSequence }} </span>{{ displayTitle }}
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
@@ -109,8 +109,8 @@ export default {
|
|||||||
},
|
},
|
||||||
episodes() {
|
episodes() {
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
if (this.media.numEpisodes==1) {
|
if (this.media.numEpisodes == 1) {
|
||||||
return "1 episode"
|
return '1 episode'
|
||||||
} else {
|
} else {
|
||||||
return this.media.numEpisodes + ' episodes'
|
return this.media.numEpisodes + ' episodes'
|
||||||
}
|
}
|
||||||
@@ -278,9 +278,6 @@ export default {
|
|||||||
userIsRoot() {
|
userIsRoot() {
|
||||||
return this.store.getters['user/getIsRoot']
|
return this.store.getters['user/getIsRoot']
|
||||||
},
|
},
|
||||||
_socket() {
|
|
||||||
return this.$root.socket || this.$nuxt.$root.socket
|
|
||||||
},
|
|
||||||
titleFontSize() {
|
titleFontSize() {
|
||||||
return 0.75 * this.sizeMultiplier
|
return 0.75 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @click="clickCard">
|
||||||
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||||
|
</div>
|
||||||
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||||
|
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
index: Number,
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
playlistMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
isAltViewEnabled: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
playlist: null,
|
||||||
|
isSelectionMode: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.width < 160) return 0.75
|
||||||
|
return 0.875
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||||
|
return this.width / 240
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.playlist ? this.playlist.name : ''
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist ? this.playlist.items || [] : []
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.store.state.libraries.currentLibraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEntity(playlist) {
|
||||||
|
this.playlist = playlist
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {
|
||||||
|
this.isSelectionMode = val
|
||||||
|
},
|
||||||
|
clickCard() {
|
||||||
|
if (!this.playlist) return
|
||||||
|
var router = this.$router || this.$nuxt.$router
|
||||||
|
router.push(`/playlist/${this.playlist.id}`)
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
// destroy the vue listeners, etc
|
||||||
|
this.$destroy()
|
||||||
|
|
||||||
|
// remove the element from the DOM
|
||||||
|
if (this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.playlistMount) {
|
||||||
|
this.setEntity(this.playlistMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -24,7 +24,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
name() {
|
name() {
|
||||||
return this.series.name
|
return this.series.name
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async connectToServer(config) {
|
async connectToServer(config) {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
console.log('[ServerConnectForm] connectToServer', config.address)
|
console.log('[ServerConnectForm] connectToServer', config.address)
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
@@ -159,6 +160,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async removeServerConfigClick() {
|
async removeServerConfigClick() {
|
||||||
if (!this.serverConfig.id) return
|
if (!this.serverConfig.id) return
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
|
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Confirm',
|
title: 'Confirm',
|
||||||
@@ -189,7 +191,8 @@ export default {
|
|||||||
this.showForm = true
|
this.showForm = true
|
||||||
this.showAuth = true
|
this.showAuth = true
|
||||||
},
|
},
|
||||||
newServerConfigClick() {
|
async newServerConfigClick() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
address: '',
|
address: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
@@ -348,4 +351,4 @@ export default {
|
|||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export default {
|
|||||||
default: 120
|
default: 120
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
bookCoverAspectRatio: Number,
|
||||||
downloadCover: String
|
downloadCover: String,
|
||||||
|
raw: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -115,7 +116,7 @@ export default {
|
|||||||
if (this.downloadCover) return this.downloadCover
|
if (this.downloadCover) return this.downloadCover
|
||||||
if (!this.libraryItem) return null
|
if (!this.libraryItem) return null
|
||||||
var store = this.$store || this.$nuxt.$store
|
var store = this.$store || this.$nuxt.$store
|
||||||
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
|
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl, this.raw)
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
return this.media.coverPath || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||||
|
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / (120 * 1.6 * 2)
|
||||||
|
},
|
||||||
|
itemCoverWidth() {
|
||||||
|
if (this.libraryItemCovers.length === 1) return this.width
|
||||||
|
return this.width / 2
|
||||||
|
},
|
||||||
|
libraryItemCovers() {
|
||||||
|
if (!this.items.length) return []
|
||||||
|
if (this.items.length === 1) return [this.items[0].libraryItem]
|
||||||
|
|
||||||
|
const covers = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
let index = i % this.items.length
|
||||||
|
if (this.items.length === 2 && i >= 2) index = (i + 1) % 2 // for playlists with 2 items show covers in checker pattern
|
||||||
|
|
||||||
|
covers.push(this.items[index].libraryItem)
|
||||||
|
}
|
||||||
|
return covers
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-2">
|
||||||
|
<img v-if="podcast.imageUrl" :src="podcast.imageUrl" class="h-36 w-36 object-contain mx-auto mb-2" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.title" :label="'Title'" class="mb-2 text-sm" @input="titleUpdated" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.author" :label="'Author'" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.feedUrl" :label="'Feed URL'" readonly class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="'Genres'" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-textarea-with-label v-model="podcast.description" :label="'Description'" :rows="3" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="'Folder'" class="mb-2 text-sm" @input="folderUpdated" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="fullPath" :label="'Podcast Path'" input-class="h-10" readonly class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<div class="flex items-center py-4 px-2">
|
||||||
|
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="'Auto Download Episodes'" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" @click="submit">Submit</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
podcastData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
podcastFeedData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedFolderId: null,
|
||||||
|
fullPath: null,
|
||||||
|
podcast: {
|
||||||
|
title: '',
|
||||||
|
author: '',
|
||||||
|
description: '',
|
||||||
|
releaseDate: '',
|
||||||
|
genres: [],
|
||||||
|
feedUrl: '',
|
||||||
|
feedImageUrl: '',
|
||||||
|
itunesPageUrl: '',
|
||||||
|
itunesId: '',
|
||||||
|
itunesArtistId: '',
|
||||||
|
autoDownloadEpisodes: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_processing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this._podcastData.title
|
||||||
|
},
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
folders() {
|
||||||
|
if (!this.currentLibrary) return []
|
||||||
|
return this.currentLibrary.folders || []
|
||||||
|
},
|
||||||
|
folderItems() {
|
||||||
|
return this.folders.map((fold) => {
|
||||||
|
return {
|
||||||
|
value: fold.id,
|
||||||
|
text: fold.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_podcastData() {
|
||||||
|
return this.podcastData || {}
|
||||||
|
},
|
||||||
|
feedMetadata() {
|
||||||
|
if (!this.podcastFeedData) return {}
|
||||||
|
return this.podcastFeedData.metadata || {}
|
||||||
|
},
|
||||||
|
episodes() {
|
||||||
|
if (!this.podcastFeedData) return []
|
||||||
|
return this.podcastFeedData.episodes || []
|
||||||
|
},
|
||||||
|
selectedFolder() {
|
||||||
|
return this.folders.find((f) => f.id === this.selectedFolderId)
|
||||||
|
},
|
||||||
|
selectedFolderPath() {
|
||||||
|
if (!this.selectedFolder) return ''
|
||||||
|
return this.selectedFolder.fullPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
titleUpdated() {
|
||||||
|
this.folderUpdated()
|
||||||
|
},
|
||||||
|
folderUpdated() {
|
||||||
|
if (!this.selectedFolderPath || !this.podcast.title) {
|
||||||
|
this.fullPath = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.fullPath = Path.join(this.selectedFolderPath, this.$sanitizeFilename(this.podcast.title))
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
const podcastPayload = {
|
||||||
|
path: this.fullPath,
|
||||||
|
folderId: this.selectedFolderId,
|
||||||
|
libraryId: this.currentLibrary.id,
|
||||||
|
media: {
|
||||||
|
metadata: {
|
||||||
|
title: this.podcast.title,
|
||||||
|
author: this.podcast.author,
|
||||||
|
description: this.podcast.description,
|
||||||
|
releaseDate: this.podcast.releaseDate,
|
||||||
|
genres: [...this.podcast.genres],
|
||||||
|
feedUrl: this.podcast.feedUrl,
|
||||||
|
imageUrl: this.podcast.imageUrl,
|
||||||
|
itunesPageUrl: this.podcast.itunesPageUrl,
|
||||||
|
itunesId: this.podcast.itunesId,
|
||||||
|
itunesArtistId: this.podcast.itunesArtistId,
|
||||||
|
language: this.podcast.language
|
||||||
|
},
|
||||||
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Podcast payload', podcastPayload)
|
||||||
|
|
||||||
|
this._processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/podcasts', podcastPayload)
|
||||||
|
.then((libraryItem) => {
|
||||||
|
this._processing = false
|
||||||
|
this.$toast.success('Podcast added')
|
||||||
|
this.$router.push(`/item/${libraryItem.id}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to add podcast'
|
||||||
|
console.error('Failed to create podcast', error)
|
||||||
|
this._processing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
// Prefer using itunes podcast data but not always passed in if manually entering rss feed
|
||||||
|
this.podcast.title = this._podcastData.title || this.feedMetadata.title || ''
|
||||||
|
this.podcast.author = this._podcastData.artistName || this.feedMetadata.author || ''
|
||||||
|
this.podcast.description = this._podcastData.description || this.feedMetadata.descriptionPlain || ''
|
||||||
|
this.podcast.releaseDate = this._podcastData.releaseDate || ''
|
||||||
|
this.podcast.genres = this._podcastData.genres || this.feedMetadata.categories || []
|
||||||
|
this.podcast.feedUrl = this._podcastData.feedUrl || this.feedMetadata.feedUrl || ''
|
||||||
|
this.podcast.imageUrl = this._podcastData.cover || this.feedMetadata.image || ''
|
||||||
|
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
||||||
|
this.podcast.itunesId = this._podcastData.id || ''
|
||||||
|
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
||||||
|
this.podcast.language = this._podcastData.language || ''
|
||||||
|
this.podcast.autoDownloadEpisodes = false
|
||||||
|
|
||||||
|
if (this.folderItems[0]) {
|
||||||
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
|
this.folderUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-9 bg-bg relative">
|
<div class="w-full h-9 bg-bg relative">
|
||||||
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
|
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
|
||||||
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex items-center justify-center" :style="{ width: isPodcast ? '50%' : '25%' }" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
|
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex-grow flex items-center justify-center" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
|
||||||
<p>{{ item.text }}</p>
|
<p v-if="routeName === item.routeName" class="text-sm font-semibold">{{ item.text }}</p>
|
||||||
|
<span v-else-if="item.iconPack === 'abs-icons'" class="abs-icons" :class="`icon-${item.icon} ${item.iconClass || ''}`"></span>
|
||||||
|
<span v-else :class="`${item.iconPack} ${item.iconClass || ''}`">{{ item.icon }}</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,43 +16,114 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
currentLibraryIcon() {
|
||||||
|
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
||||||
|
},
|
||||||
|
userHasPlaylists() {
|
||||||
|
return this.$store.state.libraries.numUserPlaylists
|
||||||
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
items() {
|
items() {
|
||||||
|
let items = []
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
return [
|
items = [
|
||||||
{
|
{
|
||||||
to: '/bookshelf',
|
to: '/bookshelf',
|
||||||
routeName: 'bookshelf',
|
routeName: 'bookshelf',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'home',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Home'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/latest',
|
||||||
|
routeName: 'bookshelf-latest',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'list',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Latest'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/library',
|
||||||
|
routeName: 'bookshelf-library',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: this.currentLibraryIcon,
|
||||||
|
iconClass: 'text-lg',
|
||||||
|
text: 'Library'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.userIsAdminOrUp) {
|
||||||
|
items.push({
|
||||||
|
to: '/bookshelf/search',
|
||||||
|
routeName: 'bookshelf-search',
|
||||||
|
iconPack: 'material-icons',
|
||||||
|
icon: 'podcasts',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Search'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
to: '/bookshelf',
|
||||||
|
routeName: 'bookshelf',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'home',
|
||||||
|
iconClass: 'text-xl',
|
||||||
text: 'Home'
|
text: 'Home'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/bookshelf/library',
|
to: '/bookshelf/library',
|
||||||
routeName: 'bookshelf-library',
|
routeName: 'bookshelf-library',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: this.currentLibraryIcon,
|
||||||
|
iconClass: 'text-lg',
|
||||||
text: 'Library'
|
text: 'Library'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/series',
|
||||||
|
routeName: 'bookshelf-series',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'columns',
|
||||||
|
iconClass: 'text-lg pt-px',
|
||||||
|
text: 'Series'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/collections',
|
||||||
|
routeName: 'bookshelf-collections',
|
||||||
|
iconPack: 'material-icons-outlined',
|
||||||
|
icon: 'collections_bookmark',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Collections'
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// to: '/bookshelf/authors',
|
||||||
|
// routeName: 'bookshelf-authors',
|
||||||
|
// iconPack: 'abs-icons',
|
||||||
|
// icon: 'authors',
|
||||||
|
// iconClass: 'text-2xl pb-px',
|
||||||
|
// text: 'Authors'
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
return [
|
|
||||||
{
|
if (this.userHasPlaylists) {
|
||||||
to: '/bookshelf',
|
items.push({
|
||||||
routeName: 'bookshelf',
|
to: '/bookshelf/playlists',
|
||||||
text: 'Home'
|
routeName: 'bookshelf-playlists',
|
||||||
},
|
iconPack: 'material-icons',
|
||||||
{
|
icon: 'queue_music',
|
||||||
to: '/bookshelf/library',
|
text: 'Playlists'
|
||||||
routeName: 'bookshelf-library',
|
})
|
||||||
text: 'Library'
|
}
|
||||||
},
|
|
||||||
{
|
return items
|
||||||
to: '/bookshelf/series',
|
|
||||||
routeName: 'bookshelf-series',
|
|
||||||
text: 'Series'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: '/bookshelf/collections',
|
|
||||||
routeName: 'bookshelf-collections',
|
|
||||||
text: 'Collections'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
@@ -62,7 +135,9 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
isSelected(item) {}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
||||||
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
||||||
<template v-if="page === 'library'">
|
<template v-if="page === 'library'">
|
||||||
<div class="relative flex items-center px-2">
|
<div class="relative flex items-center px-2">
|
||||||
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
||||||
@@ -64,6 +64,8 @@ export default {
|
|||||||
return 'Series'
|
return 'Series'
|
||||||
} else if (this.page === 'collections') {
|
} else if (this.page === 'collections') {
|
||||||
return 'Collections'
|
return 'Collections'
|
||||||
|
} else if (this.page === 'playlists') {
|
||||||
|
return 'Playlists'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
@@ -100,6 +102,10 @@ export default {
|
|||||||
},
|
},
|
||||||
setTotalEntities(total) {
|
setTotalEntities(total) {
|
||||||
this.totalEntities = total
|
this.totalEntities = total
|
||||||
|
},
|
||||||
|
async changeView() {
|
||||||
|
this.bookshelfListView = !this.bookshelfListView
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -118,4 +124,4 @@ export default {
|
|||||||
#bookshelf-toolbar {
|
#bookshelf-toolbar {
|
||||||
box-shadow: 0px 5px 5px #11111155;
|
box-shadow: 0px 5px 5px #11111155;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
|
|
||||||
<path
|
|
||||||
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
|
|
||||||
transform="translate(-557.82 -281.22)"
|
|
||||||
/>
|
|
||||||
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
|
|
||||||
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
|
|
||||||
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
|
|
||||||
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg class="p-px" viewBox="0 0 122.877 120.596">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M68.925,69.906v50.689H53.953V69.906c-4.918-2.662-8.259-7.867-8.259-13.854 c0-8.694,7.05-15.744,15.745-15.744c8.694,0,15.745,7.05,15.745,15.744C77.184,62.039,73.843,67.244,68.925,69.906L68.925,69.906z M39.32,11.165c2.916-1.438,4.111-4.969,2.673-7.882c-1.438-2.914-4.966-4.111-7.88-2.674C22.213,6.479,12.958,16.19,7.11,27.625 c-4.32,8.445-6.783,17.842-7.08,27.325c-0.299,9.563,1.587,19.223,5.973,28.114c5.401,10.953,14.558,20.695,28.039,27.592 c2.889,1.477,6.429,0.33,7.905-2.559c1.477-2.889,0.331-6.428-2.558-7.904c-11.037-5.645-18.486-13.525-22.833-22.334 c-3.506-7.111-5.014-14.857-4.774-22.539c0.243-7.757,2.256-15.442,5.79-22.348C22.304,23.721,29.76,15.879,39.32,11.165 L39.32,11.165z M88.765,0.608c-2.914-1.438-6.443-0.24-7.881,2.674c-1.438,2.914-0.242,6.445,2.674,7.882 c9.561,4.715,17.017,12.556,21.747,21.808c3.533,6.905,5.547,14.59,5.789,22.348c0.24,7.682-1.268,15.428-4.773,22.539 c-4.347,8.809-11.796,16.689-22.833,22.334c-2.889,1.477-4.034,5.016-2.558,7.904c1.476,2.889,5.016,4.035,7.905,2.559 c13.48-6.896,22.638-16.639,28.039-27.592c4.386-8.891,6.272-18.551,5.973-28.114c-0.297-9.483-2.76-18.88-7.079-27.325 C109.919,16.19,100.665,6.479,88.765,0.608L88.765,0.608z M82.791,26.505c-2.195-1.581-5.256-1.082-6.837,1.113 c-1.58,2.195-1.082,5.256,1.113,6.837c0.885,0.637,1.753,1.352,2.604,2.134c4.971,4.583,7.919,10.694,8.538,17.16 c0.626,6.524-1.111,13.437-5.518,19.552c-0.748,1.039-1.61,2.092-2.585,3.15c-1.835,1.992-1.708,5.098,0.287,6.932 c1.994,1.834,5.099,1.705,6.933-0.287c1.18-1.279,2.286-2.641,3.315-4.072c5.862-8.139,8.166-17.4,7.322-26.197 c-0.848-8.853-4.871-17.208-11.648-23.457C85.249,28.387,84.074,27.431,82.791,26.505L82.791,26.505z M45.81,34.458 c2.195-1.581,2.694-4.642,1.113-6.837c-1.581-2.195-4.642-2.694-6.837-1.114c-1.284,0.926-2.458,1.882-3.524,2.864 c-6.778,6.25-10.801,14.604-11.649,23.457c-0.844,8.796,1.46,18.06,7.323,26.199c1.031,1.43,2.136,2.791,3.315,4.07 c1.834,1.992,4.939,2.121,6.932,0.287c1.996-1.834,2.123-4.939,0.288-6.932c-0.975-1.059-1.837-2.111-2.585-3.15 c-4.406-6.115-6.144-13.027-5.518-19.551c0.619-6.465,3.567-12.577,8.538-17.16C44.058,35.81,44.926,35.095,45.81,34.458 L45.81,34.458z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
@@ -94,6 +95,7 @@ export default {
|
|||||||
this.showBookmarkTitleInput = true
|
this.showBookmarkTitleInput = true
|
||||||
},
|
},
|
||||||
async deleteBookmark(bm) {
|
async deleteBookmark(bm) {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Remove Bookmark',
|
title: 'Remove Bookmark',
|
||||||
message: `Are you sure you want to remove bookmark?`
|
message: `Are you sure you want to remove bookmark?`
|
||||||
@@ -111,7 +113,8 @@ export default {
|
|||||||
})
|
})
|
||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
clickBookmark(bm) {
|
async clickBookmark(bm) {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
this.$emit('select', bm)
|
this.$emit('select', bm)
|
||||||
},
|
},
|
||||||
submitUpdateBookmark(updatedBookmark) {
|
submitUpdateBookmark(updatedBookmark) {
|
||||||
@@ -155,7 +158,8 @@ export default {
|
|||||||
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
||||||
this.showBookmarkTitleInput = true
|
this.showBookmarkTitleInput = true
|
||||||
},
|
},
|
||||||
submitBookmark() {
|
async submitBookmark() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
if (this.selectedBookmark) {
|
if (this.selectedBookmark) {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
...this.selectedBookmark,
|
...this.selectedBookmark,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="300" height="100%">
|
<modals-modal v-model="show" :width="350" height="100%">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div v-if="currentChapter" class="absolute top-7 left-4 z-40" style="max-width: 80%">
|
<div v-if="currentChapter" class="absolute top-7 left-4 z-40" style="max-width: 80%">
|
||||||
<p class="text-white text-lg truncate">Current: {{ currentChapterTitle }}</p>
|
<p class="text-white text-lg truncate">Current: {{ currentChapterTitle }}</p>
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="chapter in chapters">
|
<template v-for="chapter in chapters">
|
||||||
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="currentChapterId === chapter.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
|
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="currentChapterId === chapter.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
|
||||||
<div class="relative flex items-center pl-3" style="padding-right: 4.5rem">
|
<div class="relative flex items-center pl-3 pr-20">
|
||||||
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ chapter.title }}</p>
|
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ chapter.title }}</p>
|
||||||
<div class="absolute top-0 right-3 -mt-0.5">
|
<div class="absolute top-0 right-3 -mt-0.5">
|
||||||
<span class="font-mono text-white text-opacity-90 leading-3" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start) }}</span>
|
<span class="font-mono text-white text-opacity-90 leading-3 text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
|
<slot :name="item.value" :item="item" :selected="item.value === selected">
|
||||||
<div class="relative flex items-center px-3">
|
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
|
||||||
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
<div class="relative flex items-center px-3">
|
||||||
</div>
|
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
||||||
</li>
|
</div>
|
||||||
|
</li>
|
||||||
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<template #outer>
|
<template #outer>
|
||||||
<div v-show="selected !== 'all'" class="absolute top-4 left-4 z-40">
|
<div v-show="selected !== 'all'" class="absolute top-4 left-4 z-40">
|
||||||
<ui-btn class="text-xl border-yellow-400 border-opacity-40" @click="clearSelected">Clear</ui-btn>
|
<ui-btn class="text-xl border-yellow-400 border-opacity-40" @click="clearSelected">Clear</ui-btn>
|
||||||
<!-- <span class="font-semibold uppercase text-white text-2xl">Clear</span> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
@@ -86,6 +85,11 @@ export default {
|
|||||||
value: 'narrators',
|
value: 'narrators',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Language',
|
||||||
|
value: 'languages',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Progress',
|
text: 'Progress',
|
||||||
value: 'progress',
|
value: 'progress',
|
||||||
@@ -165,8 +169,28 @@ export default {
|
|||||||
narrators() {
|
narrators() {
|
||||||
return this.filterData.narrators || []
|
return this.filterData.narrators || []
|
||||||
},
|
},
|
||||||
|
languages() {
|
||||||
|
return this.filterData.languages || []
|
||||||
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
return [
|
||||||
|
{
|
||||||
|
id: 'finished',
|
||||||
|
name: 'Finished'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'in-progress',
|
||||||
|
name: 'In Progress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-started',
|
||||||
|
name: 'Not Started'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-finished',
|
||||||
|
name: 'Not Finished'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
return (this[this.sublist] || []).map((item) => {
|
||||||
@@ -188,7 +212,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearSelected() {
|
async clearSelected() {
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
this.selected = 'all'
|
this.selected = 'all'
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$nextTick(() => this.$emit('change', 'all'))
|
this.$nextTick(() => this.$emit('change', 'all'))
|
||||||
@@ -196,7 +221,7 @@ export default {
|
|||||||
clickedSublistOption(item) {
|
clickedSublistOption(item) {
|
||||||
this.clickedOption({ value: `${this.sublist}.${item}` })
|
this.clickedOption({ value: `${this.sublist}.${item}` })
|
||||||
},
|
},
|
||||||
clickedOption(option) {
|
async clickedOption(option) {
|
||||||
if (option.sublist) {
|
if (option.sublist) {
|
||||||
this.sublist = option.value
|
this.sublist = option.value
|
||||||
return
|
return
|
||||||
@@ -207,6 +232,7 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await this.$hapticsImpactMedium()
|
||||||
this.selected = val
|
this.selected = val
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$nextTick(() => this.$emit('change', val))
|
this.$nextTick(() => this.$emit('change', val))
|
||||||
@@ -220,4 +246,4 @@ export default {
|
|||||||
.filter-modal-wrapper {
|
.filter-modal-wrapper {
|
||||||
max-height: calc(100% - 320px);
|
max-height: calc(100% - 320px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" width="100%" height="100%" max-width="100%">
|
<modals-modal v-model="show" width="100%" height="100%" max-width="100%">
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
<covers-book-cover :library-item="libraryItem" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :width="width" raw :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||