Merge branch 'master' into center-title

This commit is contained in:
advplyr 2023-03-06 13:48:30 -06:00
commit fb4e7e6b55
60 changed files with 804 additions and 459 deletions

View file

@ -23,18 +23,18 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" >
<!-- TODO: Incomplete desktop widget -->
<!-- <receiver-->
<!-- android:name=".NewAppWidget"-->
<!-- android:exported="true" >-->
<!-- <intent-filter>-->
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
<!-- </intent-filter>-->
<!-- Android app widget -->
<receiver
android:name="MediaPlayerWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<!-- <meta-data-->
<!-- android:name="android.appwidget.provider"-->
<!-- android:resource="@xml/new_app_widget_info" />-->
<!-- </receiver>-->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/media_player_widget_info" />
</receiver>
<!-- Used by Android Auto -->
<meta-data

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
@ -41,7 +42,7 @@ class LibraryItem(
@JsonIgnore
fun getCoverUri(): Uri {
if (media.coverPath == null) {
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")

View file

@ -10,6 +10,7 @@ import android.provider.MediaStore
import android.support.v4.media.MediaDescriptionCompat
import android.util.Log
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
@ -46,7 +47,7 @@ class LocalLibraryItem(
@JsonIgnore
fun getCoverUri(): Uri {
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
}
@JsonIgnore

View file

@ -6,6 +6,8 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnore
@ -136,9 +138,9 @@ class PlaybackSession(
@JsonIgnore
fun getCoverUri(): Uri {
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
if (coverPath == null) return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
}

View file

@ -8,6 +8,7 @@ import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.session.MediaControllerCompat
import android.util.Log
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
import com.google.android.exoplayer2.Player
@ -83,7 +84,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
Glide.with(playerNotificationService)
.asBitmap()
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
}

View file

@ -190,6 +190,14 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
handleMediaButtonClickCount()
}
}
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
Log.d(tag, "handleCallMediaButton: Media Fast Forward")
playerNotificationService.jumpForward()
}
KeyEvent.KEYCODE_MEDIA_REWIND -> {
Log.d(tag, "handleCallMediaButton: Media Rewind")
playerNotificationService.jumpBackward()
}
}
}
@ -224,12 +232,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playerNotificationService.jumpBackward()
}
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
playerNotificationService.jumpForward()
}
KeyEvent.KEYCODE_MEDIA_REWIND -> {
playerNotificationService.jumpBackward()
}
KeyEvent.KEYCODE_MEDIA_STOP -> {
playerNotificationService.closePlayback()
}

View file

