diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5e4a8f29..44833801 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,18 +23,18 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" > - - - - - - - + + + + + - - - - + + ?) { + super.onResourceReady(resource, transition) + } + } + + val artist = playbackSession?.displayAuthor ?: "Unknown" + views.setTextViewText(R.id.widgetArtistText, artist) + + val title = playbackSession?.displayTitle ?: "Unknown" + views.setTextViewText(R.id.widgetMediaTitle, title) + + val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon) + Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt) + + Log.i(tag, "Update App Widget | Is Playing=$isPlaying | isAppClosed=$isAppClosed") + + val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark + views.setImageViewResource(R.id.widgetPlayPauseButton, playPauseResource) + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views) +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt b/android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt deleted file mode 100644 index c160ca06..00000000 --- a/android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.audiobookshelf.app - -import android.app.PendingIntent -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE -import android.util.Log -import android.widget.RemoteViews -import androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent -import com.audiobookshelf.app.device.DeviceManager -import com.audiobookshelf.app.device.WidgetEventEmitter -import com.audiobookshelf.app.player.PlayerNotificationService -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.AppWidgetTarget -import com.bumptech.glide.request.transition.Transition - -/** - * Implementation of App Widget functionality. - */ -class NewAppWidget : AppWidgetProvider() { - val tag = "NewAppWidget" - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - // There may be multiple widgets active, so update all of them - for (appWidgetId in appWidgetIds) { - updateAppWidget(context, appWidgetManager, appWidgetId, null,false) - } - } - - override fun onEnabled(context: Context) { - Log.w(tag, "onEnabled check context ${context.packageName}") - - // Enter relevant functionality for when the first widget is created - DeviceManager.widgetUpdater = (object : WidgetEventEmitter { - override fun onPlayerChanged(pns:PlayerNotificationService) { - val isPlaying = pns.currentPlayer.isPlaying - Log.i(tag, "onPlayerChanged | Is Playing? $isPlaying") - - val appWidgetManager = AppWidgetManager.getInstance(context) - val componentName = ComponentName(context, NewAppWidget::class.java) - val ids = appWidgetManager.getAppWidgetIds(componentName) - - val playbackSession = pns.getCurrentPlaybackSessionCopy() - val cover = playbackSession?.getCoverUri() - - for (widgetId in ids) { - updateAppWidget(context, appWidgetManager, widgetId, cover, isPlaying) - } - } - }) - } - - override fun onDisabled(context: Context) { - // Enter relevant functionality for when the last widget is disabled - } -} - -internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, coverUri:Uri?, isPlaying:Boolean) { - - val views = RemoteViews(context.packageName, R.layout.new_app_widget) - - val playPausePI = buildMediaButtonPendingIntent(context, ACTION_PLAY_PAUSE) - views.setOnClickPendingIntent(R.id.playPauseIcon, playPausePI) - - val wholeWidgetClickI = Intent(context, MainActivity::class.java) - wholeWidgetClickI.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK - val wholeWidgetClickPI = PendingIntent.getActivity( - context, - System.currentTimeMillis().toInt(), - wholeWidgetClickI, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - views.setOnClickPendingIntent(R.id.appWidget, wholeWidgetClickPI) - - val imageUri = coverUri ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) - val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.imageView, views, appWidgetId) { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - super.onResourceReady(resource, transition) - } - } - - val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon) - Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt) - - val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark - views.setImageViewResource(R.id.playPauseIcon, playPauseResource) - - // Instruct the widget manager to update the widget - appWidgetManager.updateAppWidget(appWidgetId, views) -} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt index b0e9a5a0..cc44cc17 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.os.Bundle import android.support.v4.media.MediaDescriptionCompat import androidx.media.utils.MediaConstants +import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.fasterxml.jackson.annotation.JsonIgnore @@ -41,7 +42,7 @@ class LibraryItem( @JsonIgnore fun getCoverUri(): Uri { if (media.coverPath == null) { - return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) + return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) } return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}") diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt index b6a8ae83..c4af87ee 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -10,6 +10,7 @@ import android.provider.MediaStore import android.support.v4.media.MediaDescriptionCompat import android.util.Log import androidx.media.utils.MediaConstants +import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.fasterxml.jackson.annotation.JsonIgnore @@ -46,7 +47,7 @@ class LocalLibraryItem( @JsonIgnore fun getCoverUri(): Uri { - return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) + return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) } @JsonIgnore diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index 0616efc8..d7fccbb0 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -6,6 +6,8 @@ import android.net.Uri import android.os.Build import android.provider.MediaStore import android.support.v4.media.MediaMetadataCompat +import com.audiobookshelf.app.BuildConfig +import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.media.MediaProgressSyncData import com.fasterxml.jackson.annotation.JsonIgnore @@ -136,9 +138,9 @@ class PlaybackSession( @JsonIgnore fun getCoverUri(): Uri { - if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon) + if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) - if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon) + if (coverPath == null) return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}") } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt b/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt index 3a2c2064..12cb827a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt @@ -8,6 +8,7 @@ import android.os.Build import android.provider.MediaStore import android.support.v4.media.session.MediaControllerCompat import android.util.Log +import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.R import com.bumptech.glide.Glide import com.google.android.exoplayer2.Player @@ -83,7 +84,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl Glide.with(playerNotificationService) .asBitmap() - .load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)) + .load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)) .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) .get() } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index 222bd5f6..cc2ba1f6 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -190,6 +190,14 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi handleMediaButtonClickCount() } } + KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { + Log.d(tag, "handleCallMediaButton: Media Fast Forward") + playerNotificationService.jumpForward() + } + KeyEvent.KEYCODE_MEDIA_REWIND -> { + Log.d(tag, "handleCallMediaButton: Media Rewind") + playerNotificationService.jumpBackward() + } } } @@ -224,12 +232,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { playerNotificationService.jumpBackward() } - KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { - playerNotificationService.jumpForward() - } - KeyEvent.KEYCODE_MEDIA_REWIND -> { - playerNotificationService.jumpBackward() - } KeyEvent.KEYCODE_MEDIA_STOP -> { playerNotificationService.closePlayback() } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 71f12af6..f21b93a2 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -1,6 +1,8 @@ package com.audiobookshelf.app.player import android.app.* +import android.appwidget.AppWidgetManager +import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -21,12 +23,15 @@ import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log +import android.view.View +import android.widget.RemoteViews import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import com.audiobookshelf.app.BuildConfig +import com.audiobookshelf.app.MediaPlayerWidget import com.audiobookshelf.app.R import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.DeviceInfo @@ -170,12 +175,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } Log.d(tag, "onDestroy") + isStarted = false + isClosed = true + DeviceManager.widgetUpdater?.onPlayerChanged(this) + playerNotificationManager.setPlayer(null) mPlayer.release() castPlayer?.release() mediaSession.release() mediaProgressSyncer.reset() - isStarted = false + super.onDestroy() } @@ -184,6 +193,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) Log.d(tag, "onTaskRemoved") + stopSelf() } @@ -258,7 +268,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX) playerNotificationManager.setUseFastForwardActionInCompactView(true) playerNotificationManager.setUseRewindActionInCompactView(true) - playerNotificationManager.setSmallIcon(R.drawable.exo_icon_localaudio) + playerNotificationManager.setSmallIcon(R.drawable.icon_monochrome) // Unknown action playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE) @@ -409,9 +419,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}") - + // Notify client clientEventEmitter?.onPlaybackSession(playbackSession) + // Update widget + DeviceManager.widgetUpdater?.onPlayerChanged(this) + if (mediaItems.isEmpty()) { Log.e(tag, "Invalid playback session no media items to play") currentPlaybackSession = null diff --git a/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png deleted file mode 100644 index 894b069a..00000000 Binary files a/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-nodpi/media_player_widget_preview.png b/android/app/src/main/res/drawable-nodpi/media_player_widget_preview.png new file mode 100644 index 00000000..27a07efe Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/media_player_widget_preview.png differ diff --git a/android/app/src/main/res/drawable/icon_monochrome.xml b/android/app/src/main/res/drawable/icon_monochrome.xml new file mode 100644 index 00000000..594c52c7 --- /dev/null +++ b/android/app/src/main/res/drawable/icon_monochrome.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/layout/media_player_widget.xml b/android/app/src/main/res/layout/media_player_widget.xml new file mode 100644 index 00000000..a71a7d33 --- /dev/null +++ b/android/app/src/main/res/layout/media_player_widget.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/new_app_widget.xml b/android/app/src/main/res/layout/new_app_widget.xml deleted file mode 100644 index 99ac1b46..00000000 --- a/android/app/src/main/res/layout/new_app_widget.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/values-night-v31/themes.xml b/android/app/src/main/res/values-night-v31/themes.xml new file mode 100644 index 00000000..db9d77f0 --- /dev/null +++ b/android/app/src/main/res/values-night-v31/themes.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..3a0135c0 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/android/app/src/main/res/values-v31/themes.xml b/android/app/src/main/res/values-v31/themes.xml new file mode 100644 index 00000000..8ffe2e9f --- /dev/null +++ b/android/app/src/main/res/values-v31/themes.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml index 7781ac86..3f37ec30 100644 --- a/android/app/src/main/res/values/attrs.xml +++ b/android/app/src/main/res/values/attrs.xml @@ -1,7 +1,9 @@ - - - - - - \ No newline at end of file + + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 713e8445..4234d998 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,33 +1,42 @@ - - + + - + - - + - + + + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 254f4a55..fc7585c3 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -1,17 +1,17 @@ - + 0dp + - + diff --git a/android/app/src/main/res/xml/media_player_widget_info.xml b/android/app/src/main/res/xml/media_player_widget_info.xml new file mode 100644 index 00000000..45dab3b3 --- /dev/null +++ b/android/app/src/main/res/xml/media_player_widget_info.xml @@ -0,0 +1,14 @@ + + diff --git a/android/app/src/main/res/xml/new_app_widget_info.xml b/android/app/src/main/res/xml/new_app_widget_info.xml deleted file mode 100644 index ffa76ee7..00000000 --- a/android/app/src/main/res/xml/new_app_widget_info.xml +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/assets/app.css b/assets/app.css index a7a06d1a..15ab64ab 100644 --- a/assets/app.css +++ b/assets/app.css @@ -2,6 +2,19 @@ @import './defaultStyles.css'; @import './absicons.css'; +* { + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +input, +textarea { + -webkit-touch-callout: auto; + -webkit-user-select: auto; + user-select: auto; +} + body { background-color: #262626; } diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 1564ba08..e92623cb 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -15,24 +15,24 @@

