diff --git a/android/app/src/main/assets/capacitor.config.json b/android/app/src/main/assets/capacitor.config.json index a5ef24e4..0551bb2e 100644 --- a/android/app/src/main/assets/capacitor.config.json +++ b/android/app/src/main/assets/capacitor.config.json @@ -5,6 +5,10 @@ "plugins": { "CapacitorHttp": { "enabled": false + }, + "StatusBar": { + "backgroundColor": "#232323", + "style": "DARK" } }, "server": { diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt index 87080a82..d9726654 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -6,10 +6,15 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.os.IBinder import android.util.Log +import android.view.ViewGroup +import android.view.WindowInsets +import android.webkit.WebView import androidx.core.app.ActivityCompat +import androidx.core.view.updateLayoutParams import com.anggrayudi.storage.SimpleStorage import com.anggrayudi.storage.SimpleStorageHelper import com.audiobookshelf.app.managers.DbManager @@ -40,18 +45,6 @@ class MainActivity : BridgeActivity() { ) public override fun onCreate(savedInstanceState: Bundle?) { - // TODO: Optimize using strict mode logs -// StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() -// .detectDiskReads() -// .detectDiskWrites().detectAll() -// .detectNetwork() // or .detectAll() for all detectable problems -// .penaltyLog() -// .build()) -// StrictMode.setVmPolicy(VmPolicy.Builder() -// .detectLeakedSqlLiteObjects() -// .detectLeakedClosableObjects() -// .penaltyLog() -// .build()) DbManager.initialize(applicationContext) registerPlugin(AbsAudioPlayer::class.java) @@ -63,7 +56,47 @@ class MainActivity : BridgeActivity() { super.onCreate(savedInstanceState) Log.d(tag, "onCreate") + // Update the margins to handle edge-to-edge enforced in SDK 35 + // See: https://developer.android.com/develop/ui/views/layout/edge-to-edge + val webView: WebView = findViewById(R.id.webview) + webView.setOnApplyWindowInsetsListener { v, insets -> + val (left, top, right, bottom) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val sysInsets = insets.getInsets(WindowInsets.Type.systemBars()) + Log.d(tag, "safe sysInsets: $sysInsets") + arrayOf(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom) + } else { + arrayOf( + insets.systemWindowInsetLeft, + insets.systemWindowInsetTop, + insets.systemWindowInsetRight, + insets.systemWindowInsetBottom + ) + } + // Inject as CSS variables + // NOTE: Possibly able to use in the future to support edge-to-edge better. + val js = """ + document.documentElement.style.setProperty('--safe-area-inset-top', '${top}px'); + document.documentElement.style.setProperty('--safe-area-inset-bottom', '${bottom}px'); + document.documentElement.style.setProperty('--safe-area-inset-left', '${left}px'); + document.documentElement.style.setProperty('--safe-area-inset-right', '${right}px'); + """.trimIndent() + webView.evaluateJavascript(js, null) + + // Set margins + v.updateLayoutParams { + leftMargin = left + bottomMargin = bottom + rightMargin = right + topMargin = top + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowInsets.CONSUMED + } else { + insets + } + } val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) if (permission != PackageManager.PERMISSION_GRANTED) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt index 48dd51fb..77d4c70e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt @@ -5,8 +5,7 @@ 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.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import org.json.JSONObject @@ -18,8 +17,7 @@ data class ItemInProgress( val isLocal: Boolean ) { companion object { - fun makeFromServerObject(serverItem: JSONObject):ItemInProgress { - val jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + fun makeFromServerObject(serverItem: JSONObject, jacksonMapper: ObjectMapper):ItemInProgress { val libraryItem = jacksonMapper.readValue(serverItem.toString()) var episode:PodcastEpisode? = null diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index bf807f73..4086d6b7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -334,8 +334,7 @@ class ApiHandler(var ctx:Context) { val array = it.getJSONArray("libraryItems") for (i in 0 until array.length()) { val jsobj = array.get(i) as JSONObject - - val itemInProgress = ItemInProgress.makeFromServerObject(jsobj) + val itemInProgress = ItemInProgress.makeFromServerObject(jsobj, jacksonMapper) items.add(itemInProgress) } } diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index b5ad1387..aeba3591 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -7,6 +7,7 @@ tools:context=".MainActivity"> diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml index adcb18a0..91b56089 100644 --- a/android/app/src/main/res/values-v21/styles.xml +++ b/android/app/src/main/res/values-v21/styles.xml @@ -10,7 +10,7 @@ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 41281939..6039c197 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -3,5 +3,5 @@ #FF81D4FA #FF039BE5 #FF01579B - #262626 - \ No newline at end of file + #232323 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 4234d998..4f1668d9 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -12,7 +12,7 @@ diff --git a/assets/app.css b/assets/app.css index 3429acd8..dde1e0a9 100644 --- a/assets/app.css +++ b/assets/app.css @@ -1,5 +1,5 @@ -@import "./tailwind.css"; -@import "./fonts.css"; +@import './tailwind.css'; +@import './fonts.css'; @import './defaultStyles.css'; @import './absicons.css'; @import './transitions.css'; @@ -61,6 +61,10 @@ textarea { box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166; } +.box-shadow-progressbar { + box-shadow: 0px -1px 4px rgb(62, 50, 2, 0.5); +} + .shadow-height { height: calc(100% - 4px); } @@ -164,4 +168,4 @@ Bookshelf Label .tracksTable th { padding: 4px 8px; font-size: 0.75rem; -} \ No newline at end of file +} diff --git a/assets/tailwind.css b/assets/tailwind.css index 08764ff9..2ecc8dfe 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -22,6 +22,25 @@ --gradient-minimized-audio-player: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%); } + html[data-theme='black'] { + color: white; + --color-bg: 0 0 0; + --color-bg-hover: 0 0 0; + --color-fg: 230 237 243; + --color-fg-muted: 120 126 132; + --color-primary: 0 0 0; + --color-secondary: 0 0 0; + --color-border: 55 62 65; + --color-bg-toggle: 0 0 0; + --color-bg-toggle-selected: 35 35 35; + --color-track-cursor: 229 231 235; + --color-track: 107 114 128; + --color-track-buffered: 75 85 99; + --gradient-item-page: rgb(0, 0, 0); + --gradient-audio-player: rgb(0, 0, 0); + --gradient-minimized-audio-player: rgb(0, 0, 0); + } + html[data-theme='light'] { color: black; --color-bg: 255 255 255; diff --git a/capacitor.config.json b/capacitor.config.json index 81df5320..8947e8d7 100644 --- a/capacitor.config.json +++ b/capacitor.config.json @@ -5,6 +5,10 @@ "plugins": { "CapacitorHttp": { "enabled": false + }, + "StatusBar": { + "backgroundColor": "#232323", + "style": "DARK" } }, "server": { diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index b7747f2c..7684fb90 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -4,15 +4,15 @@
- keyboard_arrow_down + keyboard_arrow_down
- {{ isCasting ? 'cast_connected' : 'cast' }} + {{ isCasting ? 'cast_connected' : 'cast' }}
- more_vert + more_vert
-

{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}

+

{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}

@@ -182,6 +182,9 @@ export default { } }, computed: { + theme() { + return document.documentElement.dataset.theme || 'dark' + }, menuItems() { const items = [] // TODO: Implement on iOS diff --git a/components/cards/LazyBookCard.vue b/components/cards/LazyBookCard.vue index d9a79eea..9aaa1739 100644 --- a/components/cards/LazyBookCard.vue +++ b/components/cards/LazyBookCard.vue @@ -59,7 +59,7 @@
-
+
diff --git a/components/cards/LazyListBookCard.vue b/components/cards/LazyListBookCard.vue index fc3a1d38..f58f507f 100644 --- a/components/cards/LazyListBookCard.vue +++ b/components/cards/LazyListBookCard.vue @@ -14,7 +14,7 @@
-
+

#{{ seriesSequence }} {{ displayTitle }}

@@ -29,6 +29,16 @@ {{ $getString('LabelNumEpisodes', [numEpisodes]) }}

+
+ +
download_done @@ -129,6 +139,9 @@ export default { libraryItemId() { return this._libraryItem.id }, + localLibraryItemId() { + return this.localLibraryItem?.id + }, series() { return this.mediaMetadata.series }, @@ -217,13 +230,31 @@ export default { return this.isMissing || this.isInvalid }, isStreaming() { - return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId + return this.isPlaying && !this.store.getters['getIsCurrentSessionLocal'] + }, + isPlaying() { + if (this.localLibraryItemId && this.store.getters['getIsMediaStreaming'](this.localLibraryItemId)) return true + return this.store.getters['getIsMediaStreaming'](this.libraryItemId) + }, + playerIsPlaying() { + return this.store.state.playerIsPlaying && (this.isStreaming || this.isPlaying) + }, + playerIsStartingPlayback() { + // Play has been pressed and waiting for native play response + return this.store.state.playerIsStartingPlayback + }, + playerIsStartingForThisMedia() { + const mediaId = this.store.state.playerStartingPlaybackMediaId + return mediaId === this.libraryItemId + }, + isCasting() { + return this.store.state.isCasting }, showReadButton() { return !this.isSelectionMode && !this.showPlayButton && this.hasEbook }, showPlayButton() { - return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming + return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isPodcast }, showSmallEBookIcon() { return !this.isSelectionMode && this.hasEbook @@ -293,9 +324,32 @@ export default { this.selected = !this.selected this.$emit('select', this.libraryItem) }, - play() { - var eventBus = this.$eventBus || this.$nuxt.$eventBus - eventBus.$emit('play-item', { libraryItemId: this.libraryItemId }) + async play() { + if (this.playerIsStartingPlayback) return + + const hapticsImpact = this.$hapticsImpact || this.$nuxt.$hapticsImpact + if (hapticsImpact) { + await hapticsImpact() + } + + const eventBus = this.$eventBus || this.$nuxt.$eventBus + + if (this.playerIsPlaying) { + eventBus.$emit('pause-item') + } else { + // Audiobook + let libraryItemId = this.libraryItemId + + // When casting use server library item + if (this.localLibraryItem && !this.isCasting) { + libraryItemId = this.localLibraryItem.id + } else if (this.hasLocal) { + libraryItemId = this.localLibraryItem.id + } + + this.store.commit('setPlayerIsStartingPlayback', this.libraryItemId) + eventBus.$emit('play-item', { libraryItemId, serverLibraryItemId: this.libraryItemId }) + } }, destroy() { // destroy the vue listeners, etc diff --git a/components/cards/LazySeriesCard.vue b/components/cards/LazySeriesCard.vue index a52a828d..cff44410 100644 --- a/components/cards/LazySeriesCard.vue +++ b/components/cards/LazySeriesCard.vue @@ -5,7 +5,7 @@
-
+

{{ title }}

diff --git a/components/modals/BookmarksModal.vue b/components/modals/BookmarksModal.vue index 5d91bcf8..edd93356 100644 --- a/components/modals/BookmarksModal.vue +++ b/components/modals/BookmarksModal.vue @@ -17,7 +17,7 @@

{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}

- +
{{ selectedBookmark ? 'Update' : 'Create' }}
@@ -93,6 +93,10 @@ export default { } }, methods: { + bookmarkPlaceholder() { + // using a method prevents caching the date + return this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm') + }, editBookmark(bm) { this.selectedBookmark = bm this.newBookmarkTitle = bm.title @@ -157,18 +161,8 @@ export default { }, createBookmark() { this.selectedBookmark = null - this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm') + this.newBookmarkTitle = '' this.showBookmarkTitleInput = true - - // Auto focus the input and select the text - this.$nextTick(() => { - if (this.$refs.noteInput?.$refs.input?.$refs.input) { - this.$refs.noteInput.$refs.input.$refs.input.focus() - setTimeout(() => { - this.$refs.noteInput?.$refs.input?.$refs.input?.select() - }, 10) - } - }) }, async submitBookmark() { await this.$hapticsImpact() diff --git a/components/modals/FilterModal.vue b/components/modals/FilterModal.vue index 7664e768..296db325 100644 --- a/components/modals/FilterModal.vue +++ b/components/modals/FilterModal.vue @@ -54,8 +54,39 @@ export default { }, data() { return { - sublist: null, - bookItems: [ + sublist: null + } + }, + watch: { + show(newVal) { + if (!newVal) { + if (this.sublist && !this.selectedItemSublist) this.sublist = null + if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist + } + } + }, + computed: { + show: { + get() { + return this.value + }, + set(val) { + this.$emit('input', val) + } + }, + selected: { + get() { + return this.filterBy + }, + set(val) { + this.$emit('update:filterBy', val) + } + }, + userCanAccessExplicitContent() { + return this.$store.getters['user/getUserCanAccessExplicitContent'] + }, + bookItems() { + const items = [ { text: this.$strings.LabelAll, value: 'all' @@ -104,9 +135,26 @@ export default { text: this.$strings.ButtonIssues, value: 'issues', sublist: false + }, + { + text: this.$strings.LabelRSSFeedOpen, + value: 'feed-open', + sublist: false } - ], - podcastItems: [ + ] + + if (this.userCanAccessExplicitContent) { + items.push({ + text: this.$strings.LabelExplicit, + value: 'explicit', + sublist: false + }) + } + + return items + }, + podcastItems() { + const items = [ { text: this.$strings.LabelAll, value: 'all' @@ -120,34 +168,23 @@ export default { text: this.$strings.LabelTag, value: 'tags', sublist: true + }, + { + text: this.$strings.LabelRSSFeedOpen, + value: 'feed-open', + sublist: false } ] - } - }, - watch: { - show(newVal) { - if (!newVal) { - if (this.sublist && !this.selectedItemSublist) this.sublist = null - if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist - } - } - }, - computed: { - show: { - get() { - return this.value - }, - set(val) { - this.$emit('input', val) - } - }, - selected: { - get() { - return this.filterBy - }, - set(val) { - this.$emit('update:filterBy', val) + + if (this.userCanAccessExplicitContent) { + items.push({ + text: this.$strings.LabelExplicit, + value: 'explicit', + sublist: false + }) } + + return items }, isPodcast() { return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast' diff --git a/components/readers/Reader.vue b/components/readers/Reader.vue index 137457a4..dda49acb 100644 --- a/components/readers/Reader.vue +++ b/components/readers/Reader.vue @@ -14,7 +14,7 @@ settings
@@ -260,6 +260,7 @@ export default { return null }, ebookFile() { + if (!this.media) return null // ebook file id is passed when reading a supplementary ebook if (this.ebookFileId) { return this.selectedLibraryItem.libraryFiles.find((lf) => lf.ino === this.ebookFileId) diff --git a/components/ui/TextInput.vue b/components/ui/TextInput.vue index 942a97e5..600f77ee 100644 --- a/components/ui/TextInput.vue +++ b/components/ui/TextInput.vue @@ -1,6 +1,6 @@