@ -1,6 +1,8 @@
package com.audiobookshelf.app.player
import android.app.*
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
@ -21,12 +23,15 @@ import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.MediaPlayerWidget
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceInfo
@ -170,12 +175,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
Log.d(tag, "onDestroy")
isStarted = false
isClosed = true
DeviceManager.widgetUpdater?.onPlayerChanged(this)
playerNotificationManager.setPlayer(null)
mPlayer.release()
castPlayer?.release()
mediaSession.release()
mediaProgressSyncer.reset()
isStarted = false
super.onDestroy()
}
@ -184,6 +193,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
Log.d(tag, "onTaskRemoved")
stopSelf()
}
@ -258,7 +268,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
playerNotificationManager.setUseFastForwardActionInCompactView(true)
playerNotificationManager.setUseRewindActionInCompactView(true)
playerNotificationManager.setSmallIcon(R.drawable.exo_icon_localaudio)
playerNotificationManager.setSmallIcon(R.drawable.icon_monochrome)
// Unknown action
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
@ -409,9 +419,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
// Notify client
clientEventEmitter?.onPlaybackSession(playbackSession)
// Update widget
DeviceManager.widgetUpdater?.onPlayerChanged(this)
if (mediaItems.isEmpty()) {
Log.e(tag, "Invalid playback session no media items to play")
currentPlaybackSession = null

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
<resources>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" />
</declare-styleable>
</resources>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" />
<attr name="backgroundRadius" format="dimension" />
<attr name="appWidgetViewPadding" format="dimension" />
</declare-styleable>
</resources>

View file

@ -1,33 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:statusBarColor">@color/background_dark</item>
<item name="android:navigationBarColor">@color/background_dark</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:statusBarColor">@color/background_dark</item>
<item name="android:navigationBarColor">@color/background_dark</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<!-- <item name="android:background">@drawable/screen</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="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<!-- <item name="android:background">@drawable/screen</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>
</style>
<style name="MediaPlayerWidgetTheme" parent="Theme.AppCompat.DayNight">
<!-- These colors match the vue pages -->
<item name="android:colorBackground">#232323</item>
<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>

View file

@ -1,17 +1,17 @@
<resources>
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<!-- Radius of the outer bound of widgets to make the rounded corners -->
<item name="appWidgetRadius">16dp</item>
<!--
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<!-- 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.
It needs to be 8dp or less than the value of appWidgetRadius
-->
<item name="appWidgetInnerRadius">8dp</item>
</style>
<item name="appWidgetInnerRadius">0dp</item>
</style>
<style name="AppTheme.AppWidgetContainer"
parent="AppTheme.AppWidgetContainerParent">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">16dp</item>
</style>
<style name="AppTheme.AppWidgetContainer" parent="AppTheme.AppWidgetContainerParent">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">0dp</item>
</style>
</resources>

View file

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

View file

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

View file

@ -2,6 +2,19 @@
@import './defaultStyles.css';
@import './absicons.css';
* {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
input,
textarea {
-webkit-touch-callout: auto;
-webkit-user-select: auto;
user-select: auto;
}
body {
background-color: #262626;
}

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>
</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">
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
<div class="flex-grow" />
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
</div>
<div class="w-full">
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative">
<div ref="totalReadyTrack" class="h-full bg-gray-600 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" />
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
<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 rounded-full" />
<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 rounded-full" />
</div>
</div>
</div>
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
<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 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 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">
<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 -->
@ -65,8 +65,8 @@
</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 id="playerControls" class="absolute right-0 bottom-0 py-2">
<div class="flex items-center justify-center">
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<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="!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">
@ -80,20 +80,17 @@
</div>
</div>
<div id="playerTrack" class="absolute left-0 w-full px-3">
<div class="flex pb-0.5">
<div id="playerTrack" class="absolute left-0 w-full px-6">
<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>
<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>
</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="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
<div ref="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 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 rounded-full 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 rounded-full 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>
</div>
</div>
@ -151,8 +148,10 @@ export default {
useTotalTrack: true,
lockUi: false,
isLoading: false,
touchTrackStart: false,
dragPercent: 0,
isDraggingCursor: false,
draggingTouchStartX: 0,
draggingTouchStartTime: 0,
draggingCurrentTime: 0,
syncStatus: 0,
showMoreMenuDialog: false,
coverRgb: 'rgb(55, 56, 56)',
@ -287,6 +286,7 @@ export default {
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
},
title() {
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
if (this.playbackSession) return this.playbackSession.displayTitle
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
},
@ -318,17 +318,20 @@ export default {
return this.$secondsToTimestamp(this.totalDuration)
},
currentTimePretty() {
return this.$secondsToTimestamp(this.currentTime / this.currentPlaybackRate)
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
},
timeRemaining() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start
var currChapTime = currentTimeToUse - this.currentChapter.start
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
}
return this.totalTimeRemaining
},
totalTimeRemaining() {
return (this.totalDuration - this.currentTime) / this.currentPlaybackRate
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return (this.totalDuration - currentTimeToUse) / this.currentPlaybackRate
},
totalTimeRemainingPretty() {
if (this.totalTimeRemaining < 0) {
@ -342,10 +345,6 @@ export default {
}
return '-' + this.$secondsToTimestamp(this.timeRemaining)
},
timeLeftInChapter() {
if (!this.currentChapter) return 0
return this.currentChapter.end - this.currentTime
},
sleepTimeRemainingPretty() {
if (!this.sleepTimeRemaining) return '0s'
var secondsRemaining = Math.round(this.sleepTimeRemaining)
@ -392,11 +391,6 @@ export default {
this.showFullscreen = false
}
},
async touchstartTrack(e) {
await this.$hapticsImpact()
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
this.touchTrackStart = true
},
async selectChapter(chapter) {
await this.$hapticsImpact()
this.seek(chapter.start)
@ -450,7 +444,6 @@ export default {
this.$emit('showSleepTimer')
},
async setPlaybackSpeed(speed) {
await this.$hapticsImpact()
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
this.currentPlaybackRate = speed
this.updateTimestamp()
@ -509,12 +502,13 @@ export default {
console.error('No timestamp el')
return
}
let currentTime = this.currentTime / this.currentPlaybackRate
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) {
const currChapTime = Math.max(0, this.currentTime - this.currentChapter.start)
currentTime = currChapTime / this.currentPlaybackRate
currentTime = Math.max(0, currentTime - this.currentChapter.start)
}
ts.innerText = this.$secondsToTimestamp(currentTime)
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
},
timeupdate() {
if (!this.$refs.playedTrack) {
@ -536,22 +530,24 @@ export default {
},
updateTrack() {
// Update progress track UI
let percentDone = this.currentTime / this.totalDuration
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
let percentDone = currentTimeToUse / this.totalDuration
const totalPercentDone = percentDone
let bufferedPercent = this.bufferedTime / this.totalDuration
const totalBufferedPercent = bufferedPercent
if (this.useChapterTrack && this.currentChapter) {
const currChapTime = this.currentTime - this.currentChapter.start
const currChapTime = currentTimeToUse - this.currentChapter.start
percentDone = currChapTime / this.currentChapterDuration
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
}
const ptWidth = Math.round(percentDone * this.trackWidth)
this.$refs.playedTrack.style.width = ptWidth + 'px'
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
if (this.$refs.trackCursor) {
this.$refs.trackCursor.style.left = ptWidth - 8 + 'px'
this.$refs.trackCursor.style.left = ptWidth - 7 + 'px'
}
if (this.useChapterTrack) {
@ -580,27 +576,15 @@ export default {
this.$refs.playedTrack.classList.add('bg-yellow-300')
}
},
clickTrack(e) {
if (this.isLoading || this.lockUi) return
if (!this.showFullscreen) {
// Track not clickable on mini-player
return
}
if (e) e.stopPropagation()
async touchstartCursor(e) {
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
var offsetX = e.offsetX
var perc = offsetX / this.trackWidth
var time = 0
if (this.useChapterTrack && this.currentChapter) {
time = perc * this.currentChapterDuration + this.currentChapter.start
} else {
time = perc * this.totalDuration
}
if (isNaN(time) || time === null) {
console.error('Invalid time', perc, time)
return
}
this.seek(time)
await this.$hapticsImpact()
this.isDraggingCursor = true
this.draggingTouchStartX = e.touches[0].pageX
this.draggingTouchStartTime = this.currentTime
this.draggingCurrentTime = this.currentTime
this.updateTrack()
},
async playPauseClick() {
await this.$hapticsImpact()
@ -653,24 +637,11 @@ export default {
touchend(e) {
if (!e.changedTouches) return
if (this.touchTrackStart) {
var touch = e.changedTouches[0]
const touchOnTrackPos = touch.pageX - 12
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
var seekToTime = 0
if (this.useChapterTrack && this.currentChapter) {
const currChapTime = dragPercent * this.currentChapterDuration
seekToTime = this.currentChapter.start + currChapTime
} else {
seekToTime = dragPercent * this.totalDuration
if (this.isDraggingCursor) {
if (this.draggingCurrentTime !== this.currentTime) {
this.seek(this.draggingCurrentTime)
}
this.seek(seekToTime)
if (this.$refs.draggingTrack) {
this.$refs.draggingTrack.style.width = '0px'
}
this.touchTrackStart = false
this.isDraggingCursor = false
} else if (this.showFullscreen) {
this.touchEndY = e.changedTouches[0].screenY
var touchDuration = Date.now() - this.touchStartTime
@ -682,29 +653,24 @@ export default {
}
},
touchmove(e) {
if (!this.touchTrackStart) return
if (!this.isDraggingCursor || !e.touches) return
var touch = e.touches[0]
const touchOnTrackPos = touch.pageX - 12
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
this.dragPercent = dragPercent
if (this.$refs.draggingTrack) {
this.$refs.draggingTrack.style.width = this.dragPercent * this.trackWidth + 'px'
const distanceMoved = e.touches[0].pageX - this.draggingTouchStartX
let duration = this.totalDuration
let minTime = 0
let maxTime = duration
if (this.useChapterTrack && this.currentChapter) {
duration = this.currentChapterDuration
minTime = this.currentChapter.start
maxTime = minTime + duration
}
var ts = this.$refs.currentTimestamp
if (ts) {
var currTimeStr = ''
if (this.useChapterTrack && this.currentChapter) {
const currChapTime = dragPercent * this.currentChapterDuration
currTimeStr = this.$secondsToTimestamp(currChapTime)
} else {
const dragTime = dragPercent * this.totalDuration
currTimeStr = this.$secondsToTimestamp(dragTime)
}
ts.innerText = currTimeStr
}
const timePerPixel = duration / this.trackWidth
const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
this.updateTimestamp()
this.updateTrack()
},
async clickMenuAction(action) {
await this.$hapticsImpact()
@ -850,7 +816,7 @@ export default {
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 24 + coverImageWidthCollapsed + 'px')
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px')
},
minimizePlayerEvt() {
this.collapseFullscreen()
@ -917,7 +883,7 @@ export default {
--cover-image-height: 0px;
--cover-image-width-collapsed: 46px;
--cover-image-height-collapsed: 46px;
--title-author-left-offset-collapsed: 70px;
--title-author-left-offset-collapsed: 80px;
}
.playerContainer {
@ -944,12 +910,14 @@ export default {
.cover-wrapper {
bottom: 68px;
left: 12px;
left: 24px;
height: var(--cover-image-height-collapsed);
width: var(--cover-image-width-collapsed);
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: left, bottom, width, height;
transform-origin: left bottom;
border-radius: 3px;
overflow: hidden;
}
.total-track {
@ -990,19 +958,17 @@ export default {
pointer-events: auto;
}
.fullscreen .title-author-texts .title-text {
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.5rem);
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.3rem);
}
.fullscreen .title-author-texts .author-text {
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1.1rem);
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1rem);
}
#playerControls {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: width, bottom;
height: 48px;
width: 140px;
padding-left: 12px;
padding-right: 12px;
width: 128px;
padding-right: 24px;
bottom: 70px;
}
#playerControls .jump-icon {
@ -1020,7 +986,7 @@ export default {
width: 40px;
min-width: 40px;
min-height: 40px;
margin: 0px 14px;
margin: 0px 7px;
}
#playerControls .play-btn .material-icons {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
@ -1035,18 +1001,21 @@ export default {
width: var(--cover-image-width);
left: calc(50% - (calc(var(--cover-image-width)) / 2));
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
border-radius: 16px;
overflow: hidden;
}
.fullscreen #playerControls {
width: 100%;
bottom: 94px;
padding-left: 24px;
padding-right: 24px;
bottom: 78px;
left: 0;
}
.fullscreen #playerControls .jump-icon {
margin: 0px 18px;
font-size: 2.4rem;
}
.fullscreen #playerControls .next-icon {
margin: 0px 20px;
font-size: 2rem;
}
.fullscreen #playerControls .play-btn {
@ -1054,7 +1023,6 @@ export default {
width: 65px;
min-width: 65px;
min-height: 65px;
margin: 0px 26px;
}
.fullscreen #playerControls .play-btn .material-icons {
font-size: 2.1rem;

View file

@ -50,6 +50,9 @@ export default {
watch: {
showBookshelfListView(newVal) {
this.resetEntities()
},
seriesId() {
this.resetEntities()
}
},
computed: {
@ -85,6 +88,12 @@ export default {
filterBy() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
},
collapseSeries() {
return this.$store.getters['user/getUserSetting']('collapseSeries')
},
collapseBookSeries() {
return this.$store.getters['user/getUserSetting']('collapseBookSeries')
},
isCoverSquareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
@ -356,6 +365,9 @@ export default {
let searchParams = new URLSearchParams()
if (this.page === 'series-books') {
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
if (this.collapseBookSeries) {
searchParams.set('collapseseries', 1)
}
} else {
if (this.filterBy && this.filterBy !== 'all') {
searchParams.set('filter', this.filterBy)

View file

@ -10,11 +10,16 @@
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</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>
</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 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
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
},
displayTitle() {
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
return this.title.substr(4) + ', The'
}
return this.title
seriesSequenceList() {
return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
},
displayAuthor() {
libraryItemIdsInSeries() {
// Only added to item object when collapseSeries is enabled
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
},
displayTitle() {
if (this.recentEpisode) return this.recentEpisode.title
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title
},
displayLineTwo() {
if (this.recentEpisode) return this.title
if (this.collapsedSeries) return ''
if (this.isPodcast) return this.author
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
return this.author
},
displaySortLine() {
if (this.collapsedSeries) return null
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
return null
},
episodeProgress() {
@ -434,7 +453,7 @@ export default {
const router = this.$router || this.$nuxt.$router
if (router) {
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`)
else if (this.collapsedSeries) router.push(`/library/${this.libraryId}/series/${this.collapsedSeries.id}`)
else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`)
else router.push(`/item/${this.libraryItemId}`)
}
}

View file

@ -101,15 +101,15 @@ export default {
icon: 'collections_bookmark',
iconClass: 'text-xl',
text: 'Collections'
},
{
to: '/bookshelf/authors',
routeName: 'bookshelf-authors',
iconPack: 'abs-icons',
icon: 'authors',
iconClass: 'text-2xl',
text: 'Authors'
}
// {
// to: '/bookshelf/authors',
// routeName: 'bookshelf-authors',
// iconPack: 'abs-icons',
// icon: 'authors',
// iconClass: 'text-2xl pb-px',
// text: 'Authors'
// }
]
}

View file

@ -2,11 +2,8 @@
<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 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="ml-2pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<p v-show="selectedSeriesName" class="ml-2 pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<div class="flex-grow" />
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
<template v-if="page === 'library'">
@ -16,11 +13,13 @@
</div>
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
</template>
<span v-if="(page == 'library' && isBookLibrary) || seriesBookPage" class="material-icons px-2" @click="showMoreMenuDialog = true">more_vert</span>
</div>
</div>
<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-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
</div>
</template>
@ -31,7 +30,8 @@ export default {
showSortModal: false,
showFilterModal: false,
settings: {},
totalEntities: 0
totalEntities: 0,
showMoreMenuDialog: false
}
},
computed: {
@ -44,6 +44,12 @@ export default {
this.$store.commit('globals/setBookshelfListView', val)
}
},
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
},
hasFilters() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
},
@ -66,6 +72,8 @@ export default {
return 'Collections'
} else if (this.page === 'playlists') {
return 'Playlists'
} else if (this.page === 'authors') {
return 'Authors'
}
return ''
},
@ -77,9 +85,40 @@ export default {
},
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
},
menuItems() {
if (!this.isBookLibrary) return []
if (this.seriesBookPage) {
return [
{
text: 'Collapse Sub-Series',
value: 'collapse_subseries',
icon: this.settings.collapseBookSeries ? 'check_box' : 'check_box_outline_blank'
}
]
} else {
return [
{
text: 'Collapse Series',
value: 'collapse_series',
icon: this.settings.collapseSeries ? 'check_box' : 'check_box_outline_blank'
}
]
}
}
},
methods: {
clickMenuAction(action) {
this.showMoreMenuDialog = false
if (action === 'collapse_series') {
this.settings.collapseSeries = !this.settings.collapseSeries
this.saveSettings()
} else if (action === 'collapse_subseries') {
this.settings.collapseBookSeries = !this.settings.collapseBookSeries
this.saveSettings()
}
},
updateOrder() {
this.saveSettings()
},

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="300" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="350" height="100%">
<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>
</div>
</template>

View file

@ -1,8 +1,8 @@
<template>
<modals-modal v-model="show" :width="300" height="100%">
<template #outer>
<div v-if="title" class="absolute top-7 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">{{ title }}</p>
<div v-if="title" class="absolute top-8 left-4 z-40" style="max-width: 80%">
<p class="text-white text-xl truncate">{{ title }}</p>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" width="90%" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="400" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="300" :processing="processing" height="100%">
<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>
</div>
</template>

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 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>
</div>
<slot name="outer" />
@ -70,9 +70,9 @@ export default {
}
},
methods: {
clickBg(vm, ev) {
clickBg(ev) {
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
}
},

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" @input="modalInput" :width="200" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" width="100%" height="100%" max-width="100%">
<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>
</div>
</template>
@ -22,7 +22,7 @@
</div>
</template>
</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>
</div>
</div>
@ -130,8 +130,8 @@ export default {
<style>
.feed-content {
height: calc(100vh - 125px);
max-height: calc(100vh - 125px);
margin-top: 20px;
height: calc(100vh - 150px);
max-height: calc(100vh - 150px);
margin-top: 5px;
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="300" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="200" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="200" height="100%">
<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>
</div>
</template>

View file

@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="360" height="100%" :processing="processing">
<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>
</div>
</template>

View file

@ -1,11 +1,11 @@
<template>
<div id="comic-reader" class="w-full h-full">
<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-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 id="comic-reader" class="w-full h-full relative">
<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.stop="setPage(index)">
<p class="text-sm truncate">{{ file }}</p>
</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">
<p class="text-xs">
<strong>{{ key }}</strong>
@ -14,13 +14,13 @@
</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>
</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>
</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>
</div>
@ -61,7 +61,12 @@ export default {
showInfoMenu: false,
loadTimeout: null,
loadedFirstPage: false,
comicMetadata: null
comicMetadata: null,
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
}
}
},
watch: {
@ -84,9 +89,19 @@ export default {
}
},
methods: {
clickOutside() {
if (this.showPageMenu) this.showPageMenu = false
if (this.showInfoMenu) this.showInfoMenu = false
clickShowInfoMenu() {
this.showInfoMenu = !this.showInfoMenu
this.showPageMenu = false
},
clickShowPageMenu() {
this.showPageMenu = !this.showPageMenu
this.showInfoMenu = false
},
clickedOutside() {
this.showPageMenu = false
},
clickedOutsideInfoMenu() {
this.showInfoMenu = false
},
next() {
if (!this.canGoNext) return
@ -100,7 +115,8 @@ export default {
if (index < 0 || index > this.numPages - 1) {
return
}
var filename = this.pages[index]
this.showPageMenu = false
const filename = this.pages[index]
this.page = index
return this.extractFile(filename)
},
@ -235,8 +251,16 @@ export default {
<style scoped>
#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 {
max-height: calc(100% - 80px);
}

View file

@ -39,7 +39,7 @@ export default {
return this.$store.state.playerLibraryItemId
},
readerHeightOffset() {
return this.playerLibraryItemId ? 164 : 64
return this.playerLibraryItemId ? 196 : 96
}
},
methods: {
@ -58,13 +58,6 @@ export default {
this.rendition.next()
}
},
keyUp() {
if ((e.keyCode || e.which) == 37) {
this.prev()
} else if ((e.keyCode || e.which) == 39) {
this.next()
}
},
initEpub() {
var book = ePub(this.url)
this.book = book
@ -93,7 +86,7 @@ export default {
console.error('No Start', currentLocation)
} else {
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
})
book.loaded.metadata.then((metadata) => {
// this.author = metadata.creator
// this.title = metadata.title
})
// book.loaded.metadata.then((metadata) => {
// this.author = metadata.creator
// this.title = metadata.title
// })
// const spine_get = book.spine.get.bind(book.spine)
// book.spine.get = function (target) {
@ -121,8 +114,6 @@ export default {
// return t
// }
this.rendition.on('keyup', this.keyUp)
this.rendition.on('relocated', (location) => {
var percent = book.locations.percentageFromCfi(location.start.cfi)
this.progress = Math.floor(percent * 100)

View file

@ -1,9 +1,9 @@
<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">
<iframe title="html-viewer" class="w-full overflow-hidden"> Loading </iframe>
</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>
<div class="flex-grow" />
</div>
@ -25,9 +25,6 @@ export default {
computed: {
playerLibraryItemId() {
return this.$store.state.playerLibraryItemId
},
readerHeightOffset() {
return this.playerLibraryItemId ? 164 : 64
}
},
methods: {
@ -122,13 +119,15 @@ export default {
<style>
.mobi-ebook-viewer {
height: calc(100% - 32px);
max-height: calc(100% - 32px);
overflow: hidden;
height: calc(100% - 52px);
max-height: calc(100% - 52px);
overflow-x: hidden;
overflow-y: auto;
}
.reader-player-open .mobi-ebook-viewer {
height: calc(100% - 132px);
max-height: calc(100% - 132px);
overflow: hidden;
height: calc(100% - 152px);
max-height: calc(100% - 152px);
overflow-x: hidden;
overflow-y: auto;
}
</style>

View file

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

View file

@ -1,6 +1,6 @@
<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 class="h-8 w-full bg-primary flex items-center px-2 fixed top-0 left-0 z-30 box-shadow-sm">
<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-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>
<div class="flex-grow" />
<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 touchDistanceY = Math.abs(this.touchendY - this.touchstartY)
if (touchDistanceX < 100 || touchDistanceY > touchDistanceX) return
if (touchDistanceX < 60 || touchDistanceY > touchDistanceX) {
return
}
if (this.touchendX < this.touchstartX) {
console.log('swiped left')
@ -144,8 +146,8 @@ export default {
}
},
touchstart(e) {
this.touchstartX = e.changedTouches[0].screenX
this.touchstartY = e.changedTouches[0].screenY
this.touchstartX = e.touches[0].screenX
this.touchstartY = e.touches[0].screenY
this.touchstartTime = Date.now()
},
touchend(e) {

View file

@ -348,8 +348,9 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL {
// filename needs to be encoded otherwise would just use contentUrl
let filenameEncoded = track.metadata?.filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
let urlstr = "\(Store.serverConfig!.address)/s/item/\(item.id)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
let relPath = track.metadata?.relPath ?? ""
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)!
}

View file

@ -1,5 +1,5 @@
<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 />
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
<Nuxt />
@ -101,7 +101,7 @@ export default {
const deviceData = await this.$db.getDeviceData()
let serverConfig = null
if (deviceData) {
this.$store.commit('globals/setHapticFeedback', deviceData.hapticFeedback)
this.$store.commit('globals/setHapticFeedback', deviceData.deviceSettings?.hapticFeedback)
if (deviceData.lastServerConnectionConfigId && deviceData.serverConnectionConfigs.length) {
serverConfig = deviceData.serverConnectionConfigs.find((scc) => scc.id == deviceData.lastServerConnectionConfigId)

16
package-lock.json generated
View file

@ -26,6 +26,7 @@
"libarchive.js": "^1.3.0",
"nuxt": "^2.15.7",
"socket.io-client": "^4.1.3",
"v-click-outside": "^3.2.0",
"vue-pdf": "^4.2.0",
"vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3"
@ -17192,6 +17193,14 @@
"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": {
"version": "1.1.2",
"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",
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -33339,4 +33353,4 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
}
}
}
}

View file

@ -30,6 +30,7 @@
"libarchive.js": "^1.3.0",
"nuxt": "^2.15.7",
"socket.io-client": "^4.1.3",
"v-click-outside": "^3.2.0",
"vue-pdf": "^4.2.0",
"vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3"
@ -41,4 +42,4 @@
"@nuxtjs/tailwindcss": "^4.2.0",
"postcss": "^8.3.5"
}
}
}

View file

@ -1,14 +1,83 @@
<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>
<script>
export default {
data() {
return {}
return {
loading: true,
authors: [],
loadedLibraryId: null
}
},
watch: {},
computed: {},
methods: {}
computed: {
currentLibraryId() {
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>

View file

@ -37,6 +37,7 @@ export default {
data() {
return {
shelves: [],
isSettingsLoaded: false,
isFirstNetworkConnection: true,
lastServerFetch: 0,
lastServerFetchLibraryId: null,
@ -276,12 +277,25 @@ export default {
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
// Try opening the first book from continue-listening without playing it
if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return
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 mostRecentEntity = continueListeningShelf?.entities?.[0]
if (mostRecentEntity) {
@ -356,11 +370,16 @@ export default {
}
})
},
settingsUpdated() {
this.isSettingsLoaded = true
},
initListeners() {
this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('user-settings', this.settingsUpdated)
},
removeListeners() {
this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('user-settings', this.settingsUpdated)
}
},
mounted() {

View file

@ -17,7 +17,8 @@ export default {
totalEpisodes: 0,
currentPage: 0,
localEpisodeMap: {},
isLocal: false
isLocal: false,
loadedLibraryId: null
}
},
watch: {},
@ -39,6 +40,7 @@ export default {
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
},
async loadRecentEpisodes(page = 0) {
this.loadedLibraryId = this.currentLibraryId
this.processing = true
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)
@ -50,10 +52,23 @@ export default {
this.recentEpisodes = episodePayload.episodes || []
this.totalEpisodes = episodePayload.total
this.currentPage = page
},
libraryChanged(libraryId) {
if (libraryId !== this.loadedLibraryId) {
if (this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast') {
this.loadRecentEpisodes()
} else {
this.$router.replace('/bookshelf')
}
}
}
},
mounted() {
this.loadRecentEpisodes()
this.$eventBus.$on('library-changed', this.libraryChanged)
},
beforeDestroy() {
this.$eventBus.$off('library-changed', this.libraryChanged)
}
}
</script>

View file

@ -7,7 +7,6 @@ export default {
async asyncData({ store, params, query }) {
// Set filter by
if (query.filter) {
store.commit('user/setSettings', { mobileFilterBy: query.filter })
await store.dispatch('user/updateUserSettings', { mobileFilterBy: query.filter })
}
}

View file

@ -1,5 +1,5 @@
<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="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%)" />

View file

@ -189,7 +189,11 @@ export default {
<style scoped>
.media-item-container {
height: calc(100vh - 200px);
max-height: calc(100vh - 200px);
height: calc(100vh - 210px);
max-height: calc(100vh - 210px);
}
.playerOpen .media-item-container {
height: calc(100vh - 310px);
max-height: calc(100vh - 310px);
}
</style>

View file

@ -390,7 +390,7 @@ export default {
}
</script>
<style>
<style scoped>
.media-item-container {
height: calc(100vh - 200px);
max-height: calc(100vh - 200px);

View file

@ -53,7 +53,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true })
if (playbackSession) {
if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime
this.setAudioPlayer(playbackSession, true)
this.setAudioPlayer(playbackSession, playWhenReady)
}
}
return false

View file

@ -1,9 +1,12 @@
import Vue from 'vue'
import vClickOutside from 'v-click-outside'
import { App } from '@capacitor/app'
import { Dialog } from '@capacitor/dialog'
import { StatusBar, Style } from '@capacitor/status-bar';
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') {
const setStatusBarStyleDark = async () => {
@ -150,43 +153,6 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
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) {
const json = {};
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {

View file

@ -5,7 +5,9 @@ export const state = () => ({
mobileOrderBy: 'addedAt',
mobileOrderDesc: true,
mobileFilterBy: 'all',
playbackRate: 1
playbackRate: 1,
collapseSeries: false,
collapseBookSeries: false
}
})
@ -89,8 +91,8 @@ export const actions = {
}
}
if (hasChanges) {
await this.$localStore.setUserSettings(existingSettings)
commit('setSettings', existingSettings)
await this.$localStore.setUserSettings(existingSettings)
this.$eventBus.$emit('user-settings', state.settings)
}
},