{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}

-
+

{{ currentTimePretty }}

{{ totalTimeRemainingPretty }}

-
-
-
-
+
+
+
+
- +
@@ -46,7 +46,7 @@
-
+
{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }} @@ -65,8 +65,8 @@
-
-
+
+
first_page {{ jumpBackwardsIcon }}
@@ -80,20 +80,17 @@
-
-
+
+

0:00

-

{{ currentChapterTitle }}

-

{{ timeRemainingPretty }}

-
-
-
-
-
-
+
+
+
+
+
@@ -151,8 +148,10 @@ export default { useTotalTrack: true, lockUi: false, isLoading: false, - touchTrackStart: false, - dragPercent: 0, + isDraggingCursor: false, + draggingTouchStartX: 0, + draggingTouchStartTime: 0, + draggingCurrentTime: 0, syncStatus: 0, showMoreMenuDialog: false, coverRgb: 'rgb(55, 56, 56)', @@ -287,6 +286,7 @@ export default { return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY }, title() { + if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle if (this.playbackSession) return this.playbackSession.displayTitle return this.mediaMetadata ? this.mediaMetadata.title : 'Title' }, @@ -318,17 +318,20 @@ export default { return this.$secondsToTimestamp(this.totalDuration) }, currentTimePretty() { - return this.$secondsToTimestamp(this.currentTime / this.currentPlaybackRate) + let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime + return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate) }, timeRemaining() { + let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime if (this.useChapterTrack && this.currentChapter) { - var currChapTime = this.currentTime - this.currentChapter.start + var currChapTime = currentTimeToUse - this.currentChapter.start return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate } return this.totalTimeRemaining }, totalTimeRemaining() { - return (this.totalDuration - this.currentTime) / this.currentPlaybackRate + let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime + return (this.totalDuration - currentTimeToUse) / this.currentPlaybackRate }, totalTimeRemainingPretty() { if (this.totalTimeRemaining < 0) { @@ -342,10 +345,6 @@ export default { } return '-' + this.$secondsToTimestamp(this.timeRemaining) }, - timeLeftInChapter() { - if (!this.currentChapter) return 0 - return this.currentChapter.end - this.currentTime - }, sleepTimeRemainingPretty() { if (!this.sleepTimeRemaining) return '0s' var secondsRemaining = Math.round(this.sleepTimeRemaining) @@ -392,11 +391,6 @@ export default { this.showFullscreen = false } }, - async touchstartTrack(e) { - await this.$hapticsImpact() - if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return - this.touchTrackStart = true - }, async selectChapter(chapter) { await this.$hapticsImpact() this.seek(chapter.start) @@ -450,7 +444,6 @@ export default { this.$emit('showSleepTimer') }, async setPlaybackSpeed(speed) { - await this.$hapticsImpact() console.log(`[AudioPlayer] Set Playback Rate: ${speed}`) this.currentPlaybackRate = speed this.updateTimestamp() @@ -509,12 +502,13 @@ export default { console.error('No timestamp el') return } - let currentTime = this.currentTime / this.currentPlaybackRate + + let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime if (this.useChapterTrack && this.currentChapter) { - const currChapTime = Math.max(0, this.currentTime - this.currentChapter.start) - currentTime = currChapTime / this.currentPlaybackRate + currentTime = Math.max(0, currentTime - this.currentChapter.start) } - ts.innerText = this.$secondsToTimestamp(currentTime) + + ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate) }, timeupdate() { if (!this.$refs.playedTrack) { @@ -536,22 +530,24 @@ export default { }, updateTrack() { // Update progress track UI - let percentDone = this.currentTime / this.totalDuration + let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime + let percentDone = currentTimeToUse / this.totalDuration const totalPercentDone = percentDone let bufferedPercent = this.bufferedTime / this.totalDuration const totalBufferedPercent = bufferedPercent if (this.useChapterTrack && this.currentChapter) { - const currChapTime = this.currentTime - this.currentChapter.start + const currChapTime = currentTimeToUse - this.currentChapter.start percentDone = currChapTime / this.currentChapterDuration bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration)) } + const ptWidth = Math.round(percentDone * this.trackWidth) this.$refs.playedTrack.style.width = ptWidth + 'px' this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px' if (this.$refs.trackCursor) { - this.$refs.trackCursor.style.left = ptWidth - 8 + 'px' + this.$refs.trackCursor.style.left = ptWidth - 7 + 'px' } if (this.useChapterTrack) { @@ -580,27 +576,15 @@ export default { this.$refs.playedTrack.classList.add('bg-yellow-300') } }, - clickTrack(e) { - if (this.isLoading || this.lockUi) return - if (!this.showFullscreen) { - // Track not clickable on mini-player - return - } - if (e) e.stopPropagation() + async touchstartCursor(e) { + if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return - var offsetX = e.offsetX - var perc = offsetX / this.trackWidth - var time = 0 - if (this.useChapterTrack && this.currentChapter) { - time = perc * this.currentChapterDuration + this.currentChapter.start - } else { - time = perc * this.totalDuration - } - if (isNaN(time) || time === null) { - console.error('Invalid time', perc, time) - return - } - this.seek(time) + await this.$hapticsImpact() + this.isDraggingCursor = true + this.draggingTouchStartX = e.touches[0].pageX + this.draggingTouchStartTime = this.currentTime + this.draggingCurrentTime = this.currentTime + this.updateTrack() }, async playPauseClick() { await this.$hapticsImpact() @@ -653,24 +637,11 @@ export default { touchend(e) { if (!e.changedTouches) return - if (this.touchTrackStart) { - var touch = e.changedTouches[0] - const touchOnTrackPos = touch.pageX - 12 - const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth)) - - var seekToTime = 0 - if (this.useChapterTrack && this.currentChapter) { - const currChapTime = dragPercent * this.currentChapterDuration - seekToTime = this.currentChapter.start + currChapTime - } else { - seekToTime = dragPercent * this.totalDuration + if (this.isDraggingCursor) { + if (this.draggingCurrentTime !== this.currentTime) { + this.seek(this.draggingCurrentTime) } - this.seek(seekToTime) - - if (this.$refs.draggingTrack) { - this.$refs.draggingTrack.style.width = '0px' - } - this.touchTrackStart = false + this.isDraggingCursor = false } else if (this.showFullscreen) { this.touchEndY = e.changedTouches[0].screenY var touchDuration = Date.now() - this.touchStartTime @@ -682,29 +653,24 @@ export default { } }, touchmove(e) { - if (!this.touchTrackStart) return + if (!this.isDraggingCursor || !e.touches) return - var touch = e.touches[0] - const touchOnTrackPos = touch.pageX - 12 - const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth)) - this.dragPercent = dragPercent - - if (this.$refs.draggingTrack) { - this.$refs.draggingTrack.style.width = this.dragPercent * this.trackWidth + 'px' + const distanceMoved = e.touches[0].pageX - this.draggingTouchStartX + let duration = this.totalDuration + let minTime = 0 + let maxTime = duration + if (this.useChapterTrack && this.currentChapter) { + duration = this.currentChapterDuration + minTime = this.currentChapter.start + maxTime = minTime + duration } - var ts = this.$refs.currentTimestamp - if (ts) { - var currTimeStr = '' - if (this.useChapterTrack && this.currentChapter) { - const currChapTime = dragPercent * this.currentChapterDuration - currTimeStr = this.$secondsToTimestamp(currChapTime) - } else { - const dragTime = dragPercent * this.totalDuration - currTimeStr = this.$secondsToTimestamp(dragTime) - } - ts.innerText = currTimeStr - } + const timePerPixel = duration / this.trackWidth + const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved + this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime)) + + this.updateTimestamp() + this.updateTrack() }, async clickMenuAction(action) { await this.$hapticsImpact() @@ -850,7 +816,7 @@ export default { document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px') document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px') document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px') - document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 24 + coverImageWidthCollapsed + 'px') + document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px') }, minimizePlayerEvt() { this.collapseFullscreen() @@ -917,7 +883,7 @@ export default { --cover-image-height: 0px; --cover-image-width-collapsed: 46px; --cover-image-height-collapsed: 46px; - --title-author-left-offset-collapsed: 70px; + --title-author-left-offset-collapsed: 80px; } .playerContainer { @@ -944,12 +910,14 @@ export default { .cover-wrapper { bottom: 68px; - left: 12px; + left: 24px; height: var(--cover-image-height-collapsed); width: var(--cover-image-width-collapsed); transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1); transition-property: left, bottom, width, height; transform-origin: left bottom; + border-radius: 3px; + overflow: hidden; } .total-track { @@ -990,19 +958,17 @@ export default { pointer-events: auto; } .fullscreen .title-author-texts .title-text { - font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.5rem); + font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.3rem); } .fullscreen .title-author-texts .author-text { - font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1.1rem); + font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1rem); } #playerControls { transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition-property: width, bottom; - height: 48px; - width: 140px; - padding-left: 12px; - padding-right: 12px; + width: 128px; + padding-right: 24px; bottom: 70px; } #playerControls .jump-icon { @@ -1020,7 +986,7 @@ export default { width: 40px; min-width: 40px; min-height: 40px; - margin: 0px 14px; + margin: 0px 7px; } #playerControls .play-btn .material-icons { transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); @@ -1035,18 +1001,21 @@ export default { width: var(--cover-image-width); left: calc(50% - (calc(var(--cover-image-width)) / 2)); bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2)); + border-radius: 16px; + overflow: hidden; } .fullscreen #playerControls { width: 100%; - bottom: 94px; + padding-left: 24px; + padding-right: 24px; + bottom: 78px; + left: 0; } .fullscreen #playerControls .jump-icon { - margin: 0px 18px; font-size: 2.4rem; } .fullscreen #playerControls .next-icon { - margin: 0px 20px; font-size: 2rem; } .fullscreen #playerControls .play-btn { @@ -1054,7 +1023,6 @@ export default { width: 65px; min-width: 65px; min-height: 65px; - margin: 0px 26px; } .fullscreen #playerControls .play-btn .material-icons { font-size: 2.1rem; diff --git a/components/bookshelf/LazyBookshelf.vue b/components/bookshelf/LazyBookshelf.vue index 2eede182..4309238a 100644 --- a/components/bookshelf/LazyBookshelf.vue +++ b/components/bookshelf/LazyBookshelf.vue @@ -50,6 +50,9 @@ export default { watch: { showBookshelfListView(newVal) { this.resetEntities() + }, + seriesId() { + this.resetEntities() } }, computed: { @@ -85,6 +88,12 @@ export default { filterBy() { return this.$store.getters['user/getUserSetting']('mobileFilterBy') }, + collapseSeries() { + return this.$store.getters['user/getUserSetting']('collapseSeries') + }, + collapseBookSeries() { + return this.$store.getters['user/getUserSetting']('collapseBookSeries') + }, isCoverSquareAspectRatio() { return this.bookCoverAspectRatio === 1 }, @@ -356,6 +365,9 @@ export default { let searchParams = new URLSearchParams() if (this.page === 'series-books') { searchParams.set('filter', `series.${this.$encode(this.seriesId)}`) + if (this.collapseBookSeries) { + searchParams.set('collapseseries', 1) + } } else { if (this.filterBy && this.filterBy !== 'all') { searchParams.set('filter', this.filterBy) diff --git a/components/cards/LazyBookCard.vue b/components/cards/LazyBookCard.vue index 0ff4f236..65b763d0 100644 --- a/components/cards/LazyBookCard.vue +++ b/components/cards/LazyBookCard.vue @@ -10,11 +10,16 @@

{{ displayTitle }}

-

{{ displayAuthor || ' ' }}

+

{{ displayLineTwo || ' ' }}

{{ displaySortLine }}

-
{{ booksInSeries }}
+
+

#{{ seriesSequenceList }}

+
+
+

{{ booksInSeries }}

+
@@ -226,22 +231,36 @@ export default { // Only added to item object when collapseSeries is enabled return this.collapsedSeries ? this.collapsedSeries.numBooks : 0 }, - displayTitle() { - if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) { - return this.title.substr(4) + ', The' - } - return this.title + seriesSequenceList() { + return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null }, - displayAuthor() { + libraryItemIdsInSeries() { + // Only added to item object when collapseSeries is enabled + return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : [] + }, + displayTitle() { + if (this.recentEpisode) return this.recentEpisode.title + + const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix + if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name + return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title + }, + displayLineTwo() { + if (this.recentEpisode) return this.title + if (this.collapsedSeries) return '' + if (this.isPodcast) return this.author + if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF return this.author }, displaySortLine() { + if (this.collapsedSeries) return null 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 === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) 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 === 'media.numTracks') return `${this.numEpisodes} Episodes` return null }, episodeProgress() { @@ -434,7 +453,7 @@ export default { const router = this.$router || this.$nuxt.$router if (router) { if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`) - else if (this.collapsedSeries) router.push(`/library/${this.libraryId}/series/${this.collapsedSeries.id}`) + else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`) else router.push(`/item/${this.libraryItemId}`) } } diff --git a/components/home/BookshelfNavBar.vue b/components/home/BookshelfNavBar.vue index 2508120b..06294c1e 100644 --- a/components/home/BookshelfNavBar.vue +++ b/components/home/BookshelfNavBar.vue @@ -101,15 +101,15 @@ export default { icon: 'collections_bookmark', iconClass: 'text-xl', text: 'Collections' + }, + { + to: '/bookshelf/authors', + routeName: 'bookshelf-authors', + iconPack: 'abs-icons', + icon: 'authors', + iconClass: 'text-2xl', + text: 'Authors' } - // { - // to: '/bookshelf/authors', - // routeName: 'bookshelf-authors', - // iconPack: 'abs-icons', - // icon: 'authors', - // iconClass: 'text-2xl pb-px', - // text: 'Authors' - // } ] } diff --git a/components/home/BookshelfToolbar.vue b/components/home/BookshelfToolbar.vue index 148bc71a..c6b09b4e 100644 --- a/components/home/BookshelfToolbar.vue +++ b/components/home/BookshelfToolbar.vue @@ -2,11 +2,8 @@
- - arrow_back -

{{ totalEntities }} {{ entityTitle }}

-

{{ selectedSeriesName }} ({{ totalEntities }})

+

{{ selectedSeriesName }} ({{ totalEntities }})

{{ !bookshelfListView ? 'view_list' : 'grid_view' }} + more_vert
+
@@ -31,7 +30,8 @@ export default { showSortModal: false, showFilterModal: false, settings: {}, - totalEntities: 0 + totalEntities: 0, + showMoreMenuDialog: false } }, computed: { @@ -44,6 +44,12 @@ export default { this.$store.commit('globals/setBookshelfListView', val) } }, + currentLibraryMediaType() { + return this.$store.getters['libraries/getCurrentLibraryMediaType'] + }, + isBookLibrary() { + return this.currentLibraryMediaType === 'book' + }, hasFilters() { return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all' }, @@ -66,6 +72,8 @@ export default { return 'Collections' } else if (this.page === 'playlists') { return 'Playlists' + } else if (this.page === 'authors') { + return 'Authors' } return '' }, @@ -77,9 +85,40 @@ export default { }, isPodcast() { return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast' + }, + menuItems() { + if (!this.isBookLibrary) return [] + + if (this.seriesBookPage) { + return [ + { + text: 'Collapse Sub-Series', + value: 'collapse_subseries', + icon: this.settings.collapseBookSeries ? 'check_box' : 'check_box_outline_blank' + } + ] + } else { + return [ + { + text: 'Collapse Series', + value: 'collapse_series', + icon: this.settings.collapseSeries ? 'check_box' : 'check_box_outline_blank' + } + ] + } } }, methods: { + clickMenuAction(action) { + this.showMoreMenuDialog = false + if (action === 'collapse_series') { + this.settings.collapseSeries = !this.settings.collapseSeries + this.saveSettings() + } else if (action === 'collapse_subseries') { + this.settings.collapseBookSeries = !this.settings.collapseBookSeries + this.saveSettings() + } + }, updateOrder() { this.saveSettings() }, diff --git a/components/modals/BookmarksModal.vue b/components/modals/BookmarksModal.vue index 5c7fc805..675758cf 100644 --- a/components/modals/BookmarksModal.vue +++ b/components/modals/BookmarksModal.vue @@ -1,7 +1,7 @@