Merge branch 'master' into center-title

This commit is contained in:
advplyr
2023-03-06 13:48:30 -06:00
60 changed files with 804 additions and 459 deletions
+11 -11
View File
@@ -23,18 +23,18 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" > android:usesCleartextTraffic="true" >
<!-- TODO: Incomplete desktop widget --> <!-- Android app widget -->
<!-- <receiver--> <receiver
<!-- android:name=".NewAppWidget"--> android:name="MediaPlayerWidget"
<!-- android:exported="true" >--> android:exported="false">
<!-- <intent-filter>--> <intent-filter>
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />--> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<!-- </intent-filter>--> </intent-filter>
<!-- <meta-data--> <meta-data
<!-- android:name="android.appwidget.provider"--> android:name="android.appwidget.provider"
<!-- android:resource="@xml/new_app_widget_info" />--> android:resource="@xml/media_player_widget_info" />
<!-- </receiver>--> </receiver>
<!-- Used by Android Auto --> <!-- Used by Android Auto -->
<meta-data <meta-data
@@ -0,0 +1,123 @@
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
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.media.session.MediaButtonReceiver
import com.audiobookshelf.app.data.PlaybackSession
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 MediaPlayerWidget : AppWidgetProvider() {
val tag = "MediaPlayerWidget"
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, PlayerNotificationService.isClosed)
}
}
override fun onEnabled(context: Context) {
Log.i(tag, "onEnabled check context ${context.packageName}")
DeviceManager.deviceData.lastPlaybackSession?.let {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
Log.d(tag, "Setting initial widget state with last playback session ${it.displayTitle}")
for (widgetId in ids) {
updateAppWidget(context, appWidgetManager, widgetId, it, false, true)
}
}
// Enter relevant functionality for when the first widget is created
DeviceManager.widgetUpdater = (object : WidgetEventEmitter {
override fun onPlayerChanged(pns: PlayerNotificationService) {
val isPlaying = pns.currentPlayer.isPlaying
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
val playbackSession = pns.getCurrentPlaybackSessionCopy()
for (widgetId in ids) {
updateAppWidget(context, appWidgetManager, widgetId, playbackSession, isPlaying, PlayerNotificationService.isClosed)
}
}
})
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, playbackSession: PlaybackSession?, isPlaying:Boolean, isAppClosed:Boolean) {
val tag = "MediaPlayerWidget"
val views = RemoteViews(context.packageName, R.layout.media_player_widget)
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
)
val playPausePI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
views.setOnClickPendingIntent(R.id.widgetPlayPauseButton, playPausePI)
val fastForwardPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_FAST_FORWARD)
views.setOnClickPendingIntent(R.id.widgetFastForwardButton, fastForwardPI)
val rewindPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_REWIND)
views.setOnClickPendingIntent(R.id.widgetRewindButton, rewindPI)
// Show/Hide button container
views.setViewVisibility(R.id.widgetButtonContainer, if (isAppClosed) View.GONE else View.VISIBLE)
views.setOnClickPendingIntent(R.id.widgetBackground, wholeWidgetClickPI)
val imageUri = playbackSession?.getCoverUri() ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.widgetAlbumArt, views, appWidgetId) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
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)
}
@@ -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<in Bitmap>?) {
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)
}
@@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
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
@@ -41,7 +42,7 @@ class LibraryItem(
@JsonIgnore @JsonIgnore
fun getCoverUri(): Uri { fun getCoverUri(): Uri {
if (media.coverPath == null) { 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}") return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
@@ -10,6 +10,7 @@ import android.provider.MediaStore
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
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
@@ -46,7 +47,7 @@ class LocalLibraryItem(
@JsonIgnore @JsonIgnore
fun getCoverUri(): Uri { 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 @JsonIgnore
@@ -6,6 +6,8 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat 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.device.DeviceManager
import com.audiobookshelf.app.media.MediaProgressSyncData import com.audiobookshelf.app.media.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
@@ -136,9 +138,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/" + 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}") return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
} }
@@ -8,6 +8,7 @@ import android.os.Build
import android.provider.MediaStore 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.BuildConfig
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
@@ -83,7 +84,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
Glide.with(playerNotificationService) Glide.with(playerNotificationService)
.asBitmap() .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) .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get() .get()
} }
@@ -190,6 +190,14 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
handleMediaButtonClickCount() 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 -> { KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playerNotificationService.jumpBackward() 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()
} }
@@ -1,6 +1,8 @@
package com.audiobookshelf.app.player package com.audiobookshelf.app.player
import android.app.* import android.app.*
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap 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.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import android.view.View
import android.widget.RemoteViews
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.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
import com.audiobookshelf.app.MediaPlayerWidget
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceInfo import com.audiobookshelf.app.data.DeviceInfo
@@ -170,12 +175,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
Log.d(tag, "onDestroy") Log.d(tag, "onDestroy")
isStarted = false
isClosed = true
DeviceManager.widgetUpdater?.onPlayerChanged(this)
playerNotificationManager.setPlayer(null) playerNotificationManager.setPlayer(null)
mPlayer.release() mPlayer.release()
castPlayer?.release() castPlayer?.release()
mediaSession.release() mediaSession.release()
mediaProgressSyncer.reset() mediaProgressSyncer.reset()
isStarted = false
super.onDestroy() super.onDestroy()
} }
@@ -184,6 +193,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
Log.d(tag, "onTaskRemoved") Log.d(tag, "onTaskRemoved")
stopSelf() stopSelf()
} }
@@ -258,7 +268,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) playerNotificationManager.setSmallIcon(R.drawable.icon_monochrome)
// Unknown action // Unknown action
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE) 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 DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}") Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
// Notify client
clientEventEmitter?.onPlaybackSession(playbackSession) clientEventEmitter?.onPlaybackSession(playbackSession)
// Update widget
DeviceManager.widgetUpdater?.onPlayerChanged(this)
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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="862dp"
android:height="1024dp"
android:viewportWidth="862"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFF"
android:pathData="M855,504.4c-5.3,-4.4 -13.5,-10.9 -24.7,-18.6v-86.5c0,-220.5 -178.8,-399.3 -399.3,-399.3v0c-220.5,0 -399.3,178.8 -399.3,399.3v86.5c-11.1,7.7 -19.4,14.2 -24.7,18.6 -4.4,3.7 -7.1,9.1 -7.1,15.2 0,0 0,0 0,0v-0,103.6c0,0 0,0 0,0 0,6.1 2.8,11.6 7.1,15.2l0,0c12.3,10.3 40.6,31.8 84.7,53.8v10.1c0,27.2 17.5,49.2 39.1,49.2v0c21.6,0 39.1,-22 39.1,-49.2v-248.5c0,-27.2 -17.5,-49.2 -39.1,-49.2v0c-20.7,0 -37.7,20.2 -39,45.8l-0.1,0v-51.2c0,-187.4 151.9,-339.3 339.3,-339.3v0c187.4,0 339.3,151.9 339.3,339.3v51.2l-0.1,-0c-1.4,-25.6 -18.3,-45.8 -39,-45.8v0c-21.6,0 -39.1,22 -39.1,49.2v248.5c0,27.2 17.5,49.2 39.1,49.2v0c21.6,0 39.1,-22 39.1,-49.2v-10.1c44,-22.1 72.3,-43.6 84.7,-53.8 4.4,-3.7 7.1,-9.1 7.1,-15.2 0,-0 0,-0 0,-0v0,-103.6c0,-0 0,-0 0,-0 0,-6.1 -2.8,-11.6 -7.1,-15.2l-0,-0zM291.8,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM213.5,439.4h107.8v11.3h-107.8zM455.5,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM377.2,439.4h107.8v11.3h-107.8zM619.1,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM540.8,439.4h107.8v11.3h-107.8zM114.1,952.7h633.9c19.7,0 35.6,15.9 35.6,35.6v0c0,19.7 -15.9,35.6 -35.6,35.6h-633.9c-19.7,0 -35.6,-15.9 -35.6,-35.6v-0c0,-19.7 15.9,-35.6 35.6,-35.6z"/>
</vector>
@@ -0,0 +1,108 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/widgetBackground"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
android:theme="@style/AppTheme.AppWidgetContainer"
style="@style/Widget.Android.AppWidget.Container">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:background="?android:attr/colorAccent"
android:theme="@style/MediaPlayerWidgetTheme"
android:orientation="horizontal">
<ImageView
android:id="@+id/widgetAlbumArt"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:scaleType="centerCrop"
android:src="@drawable/icon" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:layout_marginVertical="8dp"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:id="@+id/widgetArtistText"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:layout_marginLeft="8dp"
android:gravity="bottom"
android:ellipsize="end"
android:maxLines="1"
android:text="Artist"/>
<TextView
android:id="@+id/widgetMediaTitle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:layout_marginLeft="8dp"
android:gravity="top"
android:ellipsize="end"
android:maxLines="1"
android:text="Title"/>
<LinearLayout
android:id="@+id/widgetButtonContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:orientation="horizontal"
android:background="?android:attr/colorAccent"
android:theme="@style/MediaPlayerWidgetTheme">
<ImageView
android:id="@+id/widgetRewindButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_margin="8dp"
android:src="@drawable/exo_icon_rewind"
style="@style/Widget.Android.AppWidget.InnerView"/>
<ImageView
android:id="@+id/widgetPlayPauseButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_margin="6dp"
android:src="@drawable/ic_media_play_dark" />
<ImageView
android:id="@+id/widgetFastForwardButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_margin="8dp"
android:src="@drawable/exo_icon_fastforward" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="36dp"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/tinyCornerIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_margin="6dp"
android:adjustViewBounds ="true"
android:src="@drawable/icon_monochrome" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -1,26 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Android.AppWidget.Container"
android:id="@+id/appWidget"
android:theme="@style/AppTheme.AppWidgetContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
android:alpha="0.75"
android:contentDescription="Cover image"
android:visibility="visible" />
<ImageView
android:id="@+id/playPauseIcon"
android:layout_width="71dp"
android:layout_height="55dp"
android:layout_centerInParent="true"
app:srcCompat="@drawable/cast_ic_expanded_controller_play" />
</RelativeLayout>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
-->
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>
@@ -0,0 +1,16 @@
<resources>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
<item name="android:clipToOutline">true</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<!-- <item name="android:background">@drawable/app_widget_inner_view_background</item>-->
<!-- <item name="android:textColor">?android:attr/textColorPrimary</item>-->
<item name="android:clipToOutline">true</item>
</style>
</resources>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
and @android:dimen/system_app_widget_internal_padding requires API level 31
-->
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>
+8 -6
View File
@@ -1,7 +1,9 @@
<resources> <resources>
<declare-styleable name="AppWidgetAttrs"> <declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" /> <attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" /> <attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" /> <attr name="appWidgetRadius" format="dimension" />
</declare-styleable> <attr name="backgroundRadius" format="dimension" />
</resources> <attr name="appWidgetViewPadding" format="dimension" />
</declare-styleable>
</resources>
+34 -25
View File
@@ -1,33 +1,42 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
</style> </style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar"> <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<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:statusBarColor">@color/background_dark</item>
<item name="android:navigationBarColor">@color/background_dark</item> <item name="android:navigationBarColor">@color/background_dark</item>
</style> </style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen"> <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<!-- <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">
<item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget"> <style name="MediaPlayerWidgetTheme" parent="Theme.AppCompat.DayNight">
<item name="android:background">?android:attr/colorBackground</item> <!-- These colors match the vue pages -->
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:colorBackground">#232323</item>
</style> <item name="android:colorAccent">#373838</item>
<item name="android:textColor">#ffffff</item>
</style>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<!-- <item name="android:background">?android:attr/colorBackground</item>-->
<!-- <item name="android:textColor">?android:attr/textColorPrimary</item>-->
<item name="android:padding">0dp</item>
</style>
</resources> </resources>
+11 -11
View File
@@ -1,17 +1,17 @@
<resources> <resources>
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<!-- Radius of the outer bound of widgets to make the rounded corners --> <style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<item name="appWidgetRadius">16dp</item> <!-- Radius of the outer bound of widgets to make the rounded corners -->
<!-- <item name="appWidgetRadius">0dp</item>
<!--
Radius of the inner view's bound of widgets to make the rounded corners. Radius of the inner view's bound of widgets to make the rounded corners.
It needs to be 8dp or less than the value of appWidgetRadius It needs to be 8dp or less than the value of appWidgetRadius
--> -->
<item name="appWidgetInnerRadius">8dp</item> <item name="appWidgetInnerRadius">0dp</item>
</style> </style>
<style name="AppTheme.AppWidgetContainer" <style name="AppTheme.AppWidgetContainer" parent="AppTheme.AppWidgetContainerParent">
parent="AppTheme.AppWidgetContainerParent"> <!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners --> <item name="appWidgetPadding">0dp</item>
<item name="appWidgetPadding">16dp</item> </style>
</style>
</resources> </resources>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialKeyguardLayout="@layout/media_player_widget"
android:initialLayout="@layout/media_player_widget"
android:minWidth="275dp"
android:minHeight="50dp"
android:previewImage="@drawable/media_player_widget_preview"
android:previewLayout="@layout/media_player_widget"
android:resizeMode="horizontal"
android:targetCellWidth="4"
android:targetCellHeight="1"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/example_appwidget_preview"
android:initialLayout="@layout/new_app_widget"
android:description="@string/app_widget_description"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:initialKeyguardLayout="@layout/new_app_widget"
/>
+13
View File
@@ -2,6 +2,19 @@
@import './defaultStyles.css'; @import './defaultStyles.css';
@import './absicons.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 { body {
background-color: #262626; background-color: #262626;
} }
+80 -112
View File
@@ -15,24 +15,24 @@
<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> <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 && useTotalTrack && showFullscreen" class="absolute total-track w-full px-3 z-30"> <div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div class="flex"> <div class="flex">
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p> <p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p> <p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
</div> </div>
<div class="w-full"> <div class="w-full">
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative"> <div class="h-1 w-full bg-gray-500 bg-opacity-50 relative rounded-full">
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" /> <div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" /> <div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" /> <div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none rounded-full" />
</div> </div>
</div> </div>
</div> </div>
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer"> <div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
<div class="w-full h-full flex justify-center"> <div class="w-full h-full flex justify-center">
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" /> <covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" />
</div> </div>
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30"> <div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
@@ -46,7 +46,7 @@
</div> </div>
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer"> <div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px"> <div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
<div class="flex items-center justify-between pointer-events-auto"> <div class="flex items-center justify-between pointer-events-auto">
<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> <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 -->
@@ -65,8 +65,8 @@
</div> </div>
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" /> <div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" />
<div id="playerControls" class="absolute right-0 bottom-0 py-2"> <div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center justify-center"> <div class="flex items-center justify-between max-w-full">
<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 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 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> <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 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @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 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
@@ -80,20 +80,17 @@
</div> </div>
</div> </div>
<div id="playerTrack" class="absolute left-0 w-full px-3"> <div id="playerTrack" class="absolute left-0 w-full px-6">
<div class="flex pb-0.5"> <div class="flex pointer-events-none">
<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>
<div class="flex-grow" /> <div class="flex-grow" />
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
<div class="flex-grow" />
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p> <p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
</div> </div>
<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="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
<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 rounded-full 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 rounded-full 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 rounded-full 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 -top-1 rounded-full bg-gray-200 absolute pointer-events-auto" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor" />
<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> </div>
</div> </div>
@@ -151,8 +148,10 @@ export default {
useTotalTrack: true, useTotalTrack: true,
lockUi: false, lockUi: false,
isLoading: false, isLoading: false,
touchTrackStart: false, isDraggingCursor: false,
dragPercent: 0, draggingTouchStartX: 0,
draggingTouchStartTime: 0,
draggingCurrentTime: 0,
syncStatus: 0, syncStatus: 0,
showMoreMenuDialog: false, showMoreMenuDialog: false,
coverRgb: 'rgb(55, 56, 56)', coverRgb: 'rgb(55, 56, 56)',
@@ -287,6 +286,7 @@ export default {
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
}, },
title() { title() {
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
if (this.playbackSession) return this.playbackSession.displayTitle if (this.playbackSession) return this.playbackSession.displayTitle
return this.mediaMetadata ? this.mediaMetadata.title : 'Title' return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
}, },
@@ -318,17 +318,20 @@ export default {
return this.$secondsToTimestamp(this.totalDuration) return this.$secondsToTimestamp(this.totalDuration)
}, },
currentTimePretty() { currentTimePretty() {
return this.$secondsToTimestamp(this.currentTime / this.currentPlaybackRate) let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
}, },
timeRemaining() { timeRemaining() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) { 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.currentChapterDuration - currChapTime) / this.currentPlaybackRate
} }
return this.totalTimeRemaining return this.totalTimeRemaining
}, },
totalTimeRemaining() { totalTimeRemaining() {
return (this.totalDuration - this.currentTime) / this.currentPlaybackRate let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return (this.totalDuration - currentTimeToUse) / this.currentPlaybackRate
}, },
totalTimeRemainingPretty() { totalTimeRemainingPretty() {
if (this.totalTimeRemaining < 0) { if (this.totalTimeRemaining < 0) {
@@ -342,10 +345,6 @@ export default {
} }
return '-' + this.$secondsToTimestamp(this.timeRemaining) return '-' + this.$secondsToTimestamp(this.timeRemaining)
}, },
timeLeftInChapter() {
if (!this.currentChapter) return 0
return this.currentChapter.end - this.currentTime
},
sleepTimeRemainingPretty() { sleepTimeRemainingPretty() {
if (!this.sleepTimeRemaining) return '0s' if (!this.sleepTimeRemaining) return '0s'
var secondsRemaining = Math.round(this.sleepTimeRemaining) var secondsRemaining = Math.round(this.sleepTimeRemaining)
@@ -392,11 +391,6 @@ export default {
this.showFullscreen = false 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) { async selectChapter(chapter) {
await this.$hapticsImpact() await this.$hapticsImpact()
this.seek(chapter.start) this.seek(chapter.start)
@@ -450,7 +444,6 @@ export default {
this.$emit('showSleepTimer') this.$emit('showSleepTimer')
}, },
async setPlaybackSpeed(speed) { async setPlaybackSpeed(speed) {
await this.$hapticsImpact()
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`) console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
this.currentPlaybackRate = speed this.currentPlaybackRate = speed
this.updateTimestamp() this.updateTimestamp()
@@ -509,12 +502,13 @@ export default {
console.error('No timestamp el') console.error('No timestamp el')
return return
} }
let currentTime = this.currentTime / this.currentPlaybackRate
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) { if (this.useChapterTrack && this.currentChapter) {
const currChapTime = Math.max(0, this.currentTime - this.currentChapter.start) currentTime = Math.max(0, currentTime - this.currentChapter.start)
currentTime = currChapTime / this.currentPlaybackRate
} }
ts.innerText = this.$secondsToTimestamp(currentTime)
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
}, },
timeupdate() { timeupdate() {
if (!this.$refs.playedTrack) { if (!this.$refs.playedTrack) {
@@ -536,22 +530,24 @@ export default {
}, },
updateTrack() { updateTrack() {
// Update progress track UI // 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 const totalPercentDone = percentDone
let bufferedPercent = this.bufferedTime / this.totalDuration let bufferedPercent = this.bufferedTime / this.totalDuration
const totalBufferedPercent = bufferedPercent const totalBufferedPercent = bufferedPercent
if (this.useChapterTrack && this.currentChapter) { if (this.useChapterTrack && this.currentChapter) {
const currChapTime = this.currentTime - this.currentChapter.start const currChapTime = currentTimeToUse - 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))
} }
const 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) { if (this.$refs.trackCursor) {
this.$refs.trackCursor.style.left = ptWidth - 8 + 'px' this.$refs.trackCursor.style.left = ptWidth - 7 + 'px'
} }
if (this.useChapterTrack) { if (this.useChapterTrack) {
@@ -580,27 +576,15 @@ export default {
this.$refs.playedTrack.classList.add('bg-yellow-300') this.$refs.playedTrack.classList.add('bg-yellow-300')
} }
}, },
clickTrack(e) { async touchstartCursor(e) {
if (this.isLoading || this.lockUi) return if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
if (!this.showFullscreen) {
// Track not clickable on mini-player
return
}
if (e) e.stopPropagation()
var offsetX = e.offsetX await this.$hapticsImpact()
var perc = offsetX / this.trackWidth this.isDraggingCursor = true
var time = 0 this.draggingTouchStartX = e.touches[0].pageX
if (this.useChapterTrack && this.currentChapter) { this.draggingTouchStartTime = this.currentTime
time = perc * this.currentChapterDuration + this.currentChapter.start this.draggingCurrentTime = this.currentTime
} else { this.updateTrack()
time = perc * this.totalDuration
}
if (isNaN(time) || time === null) {
console.error('Invalid time', perc, time)
return
}
this.seek(time)
}, },
async playPauseClick() { async playPauseClick() {
await this.$hapticsImpact() await this.$hapticsImpact()
@@ -653,24 +637,11 @@ export default {
touchend(e) { touchend(e) {
if (!e.changedTouches) return if (!e.changedTouches) return
if (this.touchTrackStart) { if (this.isDraggingCursor) {
var touch = e.changedTouches[0] if (this.draggingCurrentTime !== this.currentTime) {
const touchOnTrackPos = touch.pageX - 12 this.seek(this.draggingCurrentTime)
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
} }
this.seek(seekToTime) this.isDraggingCursor = false
if (this.$refs.draggingTrack) {
this.$refs.draggingTrack.style.width = '0px'
}
this.touchTrackStart = false
} else if (this.showFullscreen) { } else if (this.showFullscreen) {
this.touchEndY = e.changedTouches[0].screenY this.touchEndY = e.changedTouches[0].screenY
var touchDuration = Date.now() - this.touchStartTime var touchDuration = Date.now() - this.touchStartTime
@@ -682,29 +653,24 @@ export default {
} }
}, },
touchmove(e) { touchmove(e) {
if (!this.touchTrackStart) return if (!this.isDraggingCursor || !e.touches) return
var touch = e.touches[0] const distanceMoved = e.touches[0].pageX - this.draggingTouchStartX
const touchOnTrackPos = touch.pageX - 12 let duration = this.totalDuration
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth)) let minTime = 0
this.dragPercent = dragPercent let maxTime = duration
if (this.useChapterTrack && this.currentChapter) {
if (this.$refs.draggingTrack) { duration = this.currentChapterDuration
this.$refs.draggingTrack.style.width = this.dragPercent * this.trackWidth + 'px' minTime = this.currentChapter.start
maxTime = minTime + duration
} }
var ts = this.$refs.currentTimestamp const timePerPixel = duration / this.trackWidth
if (ts) { const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
var currTimeStr = '' this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
if (this.useChapterTrack && this.currentChapter) {
const currChapTime = dragPercent * this.currentChapterDuration this.updateTimestamp()
currTimeStr = this.$secondsToTimestamp(currChapTime) this.updateTrack()
} else {
const dragTime = dragPercent * this.totalDuration
currTimeStr = this.$secondsToTimestamp(dragTime)
}
ts.innerText = currTimeStr
}
}, },
async clickMenuAction(action) { async clickMenuAction(action) {
await this.$hapticsImpact() await this.$hapticsImpact()
@@ -850,7 +816,7 @@ export default {
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px') 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-width-collapsed', coverImageWidthCollapsed + 'px')
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + '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() { minimizePlayerEvt() {
this.collapseFullscreen() this.collapseFullscreen()
@@ -917,7 +883,7 @@ export default {
--cover-image-height: 0px; --cover-image-height: 0px;
--cover-image-width-collapsed: 46px; --cover-image-width-collapsed: 46px;
--cover-image-height-collapsed: 46px; --cover-image-height-collapsed: 46px;
--title-author-left-offset-collapsed: 70px; --title-author-left-offset-collapsed: 80px;
} }
.playerContainer { .playerContainer {
@@ -944,12 +910,14 @@ export default {
.cover-wrapper { .cover-wrapper {
bottom: 68px; bottom: 68px;
left: 12px; left: 24px;
height: var(--cover-image-height-collapsed); height: var(--cover-image-height-collapsed);
width: var(--cover-image-width-collapsed); width: var(--cover-image-width-collapsed);
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1); transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: left, bottom, width, height; transition-property: left, bottom, width, height;
transform-origin: left bottom; transform-origin: left bottom;
border-radius: 3px;
overflow: hidden;
} }
.total-track { .total-track {
@@ -990,19 +958,17 @@ export default {
pointer-events: auto; pointer-events: auto;
} }
.fullscreen .title-author-texts .title-text { .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 { .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 { #playerControls {
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: width, bottom; transition-property: width, bottom;
height: 48px; width: 128px;
width: 140px; padding-right: 24px;
padding-left: 12px;
padding-right: 12px;
bottom: 70px; bottom: 70px;
} }
#playerControls .jump-icon { #playerControls .jump-icon {
@@ -1020,7 +986,7 @@ export default {
width: 40px; width: 40px;
min-width: 40px; min-width: 40px;
min-height: 40px; min-height: 40px;
margin: 0px 14px; margin: 0px 7px;
} }
#playerControls .play-btn .material-icons { #playerControls .play-btn .material-icons {
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);
@@ -1035,18 +1001,21 @@ export default {
width: var(--cover-image-width); width: var(--cover-image-width);
left: calc(50% - (calc(var(--cover-image-width)) / 2)); left: calc(50% - (calc(var(--cover-image-width)) / 2));
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2)); bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
border-radius: 16px;
overflow: hidden;
} }
.fullscreen #playerControls { .fullscreen #playerControls {
width: 100%; width: 100%;
bottom: 94px; padding-left: 24px;
padding-right: 24px;
bottom: 78px;
left: 0;
} }
.fullscreen #playerControls .jump-icon { .fullscreen #playerControls .jump-icon {
margin: 0px 18px;
font-size: 2.4rem; font-size: 2.4rem;
} }
.fullscreen #playerControls .next-icon { .fullscreen #playerControls .next-icon {
margin: 0px 20px;
font-size: 2rem; font-size: 2rem;
} }
.fullscreen #playerControls .play-btn { .fullscreen #playerControls .play-btn {
@@ -1054,7 +1023,6 @@ export default {
width: 65px; width: 65px;
min-width: 65px; min-width: 65px;
min-height: 65px; min-height: 65px;
margin: 0px 26px;
} }
.fullscreen #playerControls .play-btn .material-icons { .fullscreen #playerControls .play-btn .material-icons {
font-size: 2.1rem; font-size: 2.1rem;
+12
View File
@@ -50,6 +50,9 @@ export default {
watch: { watch: {
showBookshelfListView(newVal) { showBookshelfListView(newVal) {
this.resetEntities() this.resetEntities()
},
seriesId() {
this.resetEntities()
} }
}, },
computed: { computed: {
@@ -85,6 +88,12 @@ export default {
filterBy() { filterBy() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy') return this.$store.getters['user/getUserSetting']('mobileFilterBy')
}, },
collapseSeries() {
return this.$store.getters['user/getUserSetting']('collapseSeries')
},
collapseBookSeries() {
return this.$store.getters['user/getUserSetting']('collapseBookSeries')
},
isCoverSquareAspectRatio() { isCoverSquareAspectRatio() {
return this.bookCoverAspectRatio === 1 return this.bookCoverAspectRatio === 1
}, },
@@ -356,6 +365,9 @@ 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)}`)
if (this.collapseBookSeries) {
searchParams.set('collapseseries', 1)
}
} else { } else {
if (this.filterBy && this.filterBy !== 'all') { if (this.filterBy && this.filterBy !== 'all') {
searchParams.set('filter', this.filterBy) searchParams.set('filter', this.filterBy)
+28 -9
View File
@@ -10,11 +10,16 @@
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }} {{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor || '&nbsp;' }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> </div>
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div> <div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10"> <div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }"> <div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
@@ -226,22 +231,36 @@ export default {
// Only added to item object when collapseSeries is enabled // Only added to item object when collapseSeries is enabled
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0 return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
}, },
displayTitle() { seriesSequenceList() {
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) { return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
return this.title.substr(4) + ', The'
}
return this.title
}, },
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 if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
return this.author return this.author
}, },
displaySortLine() { displaySortLine() {
if (this.collapsedSeries) return null
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 === 'media.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)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
return null return null
}, },
episodeProgress() { episodeProgress() {
@@ -434,7 +453,7 @@ export default {
const router = this.$router || this.$nuxt.$router const router = this.$router || this.$nuxt.$router
if (router) { if (router) {
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`) 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}`) else router.push(`/item/${this.libraryItemId}`)
} }
} }
+8 -8
View File
@@ -101,15 +101,15 @@ export default {
icon: 'collections_bookmark', icon: 'collections_bookmark',
iconClass: 'text-xl', iconClass: 'text-xl',
text: 'Collections' 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'
// }
] ]
} }
+44 -5
View File
@@ -2,11 +2,8 @@
<div class="w-full h-9 bg-bg relative z-20"> <div class="w-full h-9 bg-bg relative z-20">
<div id="bookshelf-toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-2"> <div id="bookshelf-toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-2">
<div class="flex items-center w-full text-sm"> <div class="flex items-center w-full text-sm">
<nuxt-link to="/bookshelf/series" v-if="selectedSeriesName" class="pt-1">
<span class="material-icons">arrow_back</span>
</nuxt-link>
<p v-show="!selectedSeriesName" class="pt-1">{{ totalEntities }} {{ entityTitle }}</p> <p v-show="!selectedSeriesName" class="pt-1">{{ totalEntities }} {{ entityTitle }}</p>
<p v-show="selectedSeriesName" class="ml-2pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p> <p v-show="selectedSeriesName" class="ml-2 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="changeView">{{ !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'">
@@ -16,11 +13,13 @@
</div> </div>
<span class="material-icons px-2" @click="showSortModal = true">sort</span> <span class="material-icons px-2" @click="showSortModal = true">sort</span>
</template> </template>
<span v-if="(page == 'library' && isBookLibrary) || seriesBookPage" class="material-icons px-2" @click="showMoreMenuDialog = true">more_vert</span>
</div> </div>
</div> </div>
<modals-order-modal v-model="showSortModal" :order-by.sync="settings.mobileOrderBy" :descending.sync="settings.mobileOrderDesc" @change="updateOrder" /> <modals-order-modal v-model="showSortModal" :order-by.sync="settings.mobileOrderBy" :descending.sync="settings.mobileOrderDesc" @change="updateOrder" />
<modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.mobileFilterBy" @change="updateFilter" /> <modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.mobileFilterBy" @change="updateFilter" />
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
</div> </div>
</template> </template>
@@ -31,7 +30,8 @@ export default {
showSortModal: false, showSortModal: false,
showFilterModal: false, showFilterModal: false,
settings: {}, settings: {},
totalEntities: 0 totalEntities: 0,
showMoreMenuDialog: false
} }
}, },
computed: { computed: {
@@ -44,6 +44,12 @@ export default {
this.$store.commit('globals/setBookshelfListView', val) this.$store.commit('globals/setBookshelfListView', val)
} }
}, },
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
},
hasFilters() { hasFilters() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all' return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
}, },
@@ -66,6 +72,8 @@ export default {
return 'Collections' return 'Collections'
} else if (this.page === 'playlists') { } else if (this.page === 'playlists') {
return 'Playlists' return 'Playlists'
} else if (this.page === 'authors') {
return 'Authors'
} }
return '' return ''
}, },
@@ -77,9 +85,40 @@ export default {
}, },
isPodcast() { isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast' 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: { 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() { updateOrder() {
this.saveSettings() this.saveSettings()
}, },
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="300" height="100%"> <modals-modal v-model="show" :width="300" height="100%">
<template #outer> <template #outer>
<div class="absolute top-5 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Bookmarks</p> <p class="text-white text-2xl truncate">Bookmarks</p>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="350" 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-8 left-4 z-40 pt-0.5" style="max-width: 80%">
<p class="text-white text-lg truncate">Current: {{ currentChapterTitle }}</p> <p class="text-white text-lg truncate">Current: {{ currentChapterTitle }}</p>
</div> </div>
</template> </template>
+2 -2
View File
@@ -1,8 +1,8 @@
<template> <template>
<modals-modal v-model="show" :width="300" height="100%"> <modals-modal v-model="show" :width="300" height="100%">
<template #outer> <template #outer>
<div v-if="title" class="absolute top-7 left-4 z-40" style="max-width: 80%"> <div v-if="title" class="absolute top-8 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">{{ title }}</p> <p class="text-white text-xl truncate">{{ title }}</p>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" width="90%" height="100%"> <modals-modal v-model="show" width="90%" height="100%">
<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-6 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>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="400" height="100%"> <modals-modal v-model="show" :width="400" height="100%">
<template #outer> <template #outer>
<div class="absolute top-5 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Details</p> <p class="text-white text-2xl truncate">Details</p>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="300" :processing="processing" height="100%"> <modals-modal v-model="show" :width="300" :processing="processing" height="100%">
<template #outer> <template #outer>
<div class="absolute top-4 left-4 z-40" style="max-width: 80%"> <div class="absolute top-8 left-4 z-40" style="max-width: 80%">
<p class="text-white text-2xl truncate">Libraries</p> <p class="text-white text-2xl truncate">Libraries</p>
</div> </div>
</template> </template>
+3 -3
View File
@@ -2,7 +2,7 @@
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0"> <div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" /> <div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
<div class="absolute z-40 top-4 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false"> <div class="absolute z-40 top-6 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
<span class="material-icons text-4xl">close</span> <span class="material-icons text-4xl">close</span>
</div> </div>
<slot name="outer" /> <slot name="outer" />
@@ -70,9 +70,9 @@ export default {
} }
}, },
methods: { methods: {
clickBg(vm, ev) { clickBg(ev) {
if (this.processing && this.persistent) return if (this.processing && this.persistent) return
if (vm.srcElement.classList.contains('modal-bg')) { if (ev && ev.srcElement && ev.srcElement.classList && ev.srcElement.classList.contains('modal-bg')) {
this.show = false this.show = false
} }
}, },
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" @input="modalInput" :width="200" height="100%"> <modals-modal v-model="show" @input="modalInput" :width="200" height="100%">
<template #outer> <template #outer>
<div class="absolute top-5 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Playback Speed</p> <p class="text-white text-2xl truncate">Playback Speed</p>
</div> </div>
</template> </template>
@@ -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%">
<template #outer> <template #outer>
<div class="absolute top-6 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Feed Episodes</p> <p class="text-white text-2xl truncate">Feed Episodes</p>
</div> </div>
</template> </template>
@@ -22,7 +22,7 @@
</div> </div>
</template> </template>
</div> </div>
<div class="absolute bottom-0 left-0 w-full flex items-center" style="height: 50px"> <div class="absolute bottom-6 left-0 w-full flex items-center" style="height: 50px">
<ui-btn class="w-full" :disabled="!episodesSelected.length" color="success" @click.stop="downloadEpisodes">{{ episodesSelected.length ? `Add ${episodesSelected.length} Episode(s) to Server` : 'No Episodes Selected' }}</ui-btn> <ui-btn class="w-full" :disabled="!episodesSelected.length" color="success" @click.stop="downloadEpisodes">{{ episodesSelected.length ? `Add ${episodesSelected.length} Episode(s) to Server` : 'No Episodes Selected' }}</ui-btn>
</div> </div>
</div> </div>
@@ -130,8 +130,8 @@ export default {
<style> <style>
.feed-content { .feed-content {
height: calc(100vh - 125px); height: calc(100vh - 150px);
max-height: calc(100vh - 125px); max-height: calc(100vh - 150px);
margin-top: 20px; margin-top: 5px;
} }
</style> </style>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="300" height="100%"> <modals-modal v-model="show" :width="300" height="100%">
<template #outer> <template #outer>
<div class="absolute top-7 left-4 z-40" style="max-width: 80%"> <div class="absolute top-8 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">Select Local Folder</p> <p class="text-white text-lg truncate">Select Local Folder</p>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="200" height="100%"> <modals-modal v-model="show" :width="200" height="100%">
<template #outer> <template #outer>
<div class="absolute top-5 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Sleep Timer</p> <p class="text-white text-2xl truncate">Sleep Timer</p>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="200" height="100%"> <modals-modal v-model="show" :width="200" height="100%">
<template #outer> <template #outer>
<div class="absolute top-5 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Sleep Timer</p> <p class="text-white text-2xl truncate">Sleep Timer</p>
</div> </div>
</template> </template>
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" :width="360" height="100%" :processing="processing"> <modals-modal v-model="show" :width="360" height="100%" :processing="processing">
<template #outer> <template #outer>
<div class="absolute top-5 left-4 z-40"> <div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Add to Playlist</p> <p class="text-white text-2xl truncate">Add to Playlist</p>
</div> </div>
</template> </template>
+37 -13
View File
@@ -1,11 +1,11 @@
<template> <template>
<div id="comic-reader" class="w-full h-full"> <div id="comic-reader" class="w-full h-full relative">
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute right-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52" style="top: 72px"> <div v-show="showPageMenu" v-click-outside="clickOutsideObj" class="pagemenu absolute top-12 right-16 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)"> <div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click.stop="setPage(index)">
<p class="text-sm truncate">{{ file }}</p> <p class="text-sm truncate">{{ file }}</p>
</div> </div>
</div> </div>
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-20 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" style="top: 72px"> <div v-show="showInfoMenu" v-click-outside="clickedOutsideInfoMenu" class="pagemenu absolute top-12 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" @click.stop.prevent>
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1"> <div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
<p class="text-xs"> <p class="text-xs">
<strong>{{ key }}</strong> <strong>{{ key }}</strong>
@@ -14,13 +14,13 @@
</div> </div>
</div> </div>
<div v-if="comicMetadata" class="absolute top-8 right-36 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu"> <div v-if="comicMetadata" class="absolute top-0 right-3 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
<span class="material-icons text-lg">more</span> <span class="material-icons text-lg">more</span>
</div> </div>
<div class="absolute top-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" style="right: 92px" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu"> <div class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @mouseup.prevent @click.stop.prevent="clickShowPageMenu">
<span class="material-icons text-lg">menu</span> <span class="material-icons text-lg">menu</span>
</div> </div>
<div class="absolute top-8 right-4 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <div class="absolute top-0 left-3 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20" @click.stop>
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p> <p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
</div> </div>
@@ -61,7 +61,12 @@ export default {
showInfoMenu: false, showInfoMenu: false,
loadTimeout: null, loadTimeout: null,
loadedFirstPage: false, loadedFirstPage: false,
comicMetadata: null comicMetadata: null,
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
}
} }
}, },
watch: { watch: {
@@ -84,9 +89,19 @@ export default {
} }
}, },
methods: { methods: {
clickOutside() { clickShowInfoMenu() {
if (this.showPageMenu) this.showPageMenu = false this.showInfoMenu = !this.showInfoMenu
if (this.showInfoMenu) this.showInfoMenu = false this.showPageMenu = false
},
clickShowPageMenu() {
this.showPageMenu = !this.showPageMenu
this.showInfoMenu = false
},
clickedOutside() {
this.showPageMenu = false
},
clickedOutsideInfoMenu() {
this.showInfoMenu = false
}, },
next() { next() {
if (!this.canGoNext) return if (!this.canGoNext) return
@@ -100,7 +115,8 @@ export default {
if (index < 0 || index > this.numPages - 1) { if (index < 0 || index > this.numPages - 1) {
return return
} }
var filename = this.pages[index] this.showPageMenu = false
const filename = this.pages[index]
this.page = index this.page = index
return this.extractFile(filename) return this.extractFile(filename)
}, },
@@ -235,8 +251,16 @@ export default {
<style scoped> <style scoped>
#comic-reader { #comic-reader {
height: calc(100% - 32px); height: calc(100% - 36px);
max-height: calc(100% - 36px);
padding-top: 36px;
} }
.reader-player-open #comic-reader {
height: calc(100% - 156px);
max-height: calc(100% - 156px);
padding-top: 36px;
}
.pagemenu { .pagemenu {
max-height: calc(100% - 80px); max-height: calc(100% - 80px);
} }
+6 -15
View File
@@ -39,7 +39,7 @@ export default {
return this.$store.state.playerLibraryItemId return this.$store.state.playerLibraryItemId
}, },
readerHeightOffset() { readerHeightOffset() {
return this.playerLibraryItemId ? 164 : 64 return this.playerLibraryItemId ? 196 : 96
} }
}, },
methods: { methods: {
@@ -58,13 +58,6 @@ export default {
this.rendition.next() this.rendition.next()
} }
}, },
keyUp() {
if ((e.keyCode || e.which) == 37) {
this.prev()
} else if ((e.keyCode || e.which) == 39) {
this.next()
}
},
initEpub() { initEpub() {
var book = ePub(this.url) var book = ePub(this.url)
this.book = book this.book = book
@@ -93,7 +86,7 @@ export default {
console.error('No Start', currentLocation) console.error('No Start', currentLocation)
} else { } else {
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi) var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
// console.log('current page', currentPage) console.log('current page', currentPage)
} }
}) })
}) })
@@ -105,10 +98,10 @@ export default {
}) })
this.chapters = _chapters this.chapters = _chapters
}) })
book.loaded.metadata.then((metadata) => { // book.loaded.metadata.then((metadata) => {
// this.author = metadata.creator // this.author = metadata.creator
// this.title = metadata.title // this.title = metadata.title
}) // })
// const spine_get = book.spine.get.bind(book.spine) // const spine_get = book.spine.get.bind(book.spine)
// book.spine.get = function (target) { // book.spine.get = function (target) {
@@ -121,8 +114,6 @@ export default {
// return t // return t
// } // }
this.rendition.on('keyup', this.keyUp)
this.rendition.on('relocated', (location) => { this.rendition.on('relocated', (location) => {
var percent = book.locations.percentageFromCfi(location.start.cfi) var percent = book.locations.percentageFromCfi(location.start.cfi)
this.progress = Math.floor(percent * 100) this.progress = Math.floor(percent * 100)
+10 -11
View File
@@ -1,9 +1,9 @@
<template> <template>
<div class="mobi-ebook-viewer w-full"> <div class="mobi-ebook-viewer w-full relative">
<div class="absolute overflow-hidden left-0 right-0 w-full max-w-full m-auto z-10 border border-black border-opacity-20 shadow-md bg-white"> <div class="absolute overflow-hidden left-0 right-0 w-full max-w-full m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
<iframe title="html-viewer" class="w-full overflow-hidden"> Loading </iframe> <iframe title="html-viewer" class="w-full overflow-hidden"> Loading </iframe>
</div> </div>
<div class="fixed bottom-0 left-0 h-8 w-full bg-bg px-2 flex items-center z-20" :style="{ bottom: playerLibraryItemId ? '100px' : '0px' }"> <div class="fixed bottom-0 left-0 h-8 w-full bg-bg px-2 flex items-center z-20" :style="{ bottom: playerLibraryItemId ? '120px' : '0px' }">
<p class="text-xs">mobi</p> <p class="text-xs">mobi</p>
<div class="flex-grow" /> <div class="flex-grow" />
</div> </div>
@@ -25,9 +25,6 @@ export default {
computed: { computed: {
playerLibraryItemId() { playerLibraryItemId() {
return this.$store.state.playerLibraryItemId return this.$store.state.playerLibraryItemId
},
readerHeightOffset() {
return this.playerLibraryItemId ? 164 : 64
} }
}, },
methods: { methods: {
@@ -122,13 +119,15 @@ export default {
<style> <style>
.mobi-ebook-viewer { .mobi-ebook-viewer {
height: calc(100% - 32px); height: calc(100% - 52px);
max-height: calc(100% - 32px); max-height: calc(100% - 52px);
overflow: hidden; overflow-x: hidden;
overflow-y: auto;
} }
.reader-player-open .mobi-ebook-viewer { .reader-player-open .mobi-ebook-viewer {
height: calc(100% - 132px); height: calc(100% - 152px);
max-height: calc(100% - 132px); max-height: calc(100% - 152px);
overflow: hidden; overflow-x: hidden;
overflow-y: auto;
} }
</style> </style>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="w-full h-full pt-20 relative"> <div class="w-full h-full pt-6 relative">
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2"> <div class="flex items-center justify-center h-full w-1/2">
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span> <span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
+7 -5
View File
@@ -1,6 +1,6 @@
<template> <template>
<div v-if="show" class="absolute top-0 left-0 w-full h-full bg-bg z-40 pt-8" :class="{ 'reader-player-open': !!playerLibraryItemId }"> <div v-if="show" class="absolute top-0 left-0 w-full h-full bg-bg z-40 pt-16" :class="{ 'reader-player-open': !!playerLibraryItemId }">
<div class="h-8 w-full bg-primary flex items-center px-2 fixed top-0 left-0 z-30 box-shadow-sm"> <div class="h-16 pt-8 w-full bg-primary flex items-center px-2 fixed top-0 left-0 z-30 box-shadow-sm">
<p class="w-5/6 truncate">{{ title }}</p> <p class="w-5/6 truncate">{{ title }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<span class="material-icons text-xl text-white" @click.stop="show = false">close</span> <span class="material-icons text-xl text-white" @click.stop="show = false">close</span>
@@ -132,7 +132,9 @@ export default {
const touchDistanceX = Math.abs(this.touchendX - this.touchstartX) const touchDistanceX = Math.abs(this.touchendX - this.touchstartX)
const touchDistanceY = Math.abs(this.touchendY - this.touchstartY) const touchDistanceY = Math.abs(this.touchendY - this.touchstartY)
if (touchDistanceX < 100 || touchDistanceY > touchDistanceX) return if (touchDistanceX < 60 || touchDistanceY > touchDistanceX) {
return
}
if (this.touchendX < this.touchstartX) { if (this.touchendX < this.touchstartX) {
console.log('swiped left') console.log('swiped left')
@@ -144,8 +146,8 @@ export default {
} }
}, },
touchstart(e) { touchstart(e) {
this.touchstartX = e.changedTouches[0].screenX this.touchstartX = e.touches[0].screenX
this.touchstartY = e.changedTouches[0].screenY this.touchstartY = e.touches[0].screenY
this.touchstartTime = Date.now() this.touchstartTime = Date.now()
}, },
touchend(e) { touchend(e) {
+3 -2
View File
@@ -348,8 +348,9 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL { private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL {
// filename needs to be encoded otherwise would just use contentUrl // filename needs to be encoded otherwise would just use contentUrl
let filenameEncoded = track.metadata?.filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) let relPath = track.metadata?.relPath ?? ""
let urlstr = "\(Store.serverConfig!.address)/s/item/\(item.id)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)" let filepathEncoded = relPath.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
let urlstr = "\(Store.serverConfig!.address)/s/item/\(item.id)/\(filepathEncoded ?? "")?token=\(Store.serverConfig!.token)"
return URL(string: urlstr)! return URL(string: urlstr)!
} }
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="select-none w-full layout-wrapper bg-bg text-white"> <div class="w-full layout-wrapper bg-bg text-white">
<app-appbar /> <app-appbar />
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''"> <div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
<Nuxt /> <Nuxt />
@@ -101,7 +101,7 @@ export default {
const deviceData = await this.$db.getDeviceData() const deviceData = await this.$db.getDeviceData()
let serverConfig = null let serverConfig = null
if (deviceData) { if (deviceData) {
this.$store.commit('globals/setHapticFeedback', deviceData.hapticFeedback) this.$store.commit('globals/setHapticFeedback', deviceData.deviceSettings?.hapticFeedback)
if (deviceData.lastServerConnectionConfigId && deviceData.serverConnectionConfigs.length) { if (deviceData.lastServerConnectionConfigId && deviceData.serverConnectionConfigs.length) {
serverConfig = deviceData.serverConnectionConfigs.find((scc) => scc.id == deviceData.lastServerConnectionConfigId) serverConfig = deviceData.serverConnectionConfigs.find((scc) => scc.id == deviceData.lastServerConnectionConfigId)
+15 -1
View File
@@ -26,6 +26,7 @@
"libarchive.js": "^1.3.0", "libarchive.js": "^1.3.0",
"nuxt": "^2.15.7", "nuxt": "^2.15.7",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"v-click-outside": "^3.2.0",
"vue-pdf": "^4.2.0", "vue-pdf": "^4.2.0",
"vue-toastification": "^1.7.11", "vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
@@ -17192,6 +17193,14 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/v-click-outside": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-3.2.0.tgz",
"integrity": "sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w==",
"engines": {
"node": ">=6"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -31992,6 +32001,11 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
}, },
"v-click-outside": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-3.2.0.tgz",
"integrity": "sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w=="
},
"vary": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -33339,4 +33353,4 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
} }
} }
} }
+2 -1
View File
@@ -30,6 +30,7 @@
"libarchive.js": "^1.3.0", "libarchive.js": "^1.3.0",
"nuxt": "^2.15.7", "nuxt": "^2.15.7",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"v-click-outside": "^3.2.0",
"vue-pdf": "^4.2.0", "vue-pdf": "^4.2.0",
"vue-toastification": "^1.7.11", "vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
@@ -41,4 +42,4 @@
"@nuxtjs/tailwindcss": "^4.2.0", "@nuxtjs/tailwindcss": "^4.2.0",
"postcss": "^8.3.5" "postcss": "^8.3.5"
} }
} }
+74 -5
View File
@@ -1,14 +1,83 @@
<template> <template>
<div>Authors</div> <div>
<div id="bookshelf" class="w-full h-full p-4 overflow-y-auto">
<div class="flex flex-wrap justify-center">
<template v-for="author in authors">
<cards-author-card :key="author.id" :author="author" :width="96" :height="120" class="p-2" />
</template>
</div>
</div>
</div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return {} return {
loading: true,
authors: [],
loadedLibraryId: null
}
}, },
watch: {}, computed: {
computed: {}, currentLibraryId() {
methods: {} return this.$store.state.libraries.currentLibraryId
}
},
methods: {
async init() {
this.loadedLibraryId = this.currentLibraryId
this.authors = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
.then((response) => response.authors)
.catch((error) => {
console.error('Failed to load authors', error)
return []
})
console.log('Loaded authors', this.authors)
this.$eventBus.$emit('bookshelf-total-entities', this.authors.length)
this.loading = false
},
authorAdded(author) {
if (!this.authors.some((au) => au.id === author.id)) {
this.authors.push(author)
this.$eventBus.$emit('bookshelf-total-entities', this.authors.length)
}
},
authorUpdated(author) {
this.authors = this.authors.map((au) => {
if (au.id === author.id) {
return author
}
return au
})
},
authorRemoved(author) {
this.authors = this.authors.filter((au) => au.id !== author.id)
this.$eventBus.$emit('bookshelf-total-entities', this.authors.length)
},
libraryChanged(libraryId) {
if (libraryId !== this.loadedLibraryId) {
if (this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'book') {
this.init()
} else {
this.$router.replace('/bookshelf')
}
}
}
},
mounted() {
this.init()
this.$socket.$on('author_added', this.authorAdded)
this.$socket.$on('author_updated', this.authorUpdated)
this.$socket.$on('author_removed', this.authorRemoved)
this.$eventBus.$on('library-changed', this.libraryChanged)
},
beforeDestroy() {
this.$socket.$off('author_added', this.authorAdded)
this.$socket.$off('author_updated', this.authorUpdated)
this.$socket.$off('author_removed', this.authorRemoved)
this.$eventBus.$off('library-changed', this.libraryChanged)
}
} }
</script> </script>
+20 -1
View File
@@ -37,6 +37,7 @@ export default {
data() { data() {
return { return {
shelves: [], shelves: [],
isSettingsLoaded: false,
isFirstNetworkConnection: true, isFirstNetworkConnection: true,
lastServerFetch: 0, lastServerFetch: 0,
lastServerFetchLibraryId: null, lastServerFetchLibraryId: null,
@@ -276,12 +277,25 @@ export default {
this.isLoading = false this.isLoading = false
}, },
openMediaPlayerWithMostRecentListening() { async waitForSettings() {
// Wait up to 3 seconds
for (let i = 0; i < 6; i++) {
if (this.isSettingsLoaded) return true
await new Promise((resolve) => setTimeout(resolve, 500))
}
return false
},
async openMediaPlayerWithMostRecentListening() {
// If we don't already have a player open // If we don't already have a player open
// Try opening the first book from continue-listening without playing it // Try opening the first book from continue-listening without playing it
if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
// Wait for settings to load to prevent race condition when setting playback speed.
if (!this.isSettingsLoaded) {
await this.waitForSettings()
}
const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening') const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening')
const mostRecentEntity = continueListeningShelf?.entities?.[0] const mostRecentEntity = continueListeningShelf?.entities?.[0]
if (mostRecentEntity) { if (mostRecentEntity) {
@@ -356,11 +370,16 @@ export default {
} }
}) })
}, },
settingsUpdated() {
this.isSettingsLoaded = true
},
initListeners() { initListeners() {
this.$eventBus.$on('library-changed', this.libraryChanged) this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('user-settings', this.settingsUpdated)
}, },
removeListeners() { removeListeners() {
this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('user-settings', this.settingsUpdated)
} }
}, },
mounted() { mounted() {
+16 -1
View File
@@ -17,7 +17,8 @@ export default {
totalEpisodes: 0, totalEpisodes: 0,
currentPage: 0, currentPage: 0,
localEpisodeMap: {}, localEpisodeMap: {},
isLocal: false isLocal: false,
loadedLibraryId: null
} }
}, },
watch: {}, watch: {},
@@ -39,6 +40,7 @@ export default {
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true) this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
}, },
async loadRecentEpisodes(page = 0) { async loadRecentEpisodes(page = 0) {
this.loadedLibraryId = this.currentLibraryId
this.processing = true this.processing = true
const episodePayload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => { const episodePayload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => {
console.error('Failed to get recent episodes', error) console.error('Failed to get recent episodes', error)
@@ -50,10 +52,23 @@ export default {
this.recentEpisodes = episodePayload.episodes || [] this.recentEpisodes = episodePayload.episodes || []
this.totalEpisodes = episodePayload.total this.totalEpisodes = episodePayload.total
this.currentPage = page this.currentPage = page
},
libraryChanged(libraryId) {
if (libraryId !== this.loadedLibraryId) {
if (this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast') {
this.loadRecentEpisodes()
} else {
this.$router.replace('/bookshelf')
}
}
} }
}, },
mounted() { mounted() {
this.loadRecentEpisodes() this.loadRecentEpisodes()
this.$eventBus.$on('library-changed', this.libraryChanged)
},
beforeDestroy() {
this.$eventBus.$off('library-changed', this.libraryChanged)
} }
} }
</script> </script>
-1
View File
@@ -7,7 +7,6 @@ export default {
async asyncData({ store, params, query }) { async asyncData({ store, params, query }) {
// Set filter by // Set filter by
if (query.filter) { if (query.filter) {
store.commit('user/setSettings', { mobileFilterBy: query.filter })
await store.dispatch('user/updateUserSettings', { mobileFilterBy: query.filter }) await store.dispatch('user/updateUserSettings', { mobileFilterBy: query.filter })
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="w-full h-full px-3 py-4 overflow-y-auto overflow-x-hidden relative bg-bg"> <div class="w-full h-full px-3 pb-4 overflow-y-auto overflow-x-hidden relative bg-bg">
<div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10"> <div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10">
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" /> <div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
<div class="w-full h-full absolute top-0 left-0" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%)" /> <div class="w-full h-full absolute top-0 left-0" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%)" />
+6 -2
View File
@@ -189,7 +189,11 @@ export default {
<style scoped> <style scoped>
.media-item-container { .media-item-container {
height: calc(100vh - 200px); height: calc(100vh - 210px);
max-height: calc(100vh - 200px); max-height: calc(100vh - 210px);
}
.playerOpen .media-item-container {
height: calc(100vh - 310px);
max-height: calc(100vh - 310px);
} }
</style> </style>
+1 -1
View File
@@ -390,7 +390,7 @@ export default {
} }
</script> </script>
<style> <style scoped>
.media-item-container { .media-item-container {
height: calc(100vh - 200px); height: calc(100vh - 200px);
max-height: calc(100vh - 200px); max-height: calc(100vh - 200px);
+1 -1
View File
@@ -53,7 +53,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true }) var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true })
if (playbackSession) { if (playbackSession) {
if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime
this.setAudioPlayer(playbackSession, true) this.setAudioPlayer(playbackSession, playWhenReady)
} }
} }
return false return false
+4 -38
View File
@@ -1,9 +1,12 @@
import Vue from 'vue' import Vue from 'vue'
import vClickOutside from 'v-click-outside'
import { App } from '@capacitor/app' import { App } from '@capacitor/app'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { StatusBar, Style } from '@capacitor/status-bar'; import { StatusBar, Style } from '@capacitor/status-bar';
import { formatDistance, format, addDays, isDate } from 'date-fns' import { formatDistance, format, addDays, isDate } from 'date-fns'
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core'
Vue.directive('click-outside', vClickOutside.directive)
if (Capacitor.getPlatform() != 'web') { if (Capacitor.getPlatform() != 'web') {
const setStatusBarStyleDark = async () => { const setStatusBarStyleDark = async () => {
@@ -150,43 +153,6 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
return sanitized return sanitized
} }
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
const isDOMElement = (element) => {
return element instanceof Element || element instanceof HTMLDocument
}
const clickedEl = clickEvent.srcElement
const didClickOnIgnoredEl = ignoreElems.filter((el) => el).some((element) => element.contains(clickedEl) || element.isEqualNode(clickedEl))
const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map((selector) => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false
if (isDOMElement(elToCheckOutside) && !elToCheckOutside.contains(clickedEl) && !didClickOnIgnoredEl && !didClickOnIgnoredSelector) {
return true
}
return false
}
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
let vm = vnode.context;
let callback = binding.value;
if (typeof callback !== 'function') {
console.error('Invalid callback', binding)
return
}
el['__click_outside__'] = (ev) => {
if (isClickedOutsideEl(ev, el)) {
callback.call(vm, ev)
}
}
document.addEventListener('click', el['__click_outside__'], false)
},
unbind: function (el, binding, vnode) {
document.removeEventListener('click', el['__click_outside__'], false)
delete el['__click_outside__']
}
})
function xmlToJson(xml) { function xmlToJson(xml) {
const json = {}; const json = {};
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) { for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
+4 -2
View File
@@ -5,7 +5,9 @@ export const state = () => ({
mobileOrderBy: 'addedAt', mobileOrderBy: 'addedAt',
mobileOrderDesc: true, mobileOrderDesc: true,
mobileFilterBy: 'all', mobileFilterBy: 'all',
playbackRate: 1 playbackRate: 1,
collapseSeries: false,
collapseBookSeries: false
} }
}) })
@@ -89,8 +91,8 @@ export const actions = {
} }
} }
if (hasChanges) { if (hasChanges) {
await this.$localStore.setUserSettings(existingSettings)
commit('setSettings', existingSettings) commit('setSettings', existingSettings)
await this.$localStore.setUserSettings(existingSettings)
this.$eventBus.$emit('user-settings', state.settings) this.$eventBus.$emit('user-settings', state.settings)
} }
}, },