Compare commits

...
Author SHA1 Message Date
advplyr 793f0c05f7 iOS version bump 0.9.70 2023-12-18 17:15:22 -06:00
advplyr cf7271dc57 Version bump 0.9.70 2023-12-18 17:10:07 -06:00
advplyr 54664b0c32 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-18 17:05:40 -06:00
advplyr ecf910dc55 Fix:Play button loading indicator when offline #990 2023-12-18 17:05:34 -06:00
advplyr 460daec72e iOS version bump 0.9.69 2023-12-16 17:21:43 -06:00
advplyr 4dd67adcab Fix:Prevent pressing download buttons twice causing crash in iOS 2023-12-16 17:21:19 -06:00
advplyr 023c83ee78 Version bump 0.9.69-beta 2023-12-16 16:24:07 -06:00
advplyr 2b8ed16616 Update:UI for playlist & rss feed modal to add more padding on top and bottom to prevent iOS cutoff 2023-12-16 15:06:49 -06:00
advplyr 6ae4d72b68 Update:Settings UI cleanup and light theme updates 2023-12-16 14:43:48 -06:00
advplyr c6bd93ef5c Light theme updates 2023-12-16 14:37:05 -06:00
advplyr e1c02ce74c Update:Show loading indicator on play buttons when starting playback 2023-12-15 17:35:37 -06:00
advplyr efc6d68403 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-15 16:05:53 -06:00
advplyr 7412d0899f Fix:Syncing when media is open in audio player and playing on another device #984
- Local playback session IDs have been updated to UUIDs instead of prefixing local_play_
2023-12-15 16:05:19 -06:00
advplyrandGitHub 42eb0a8384 Merge pull request #988 from burghy86/patch-2
Update it.json
2023-12-15 15:03:25 -06:00
burghy86andGitHub 27e83e8ed9 Update it.json 2023-12-15 22:02:26 +01:00
advplyr dd66042766 Update:Dont show chapter number infront of chapter title #981 2023-12-14 16:59:47 -06:00
advplyr dc790d5450 Fix light mode for new podcast page 2023-12-14 16:55:17 -06:00
advplyr 01ac844024 Fix light theme on add podcast page 2023-12-14 16:39:55 -06:00
advplyr c8064ac7bc Rename page name from search to add-podcast 2023-12-14 16:25:59 -06:00
advplyrandGitHub f5da44dc6f Merge pull request #986 from Hallo951/master
Update de.json
2023-12-14 08:15:26 -06:00
Hallo951andGitHub cbdff29684 Update de.json 2023-12-14 08:19:27 +01:00
advplyr 19d6c84113 Update:Light theme colors for track progress bar and ereader settings #916 2023-12-13 16:57:44 -06:00
advplyrandGitHub d9f4ed3d54 Merge pull request #983 from Nab0y/master
Fix Russian localization
2023-12-12 17:50:06 -06:00
advplyr fdb26b7700 Update:Set device language to match the first server connected to #448 2023-12-12 17:50:04 -06:00
Dmitry Naboychenko 94cb7a65a2 Fix Russian localization 2023-12-12 22:34:53 +03:00
advplyr 9298065934 Add:Theme option setting #916 2023-12-11 18:11:17 -06:00
advplyr 1cf01a5d12 Add home page shelf translation strings #448 2023-12-11 17:42:29 -06:00
advplyr e3aa96cbd5 Fix:Setting device language code 2023-12-11 17:27:26 -06:00
advplyr c7678da664 Update:Setup css colors for themes, add light theme colors #916 2023-12-10 17:53:27 -06:00
advplyrandGitHub 52701048ad Merge pull request #967 from tonyedwardspz/master
Feature - Add a low feedback mode
2023-12-09 15:14:52 -06:00
advplyr 776c02828f Fix latest page to show download indicator, update download button to be responsive right away 2023-12-09 14:57:44 -06:00
advplyr 69308413f6 Fix bad merge with master 2023-12-08 17:04:03 -06:00
advplyr 84e376a7cf Remove commented out toasts 2023-12-08 17:03:08 -06:00
advplyr 1822b21af8 Remove socketConnected check on local items alert 2023-12-08 16:49:12 -06:00
advplyr 8b222435e6 Fix:Load local settings before attempting server connection #976 2023-12-08 16:10:57 -06:00
advplyrandGitHub fdd251ec2c Merge pull request #975 from Jafeyyu/master
Improve Chinese translation
2023-12-08 16:01:24 -06:00
Tony Edwards 78170d4e10 Potential toasts for removal 2023-12-08 09:59:22 +00:00
Tony Edwards 837981b8b6 Removes download prompt on podcast ep page 2023-12-08 09:56:07 +00:00
Tony Edwards be23c6703f Shows media link status when connected to a server 2023-12-08 09:08:51 +00:00
Tony Edwards 0c72889917 Remove successfully connected media link message 2023-12-08 07:56:45 +00:00
Tony Edwards 02746bc35f Fixes my introduced errors 2023-12-08 07:53:35 +00:00
Qi, Xiaoqiang 85a5c33bd8 Improve Chinese translation 2023-12-08 15:33:37 +08:00
Tony Edwards 5067c0f6d7 Removes download confirmation prompt 2023-12-08 07:30:54 +00:00
Tony Edwards 89938ab120 Removes download success toast 2023-12-08 07:25:51 +00:00
Tony Edwards 9a32396daa Removes low feedback toggle 2023-12-08 07:19:23 +00:00
advplyr ddb1796891 Update serverconfig to remove version before saving 2023-12-07 17:02:50 -06:00
advplyrandGitHub 76253b7b9d Merge pull request #969 from Sapd/sso-redirect-ri
SSO: Add redirect_uri to oAuth auth request
2023-12-07 16:08:19 -06:00
advplyr 74fef1d2f4 Make backwards compatible with server v2.6.0 2023-12-07 11:07:37 -06:00
advplyrandGitHub 7306ffa709 Merge pull request #972 from JannikGohr/german_translation
Add German Translations
2023-12-06 17:45:41 -06:00
JannikGohr 6f289293cc Continue german translation in strings/de.json 2023-12-07 00:09:34 +01:00
advplyr 783f924bb9 Add:Ebook library filters #971 2023-12-05 16:14:20 -06:00
Denis Arnst 044a35aacf SSO: Removed isRest as requirement
Removed in the backend to increase compatibility with possible 3rd party oauth libraries people might use
2023-12-05 09:44:20 +01:00
Tony Edwards 78448894d2 Adds hiding audio book feedback promt/toast 2023-12-05 07:31:03 +00:00
Tony Edwards aca348e783 Adds low feedback mode to podcast downloads 2023-12-05 07:31:03 +00:00
Tony Edwards e6c1874cbb Adds download success toasts to low feedback mode 2023-12-05 07:31:03 +00:00
Tony Edwards 1b06dbd78c Adds basic low feedback mode implementation 2023-12-05 07:31:03 +00:00
advplyr a26f37aa49 Add more translation strings, clean out unused strings #448 2023-12-04 17:53:36 -06:00
Denis Arnst 8af3c09458 SSO: Add redirect_uri to oAuth auth request
Also:
- Add client_id with the name of the app (for possible future use)
- Add response_type simply to provide all required oauth2 parameters for reference
- For the URL to be opened in Browser: Forward the callback/redirect_uri URL provided by the server
- Use a hardcoded variable for HTTPS enforcement, for easier debugging
2023-12-04 22:43:14 +01:00
advplyr fed6579e08 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-03 17:37:14 -06:00
advplyr 6621f8b2ee Add:Language code setting and translations #448 2023-12-03 17:37:01 -06:00
advplyrandGitHub 737b6d7c26 Merge pull request #964 from Sapd/seek-fix
Fix track skipping on iOS (Fixes #686)
2023-11-30 17:06:33 -06:00
Denis Arnst 59c71b71fb Fix track skipping on iOS (Fixes #686)
When we seek into a different chapter, we will have to reset the progress of the current one before seeking to the new one.
2023-11-29 17:30:34 +01:00
131 changed files with 5928 additions and 1488 deletions
+2 -2
View File
@@ -33,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 99
versionName "0.9.68-beta"
versionCode 101
versionName "0.9.70-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
@@ -121,7 +121,8 @@ data class DeviceSettings(
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean
var disableSleepTimerResetFeedback: Boolean,
var languageCode: String
) {
companion object {
// Static method to get default device settings
@@ -143,7 +144,8 @@ data class DeviceSettings(
autoSleepTimerAutoRewind = false,
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false
disableSleepTimerResetFeedback = false,
languageCode = "en-us"
)
}
}
@@ -81,7 +81,7 @@ class LocalLibraryItem(
@JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
val localEpisodeId = episode?.id
val sessionId = "play_local_${UUID.randomUUID()}"
val sessionId = "${UUID.randomUUID()}"
// Get current progress for local media
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
@@ -48,6 +48,11 @@ object DeviceManager {
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
}
// Language added in v0.9.69
if (deviceData.deviceSettings?.languageCode == null) {
deviceData.deviceSettings?.languageCode = "en-us"
}
}
fun getBase64Id(id:String):String {
+4 -3
View File
@@ -1,3 +1,4 @@
@import "./tailwind.css";
@import "./fonts.css";
@import './defaultStyles.css';
@import './absicons.css';
@@ -148,15 +149,15 @@ Bookshelf Label
.tracksTable {
border-collapse: collapse;
width: 100%;
border: 1px solid #474747;
border: 1px solid rgb(var(--color-secondary));
}
.tracksTable tr:nth-child(even) {
background-color: #2e2e2e;
background-color: rgb(var(--color-secondary));
}
.tracksTable tr {
background-color: #373838;
background-color: rgb(var(--color-bg));
}
.tracksTable td {
+43
View File
@@ -0,0 +1,43 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
color: white;
--color-bg: 56 56 56;
--color-bg-hover: 102 104 107;
--color-fg: 230 237 243;
--color-fg-muted: 142 147 153;
--color-primary: 35 35 35;
--color-secondary: 47 48 48;
--color-border: 75 85 89;
--color-bg-toggle: 56 56 56;
--color-bg-toggle-selected: 81 82 84;
--color-track-cursor: 229 231 235;
--color-track: 107 114 128;
--color-track-buffered: 75 85 99;
--gradient-item-page: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%);
--gradient-audio-player: linear-gradient(169deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%);
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%);
}
html[data-theme='light'] {
color: black;
--color-bg: 255 255 255;
--color-bg-hover: 208 210 212;
--color-fg: 37 37 37;
--color-fg-muted: 101 109 118;
--color-primary: 222 222 222;
--color-secondary: 246 248 250;
--color-border: 189 191 191;
--color-bg-toggle: 222 222 222;
--color-bg-toggle-selected: 255 255 255;
--color-track-cursor: 101 109 118;
--color-track: 189 191 191;
--color-track-buffered: 129 131 131;
--gradient-item-page: linear-gradient(169deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-audio-player: linear-gradient(169deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.9) 20%, rgb(255, 255, 255) 60%);
}
}
+3 -3
View File
@@ -4,13 +4,13 @@
<nuxt-link v-show="!showBack" to="/" class="mr-3">
<img src="/Logo.png" class="h-10 w-10" />
</nuxt-link>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
<span class="material-icons text-3xl text-white">arrow_back</span>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<span class="material-icons text-3xl text-fg">arrow_back</span>
</a>
<div v-if="user && currentLibrary">
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
<p class="text-smleading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
</div>
</div>
+30 -30
View File
@@ -1,7 +1,7 @@
<template>
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
<div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)" />
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
<div class="top-4 left-4 absolute cursor-pointer">
<span class="material-icons text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="collapseFullscreen">expand_more</span>
@@ -12,20 +12,20 @@
<div class="top-6 right-4 absolute cursor-pointer">
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
</div>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ 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" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
</div>
<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>
<p class="font-mono text-fg" 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>
<p class="font-mono text-fg" 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 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 class="h-1 w-full bg-track/50 relative rounded-full">
<div ref="totalReadyTrack" class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalBufferedTrack" class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalPlayedTrack" class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full" />
</div>
</div>
</div>
@@ -42,56 +42,56 @@
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
<p class="title-text truncate">{{ title }}</p>
<p class="author-text text-white text-opacity-75 truncate">{{ authorName }}</p>
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
</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 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 && serverLibraryItemId && 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 && serverLibraryItemId && networkConnected" class="material-icons text-3xl text-fg-muted cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
<!-- hidden for podcasts but still using this as a placeholder -->
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
<span class="font-mono text-white text-opacity-75 cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white text-opacity-75 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
<span class="font-mono text-fg-muted cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-fg-muted cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<div v-else class="h-7 w-7 flex items-center justify-around cursor-pointer" @click.stop="$emit('showSleepTimer')">
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
</div>
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
<span class="material-icons text-3xl text-fg cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
</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: var(--gradient-minimized-audio-player)" />
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
<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="showFullscreen && !lockUi" class="material-icons next-icon text-fg 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-fg 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 v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" />
</div>
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div>
</div>
<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>
<p class="font-mono text-fg" style="font-size: 0.8rem" ref="currentTimestamp">0:00</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-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
</div>
<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="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor">
<div class="bg-gray-200 rounded-full w-3.5 h-3.5 pointer-events-none" />
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
</div>
</div>
</div>
@@ -175,7 +175,7 @@ export default {
// TODO: Implement on iOS
if (this.$platform !== 'ios' && !this.isPodcast && this.mediaId) {
items.push({
text: 'History',
text: this.$strings.ButtonHistory,
value: 'history',
icon: 'history'
})
@@ -184,22 +184,22 @@ export default {
items.push(
...[
{
text: 'Total Track',
text: this.$strings.LabelTotalTrack,
value: 'total_track',
icon: this.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
},
{
text: 'Chapter Track',
text: this.$strings.LabelChapterTrack,
value: 'chapter_track',
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.lockUi ? 'Unlock Player' : 'Lock Player',
text: this.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
value: 'lock',
icon: this.lockUi ? 'lock' : 'lock_open'
},
{
text: 'Close Player',
text: this.$strings.LabelClosePlayer,
value: 'close',
icon: 'close'
}
+5
View File
@@ -204,6 +204,7 @@ export default {
message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.`
})
if (!value) {
this.$store.commit('setPlayerDoneStartingPlayback')
return
}
}
@@ -217,6 +218,7 @@ export default {
} else if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.play()
}
this.$store.commit('setPlayerDoneStartingPlayback')
return
}
@@ -254,6 +256,9 @@ export default {
console.error('Failed', error)
this.$toast.error('Failed to play')
})
.finally(() => {
this.$store.commit('setPlayerDoneStartingPlayback')
})
},
pauseItem() {
if (this.$refs.audioPlayer && this.$refs.audioPlayer.isPlaying) {
+14 -14
View File
@@ -1,7 +1,7 @@
<template>
<div class="fixed top-0 left-0 right-0 layout-wrapper w-full z-50 overflow-hidden pointer-events-none">
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="absolute top-0 right-0 w-64 h-full bg-bg transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="px-6 mb-4">
<p v-if="user" class="text-base">
Welcome,
@@ -11,25 +11,25 @@
<div class="w-full overflow-y-auto">
<template v-for="item in navItems">
<button v-if="item.action" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300" @click="clickAction(item.action)">
<button v-if="item.action" :key="item.text" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg-muted" @click="clickAction(item.action)">
<span class="text-lg" :class="item.iconOutlined ? 'material-icons-outlined' : 'material-icons'">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</button>
<nuxt-link v-else :to="item.to" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg bg-opacity-60' : ''">
<nuxt-link v-else :to="item.to" :key="item.text" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg-hover/50' : 'text-fg-muted'">
<span class="text-lg" :class="item.iconOutlined ? 'material-icons-outlined' : 'material-icons'">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</nuxt-link>
</template>
</div>
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-fg">
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
<p class="text-xs text-gray-400" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
<p class="text-xs text-fg-muted" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
</div>
<div class="flex items-center">
<p class="text-xs">{{ $config.version }}</p>
<div class="flex-grow" />
<div v-if="user" class="flex items-center" @click="disconnect">
<p class="text-xs pr-2">Disconnect</p>
<p class="text-xs pr-2">{{ $strings.ButtonDisconnect }}</p>
<i class="material-icons text-sm -mb-0.5">cloud_off</i>
</div>
</div>
@@ -85,7 +85,7 @@ export default {
var items = [
{
icon: 'home',
text: 'Home',
text: this.$strings.ButtonHome,
to: '/bookshelf'
}
]
@@ -93,19 +93,19 @@ export default {
items = [
{
icon: 'cloud_off',
text: 'Connect to Server',
text: this.$strings.ButtonConnectToServer,
to: '/connect'
}
].concat(items)
} else {
items.push({
icon: 'person',
text: 'Account',
text: this.$strings.HeaderAccount,
to: '/account'
})
items.push({
icon: 'equalizer',
text: 'User Stats',
text: this.$strings.ButtonUserStats,
to: '/stats'
})
}
@@ -114,27 +114,27 @@ export default {
items.push({
icon: 'folder',
iconOutlined: true,
text: 'Local Media',
text: this.$strings.ButtonLocalMedia,
to: '/localMedia/folders'
})
} else {
items.push({
icon: 'download',
iconOutlined: false,
text: 'Downloads',
text: this.$strings.HeaderDownloads,
to: '/downloads'
})
}
items.push({
icon: 'settings',
text: 'Settings',
text: this.$strings.HeaderSettings,
to: '/settings'
})
if (this.serverConnectionConfig) {
items.push({
icon: 'login',
text: 'Switch Server/User',
text: this.$strings.ButtonSwitchServerUser,
action: 'logout'
})
}
+1 -1
View File
@@ -9,7 +9,7 @@
<div v-show="!entities.length && initialized" class="w-full py-16 text-center text-xl">
<div class="py-4 capitalize">No {{ entityName }}</div>
<ui-btn v-if="hasFilter" @click="clearFilter">Clear Filter</ui-btn>
<ui-btn v-if="hasFilter" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
</div>
</div>
</template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="w-full relative">
<div v-if="altViewEnabled" class="px-5 pb-3 pt-4">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ label }}</p>
<p class="font-semibold" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ label }}</p>
</div>
<div class="flex items-end px-3 max-w-full overflow-x-auto" :class="altViewEnabled ? '' : 'bookshelfRow'" :style="{ height: shelfHeight + 'px', paddingBottom: entityPaddingBottom + 'px' }">
+2 -2
View File
@@ -6,8 +6,8 @@
<!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
<p class="text-center font-semibold truncate text-white" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div>
<!-- Loading spinner -->
+3 -3
View File
@@ -5,10 +5,10 @@
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" />
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-fg-muted">{{ matchHtml }}</p>
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<p v-if="matchKey !== 'authors'" class="text-xs text-fg truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-fg" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div>
+26 -10
View File
@@ -10,20 +10,20 @@
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</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 class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</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>
<p class="text-white" :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>
<p class="text-white" :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' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-fg-muted text-center">{{ title }}</p>
</div>
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
@@ -46,8 +46,13 @@
</div>
<!-- Play/pause button for podcast episode -->
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full bg-white bg-opacity-70" @click.stop="playEpisode">
<span class="material-icons text-6xl text-black text-opacity-80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full" :class="{ 'bg-white/70': !playerIsStartingForThisMedia }" @click.stop="playEpisode">
<span v-if="!playerIsStartingForThisMedia" class="material-icons text-6xl text-black/80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
<div v-else class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center bg-black/80 rounded-full overflow-hidden">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</div>
<!-- No progress shown for collapsed series in library -->
@@ -75,19 +80,19 @@
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber !== null && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
<div v-else-if="numEpisodes && !numEpisodesIncomplete && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div>
<!-- Podcast Num Episodes Incomplete -->
<div v-else-if="numEpisodesIncomplete && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
</div>
</div>
</template>
@@ -322,6 +327,14 @@ export default {
streamIsPlaying() {
return this.store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.store.state.playerStartingPlaybackMediaId
return mediaId === this.recentEpisode?.id
},
isMissing() {
return this._libraryItem.isMissing
},
@@ -447,6 +460,8 @@ export default {
},
async play() {},
async playEpisode() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
const eventBus = this.$eventBus || this.$nuxt.$eventBus
if (this.streamIsPlaying) {
@@ -454,6 +469,7 @@ export default {
return
}
this.store.commit('setPlayerIsStartingPlayback', this.recentEpisode.id)
if (this.localEpisode) {
// Play episode locally
eventBus.$emit('play-item', {
+1 -1
View File
@@ -5,7 +5,7 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+7 -7
View File
@@ -18,10 +18,10 @@
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
<span v-if="seriesSequence">#{{ seriesSequence }}&nbsp;</span>{{ displayTitle }}
</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<p v-if="duration" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
<p v-if="episodes" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<p v-if="duration" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
<p v-if="episodes" class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
</div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
@@ -176,7 +176,7 @@ export default {
return this.mediaMetadata.series
},
seriesSequence() {
return this.series ? this.series.sequence : null
return this.series?.sequence || null
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
@@ -209,10 +209,10 @@ export default {
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
return this.userProgress?.progress || 0
},
itemIsFinished() {
return this.userProgress ? !!this.userProgress.isFinished : false
return !!this.userProgress?.isFinished
},
showError() {
return this.numMissingParts || this.isMissing || this.isInvalid
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
</div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-center" :style="{ width: Math.min(160, width) + 'px' }">
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(160, width) + 'px' }">
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+1 -1
View File
@@ -10,7 +10,7 @@
<div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-center" :style="{ width: Math.min(240, width) + 'px' }">
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">local_offer</span>
<span class="material-icons text-2xl text-fg">local_offer</span>
</div>
<div class="flex-grow px-2 tagSearchCardContent h-full">
<p class="truncate text-sm">{{ tag }}</p>
+55 -26
View File
@@ -3,16 +3,16 @@
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg px-4 py-6 w-full">
<!-- list of server connection configs -->
<template v-if="!showForm">
<div v-for="config in serverConnectionConfigs" :key="config.id" class="flex items-center py-4 my-1 border-b border-white border-opacity-10 relative" @click="connectToServer(config)">
<span class="material-icons-outlined text-xl text-gray-300">dns</span>
<p class="pl-3 pr-6 text-base text-gray-200">{{ config.name }}</p>
<div v-for="config in serverConnectionConfigs" :key="config.id" class="flex items-center py-4 my-1 border-b border-fg/10 relative" @click="connectToServer(config)">
<span class="material-icons-outlined text-xl text-fg-muted">dns</span>
<p class="pl-3 pr-6 text-base text-fg">{{ config.name }}</p>
<div class="absolute top-0 right-0 h-full px-4 flex items-center" @click.stop="editServerConfig(config)">
<span class="material-icons text-lg text-gray-300">more_vert</span>
<span class="material-icons text-lg text-fg-muted">more_vert</span>
</div>
</div>
<div class="my-1 py-4 w-full">
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
<ui-btn class="w-full" @click="newServerConfigClick">{{ $strings.ButtonAddNewServer }}</ui-btn>
</div>
</template>
<!-- form to add a new server connection config -->
@@ -20,37 +20,37 @@
<!-- server address input -->
<form v-if="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<div v-if="serverConnectionConfigs.length" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
<span class="material-icons text-fg-muted">arrow_back</span>
</div>
<h2 class="text-lg leading-7 mb-2">Server address</h2>
<h2 class="text-lg leading-7 mb-2">{{ $strings.LabelServerAddress }}</h2>
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || !!serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" />
<div class="flex justify-end items-center mt-6">
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
</div>
</form>
<!-- username/password and auth methods -->
<template v-else>
<div v-if="serverConfig.id" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
<span class="material-icons text-fg-muted">arrow_back</span>
</div>
<div class="flex items-center">
<p class="text-gray-300">{{ serverConfig.address }}</p>
<p class="text-fg-muted">{{ serverConfig.address }}</p>
<div class="flex-grow" />
<span v-if="!serverConfig.id" class="material-icons" style="font-size: 1.1rem" @click="editServerAddress">edit</span>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="w-full h-px bg-fg/10 my-2" />
<form v-if="isLocalAuthEnabled" @submit.prevent="submitAuth" class="pt-3">
<ui-text-input v-model="serverConfig.username" :disabled="processing" placeholder="username" class="w-full mb-2 text-lg" />
<ui-text-input v-model="password" type="password" :disabled="processing" placeholder="password" class="w-full mb-2 text-lg" />
<ui-text-input v-model="serverConfig.username" :disabled="processing" :placeholder="$strings.LabelUsername" class="w-full mb-2 text-lg" />
<ui-text-input v-model="password" type="password" :disabled="processing" :placeholder="$strings.LabelPassword" class="w-full mb-2 text-lg" />
<div class="flex items-center pt-2">
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" @click="removeServerConfigClick" />
<div class="flex-grow" />
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
</div>
</form>
<div v-if="isLocalAuthEnabled && isOpenIDAuthEnabled" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div v-if="isLocalAuthEnabled && isOpenIDAuthEnabled" class="w-full h-px bg-fg/10 my-4" />
<ui-btn v-if="isOpenIDAuthEnabled" :disabled="processing" class="h-10 w-full" @click="clickLoginWithOpenId">{{ oauth.buttonText }}</ui-btn>
</template>
</div>
@@ -62,7 +62,7 @@
</div>
</div>
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black/75 flex items-center justify-center z-30 transition-opacity duration-500">
<div>
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
<img src="/Logo.png" class="h-20 w-20 mb-2" />
@@ -73,7 +73,7 @@
</div>
</div>
<p v-if="!serverConnectionConfigs.length" class="mt-2 text-center text-error"><strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.</p>
<p v-if="!serverConnectionConfigs.length" class="mt-2 text-center text-error" v-html="$strings.MessageAudiobookshelfServerRequired" />
<modals-custom-headers-modal v-model="showAddCustomHeaders" :custom-headers.sync="serverConfig.customHeaders" />
</div>
@@ -95,6 +95,7 @@ export default {
processing: false,
serverConfig: {
address: null,
version: null,
username: null,
customHeaders: null
},
@@ -107,7 +108,8 @@ export default {
state: null,
verifier: null,
challenge: null,
buttonText: 'Login with OpenID'
buttonText: 'Login with OpenID',
enforceHTTPs: true // RFC 6749, Section 10.9 requires https
}
}
},
@@ -115,6 +117,9 @@ export default {
deviceData() {
return this.$store.state.deviceData || {}
},
deviceSettings() {
return this.deviceData.deviceSettings || {}
},
networkConnected() {
return this.$store.state.networkConnected
},
@@ -155,7 +160,7 @@ export default {
*/
async clickLoginWithOpenId() {
// oauth standard requires https explicitly
if (!this.serverConfig.address.startsWith('https')) {
if (!this.serverConfig.address.startsWith('https') && this.oauth.enforceHTTPs) {
console.warn(`[SSO] Oauth2 requires HTTPS`)
this.$toast.error(`SSO: The URL to the server must be https:// secured`)
return
@@ -179,14 +184,19 @@ export default {
const client_id = redirectUrl.searchParams.get('client_id')
const scope = redirectUrl.searchParams.get('scope')
const state = redirectUrl.searchParams.get('state')
let redirect_uri_param = redirectUrl.searchParams.get('redirect_uri')
// Backwards compatability with 2.6.0
if (this.serverConfig.version === '2.6.0') {
redirect_uri_param = 'audiobookshelf://oauth'
}
if (!client_id || !scope || !state) {
console.warn(`[SSO] Invalid OpenID URL - client_id scope or state missing: ${redirectUrl}`)
if (!client_id || !scope || !state || !redirect_uri_param) {
console.warn(`[SSO] Invalid OpenID URL - client_id scope state or redirect_uri missing: ${redirectUrl}`)
this.$toast.error(`SSO: Invalid answer`)
return
}
if (redirectUrl.protocol !== 'https:') {
if (redirectUrl.protocol !== 'https:' && this.oauth.enforceHTTPs) {
console.warn(`[SSO] Insecure Redirection by SSO provider: ${redirectUrl.protocol} is not allowed. Use HTTPS`)
this.$toast.error(`SSO: The SSO provider must return a HTTPS secured URL`)
return
@@ -195,8 +205,8 @@ export default {
// We need to verify if the state is the same later
this.oauth.state = state
const host = `https://${redirectUrl.host}`
const buildUrl = `${host}${redirectUrl.pathname}?response_type=code` + `&client_id=${encodeURIComponent(client_id)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}` + `&redirect_uri=${encodeURIComponent('audiobookshelf://oauth')}` + `&code_challenge=${encodeURIComponent(this.oauth.challenge)}&code_challenge_method=S256`
const host = `${redirectUrl.protocol}//${redirectUrl.host}`
const buildUrl = `${host}${redirectUrl.pathname}?response_type=code` + `&client_id=${encodeURIComponent(client_id)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}` + `&redirect_uri=${encodeURIComponent(redirect_uri_param)}` + `&code_challenge=${encodeURIComponent(this.oauth.challenge)}&code_challenge_method=S256`
// example url for authentik
// const authURL = "https://authentik/application/o/authorize/?response_type=code&client_id=41cd96f...&redirect_uri=audiobookshelf%3A%2F%2Foauth&scope=openid%20openid%20email%20profile&state=asdds..."
@@ -243,8 +253,11 @@ export default {
this.oauth.verifier = verifier
this.oauth.challenge = challenge
// set parameter isRest to true, so the backend wont attempt a redirect after we call backend:/callback in exchangeCodeForToken
const backendEndpoint = `${url}/auth/openid?code_challenge=${challenge}&code_challenge_method=S256&isRest=true`
let backendEndpoint = `${url}/auth/openid?code_challenge=${challenge}&code_challenge_method=S256&redirect_uri=${encodeURIComponent('audiobookshelf://oauth')}&client_id=${encodeURIComponent('Audiobookshelf-App')}&response_type=code`
// Backwards compatability with 2.6.0
if (this.serverConfig.version === '2.6.0') {
backendEndpoint += '&isRest=true'
}
try {
const response = await CapacitorHttp.get({
@@ -608,6 +621,7 @@ export default {
this.showAuth = true
this.authMethods = statusData.data.authMethods || []
this.oauth.buttonText = statusData.data.authFormData?.authOpenIDButtonText || 'Login with OpenID'
this.serverConfig.version = statusData.data.serverVersion
if (statusData.data.authFormData?.authOpenIDAutoLaunch) {
this.clickLoginWithOpenId()
@@ -789,6 +803,7 @@ export default {
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
this.$setServerLanguageCode(serverSettings.language)
// Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId()
@@ -801,9 +816,23 @@ export default {
this.serverConfig.userId = user.id
this.serverConfig.token = user.token
this.serverConfig.username = user.username
delete this.serverConfig.version
var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig)
// Set the device language to match the servers if this is the first server connection
if (!this.serverConnectionConfigs.length && serverSettings.language !== 'en-us') {
const deviceSettings = {
...this.deviceSettings,
languageCode: serverSettings.language
}
const updatedDeviceData = await this.$db.updateDeviceSettings(deviceSettings)
if (updatedDeviceData) {
this.$store.commit('setDeviceData', updatedDeviceData)
this.$setLanguageCode(updatedDeviceData.deviceSettings?.languageCode || 'en-us')
}
}
this.$store.commit('user/setUser', user)
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
+1 -1
View File
@@ -15,7 +15,7 @@
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
<p class="text-fg-muted text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
</div>
</div>
</template>
+11 -11
View File
@@ -2,24 +2,24 @@
<div class="w-full px-2">
<img v-if="podcast.imageUrl" :src="podcast.imageUrl" class="h-36 w-36 object-contain mx-auto mb-2" />
<ui-text-input-with-label v-model="podcast.title" :label="'Title'" class="mb-2 text-sm" @input="titleUpdated" />
<ui-text-input-with-label v-model="podcast.title" :label="$strings.LabelTitle" class="mb-2 text-sm" @input="titleUpdated" />
<ui-text-input-with-label v-model="podcast.author" :label="'Author'" class="mb-2 text-sm" />
<ui-text-input-with-label v-model="podcast.author" :label="$strings.LabelAuthor" class="mb-2 text-sm" />
<ui-text-input-with-label v-model="podcast.feedUrl" :label="'Feed URL'" readonly class="mb-2 text-sm" />
<ui-text-input-with-label v-model="podcast.feedUrl" :label="$strings.LabelFeedURL" readonly class="mb-2 text-sm" />
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="'Genres'" class="mb-2 text-sm" />
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" class="mb-2 text-sm" />
<ui-textarea-with-label v-model="podcast.description" :label="'Description'" :rows="3" class="mb-2 text-sm" />
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" class="mb-2 text-sm" />
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="'Folder'" class="mb-2 text-sm" @input="folderUpdated" />
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="$strings.LabelFolder" class="mb-2 text-sm" @input="folderUpdated" />
<ui-text-input-with-label v-model="fullPath" :label="'Podcast Path'" input-class="h-10" readonly class="mb-2 text-sm" />
<ui-text-input-with-label v-model="fullPath" :label="$strings.LabelPath" input-class="h-10" readonly class="mb-2 text-sm" />
<div class="flex items-center py-4 px-2">
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="'Auto Download Episodes'" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="$strings.LabelAutoDownloadEpisodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
<div class="flex-grow" />
<ui-btn color="success" @click="submit">Submit</ui-btn>
<ui-btn color="success" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</template>
@@ -144,11 +144,11 @@ export default {
.post('/api/podcasts', podcastPayload)
.then((libraryItem) => {
this._processing = false
this.$toast.success('Podcast added')
this.$toast.success(this.$strings.ToastPodcastCreateSuccess)
this.$router.push(`/item/${libraryItem.id}`)
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to add podcast'
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
console.error('Failed to create podcast', error)
this._processing = false
this.$toast.error(errorMsg)
+15 -15
View File
@@ -1,7 +1,7 @@
<template>
<div class="w-full h-9 bg-bg relative">
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex-grow flex items-center justify-center" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary">
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex-grow flex items-center justify-center" :class="routeName === item.routeName ? 'bg-primary' : 'text-fg-muted'">
<p v-if="routeName === item.routeName" class="text-sm font-semibold">{{ item.text }}</p>
<span v-else-if="item.iconPack === 'abs-icons'" class="abs-icons" :class="`icon-${item.icon} ${item.iconClass || ''}`"></span>
<span v-else :class="`${item.iconPack} ${item.iconClass || ''}`">{{ item.icon }}</span>
@@ -20,7 +20,7 @@ export default {
return this.$store.getters['libraries/getCurrentLibrary']
},
currentLibraryIcon() {
return this.currentLibrary ? this.currentLibrary.icon : 'database'
return this.currentLibrary?.icon || 'database'
},
userHasPlaylists() {
return this.$store.state.libraries.numUserPlaylists
@@ -38,7 +38,7 @@ export default {
iconPack: 'abs-icons',
icon: 'home',
iconClass: 'text-xl',
text: 'Home'
text: this.$strings.ButtonHome
},
{
to: '/bookshelf/latest',
@@ -46,7 +46,7 @@ export default {
iconPack: 'abs-icons',
icon: 'list',
iconClass: 'text-xl',
text: 'Latest'
text: this.$strings.ButtonLatest
},
{
to: '/bookshelf/library',
@@ -54,18 +54,18 @@ export default {
iconPack: 'abs-icons',
icon: this.currentLibraryIcon,
iconClass: 'text-lg',
text: 'Library'
text: this.$strings.ButtonLibrary
}
]
if (this.userIsAdminOrUp) {
items.push({
to: '/bookshelf/search',
routeName: 'bookshelf-search',
to: '/bookshelf/add-podcast',
routeName: 'bookshelf-add-podcast',
iconPack: 'material-icons',
icon: 'podcasts',
iconClass: 'text-xl',
text: 'Add'
text: this.$strings.ButtonAdd
})
}
} else {
@@ -76,7 +76,7 @@ export default {
iconPack: 'abs-icons',
icon: 'home',
iconClass: 'text-xl',
text: 'Home'
text: this.$strings.ButtonHome
},
{
to: '/bookshelf/library',
@@ -84,7 +84,7 @@ export default {
iconPack: 'abs-icons',
icon: this.currentLibraryIcon,
iconClass: 'text-lg',
text: 'Library'
text: this.$strings.ButtonLibrary
},
{
to: '/bookshelf/series',
@@ -92,7 +92,7 @@ export default {
iconPack: 'abs-icons',
icon: 'columns',
iconClass: 'text-lg pt-px',
text: 'Series'
text: this.$strings.ButtonSeries
},
{
to: '/bookshelf/collections',
@@ -100,7 +100,7 @@ export default {
iconPack: 'material-icons-outlined',
icon: 'collections_bookmark',
iconClass: 'text-xl',
text: 'Collections'
text: this.$strings.ButtonCollections
},
{
to: '/bookshelf/authors',
@@ -108,7 +108,7 @@ export default {
iconPack: 'abs-icons',
icon: 'authors',
iconClass: 'text-2xl',
text: 'Authors'
text: this.$strings.ButtonAuthors
}
]
}
@@ -119,7 +119,7 @@ export default {
routeName: 'bookshelf-playlists',
iconPack: 'material-icons',
icon: 'queue_music',
text: 'Playlists'
text: this.$strings.ButtonPlaylists
})
}
+8 -8
View File
@@ -64,16 +64,16 @@ export default {
return this.$route.query || {}
},
entityTitle() {
if (this.isPodcast) return 'Podcasts'
if (this.page === 'library') return 'Books'
if (this.isPodcast) return this.$strings.LabelPodcasts
if (this.page === 'library') return this.$strings.LabelBooks
else if (this.page === 'series') {
return 'Series'
return this.$strings.LabelSeries
} else if (this.page === 'collections') {
return 'Collections'
return this.$strings.ButtonCollections
} else if (this.page === 'playlists') {
return 'Playlists'
return this.$strings.ButtonPlaylists
} else if (this.page === 'authors') {
return 'Authors'
return this.$strings.LabelAuthors
}
return ''
},
@@ -92,7 +92,7 @@ export default {
if (this.seriesBookPage) {
return [
{
text: 'Collapse Sub-Series',
text: this.$strings.LabelCollapseSeries,
value: 'collapse_subseries',
icon: this.settings.collapseBookSeries ? 'check_box' : 'check_box_outline_blank'
}
@@ -100,7 +100,7 @@ export default {
} else {
return [
{
text: 'Collapse Series',
text: this.$strings.LabelCollapseSeries,
value: 'collapse_series',
icon: this.settings.collapseSeries ? 'check_box' : 'check_box_outline_blank'
}
@@ -2,7 +2,7 @@
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Auto Rewind Time</p>
<p class="text-white text-2xl truncate capitalize">{{ $strings.LabelAutoRewindTime }}</p>
</div>
</template>
@@ -23,7 +23,7 @@
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
@@ -35,7 +35,7 @@
</template>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelCustomTime }}</span>
</div>
</li>
</ul>
+9 -9
View File
@@ -2,11 +2,11 @@
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<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">{{ $strings.LabelYourBookmarks }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full rounded-lg bg-primary border border-white border-opacity-20 overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent>
<div ref="container" class="w-full rounded-lg bg-primary border border-border overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent>
<div class="w-full h-full p-4" v-show="showBookmarkTitleInput">
<div class="flex mb-4 items-center">
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showBookmarkTitleInput = false">
@@ -29,11 +29,11 @@
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
</template>
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
<p class="text-xl">No Bookmarks</p>
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
</div>
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-white border-opacity-10 bg-blue-500 bg-opacity-20 cursor-pointer text-white text-opacity-80 hover:bg-opacity-40 hover:text-opacity-100" @click.stop="createBookmark">
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80" @click.stop="createBookmark">
<span class="material-icons">add</span>
<p class="text-base pl-2">Create Bookmark</p>
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
<p class="text-sm font-mono">
{{ this.$secondsToTimestamp(currentTime) }}
</p>
@@ -98,7 +98,7 @@ export default {
await this.$hapticsImpact()
const { value } = await Dialog.confirm({
title: 'Remove Bookmark',
message: `Are you sure you want to remove bookmark?`
message: this.$strings.MessageConfirmRemoveBookmark
})
if (!value) return
@@ -108,7 +108,7 @@ export default {
this.$store.commit('user/deleteBookmark', { libraryItemId: this.libraryItemId, time: bm.time })
})
.catch((error) => {
this.$toast.error(`Failed to remove bookmark`)
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
console.error(error)
})
},
@@ -124,7 +124,7 @@ export default {
this.showBookmarkTitleInput = false
})
.catch((error) => {
this.$toast.error(`Failed to update bookmark`)
this.$toast.error(this.$strings.ToastBookmarkUpdateFailed)
console.error(error)
})
},
@@ -142,7 +142,7 @@ export default {
this.$toast.success('Bookmark added')
})
.catch((error) => {
this.$toast.error(`Failed to create bookmark`)
this.$toast.error(this.$strings.ToastBookmarkCreateFailed)
console.error(error)
})
+6 -6
View File
@@ -2,19 +2,19 @@
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<div v-if="currentChapter" class="absolute top-10 left-4 z-40 pt-1" style="max-width: 80%">
<p class="text-white text-lg truncate">{{ chapters.length }} Chapters</p>
<p class="text-white text-lg truncate">{{ chapters.length }} {{ $strings.LabelChapters }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="(chapter, index) in chapters">
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer" :class="currentChapterId === chapter.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
<template v-for="chapter in chapters">
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-fg select-none relative py-4 cursor-pointer" :class="currentChapterId === chapter.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
<div class="relative flex items-center pl-3 pr-20">
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ index + 1 }} - {{ chapter.title }}</p>
<p class="font-normal block truncate text-sm text-fg/80">{{ chapter.title }}</p>
<div class="absolute top-0 right-3 -mt-0.5">
<span class="font-mono text-white text-opacity-90 leading-3 text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start / _playbackRate) }}</span>
<span class="font-mono text-fg-muted leading-3 text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start / _playbackRate) }}</span>
</div>
</div>
+4 -4
View File
@@ -7,14 +7,14 @@
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white/20 p-2" style="max-height: 75%" @click.stop>
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20 p-2" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<slot :name="item.value" :item="item" :selected="item.value === selected">
<li :key="item.value" :ref="`item-${item.value}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" :style="{ paddingTop: itemPaddingY, paddingBottom: itemPaddingY }" role="option" @click="clickedOption(item.value)">
<li :key="item.value" :ref="`item-${item.value}`" class="text-fg select-none relative cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" :style="{ paddingTop: itemPaddingY, paddingBottom: itemPaddingY }" role="option" @click="clickedOption(item.value)">
<div class="relative flex items-center px-3">
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">{{ item.icon }}</span>
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-fg text-opacity-80">{{ item.icon }}</span>
<p class="font-normal block truncate text-base text-fg text-opacity-80">{{ item.text }}</p>
</div>
</li>
</slot>
+39 -22
View File
@@ -2,14 +2,14 @@
<modals-modal v-model="show" width="90%" height="100%">
<template #outer>
<div v-show="selected !== 'all'" class="absolute top-10 left-4 z-40">
<ui-btn class="text-xl border-yellow-400 border-opacity-40" @click="clearSelected">Clear</ui-btn>
<ui-btn class="text-lg border-yellow-400 border-opacity-40" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop>
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
<div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate text-lg">{{ item.text }}</span>
</div>
@@ -20,12 +20,12 @@
</template>
</ul>
<ul v-show="sublist" class="h-full w-full rounded-lg" role="listbox" aria-labelledby="listbox-label">
<li class="text-gray-50 select-none relative py-3 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
<li class="text-fg select-none relative py-3 pl-9 cursor-pointer" role="option" @click="sublist = null">
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
<span class="material-icons text-2xl">arrow_left</span>
</div>
<div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate text-lg">Back</span>
<span class="font-normal ml-3 block truncate text-lg">{{ $strings.ButtonBack }}</span>
</div>
</li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
@@ -34,7 +34,7 @@
</div>
</li>
<template v-for="item in sublistItems">
<li :key="item.value" class="text-gray-50 select-none relative px-4 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
<li :key="item.value" class="text-fg select-none relative px-4 cursor-pointer" :class="`${sublist}.${item.value}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
<div class="flex items-center">
<span class="font-normal truncate py-3 text-base">{{ item.text }}</span>
</div>
@@ -57,62 +57,67 @@ export default {
sublist: null,
bookItems: [
{
text: 'All',
text: this.$strings.LabelAll,
value: 'all'
},
{
text: 'Genre',
text: this.$strings.LabelGenre,
value: 'genres',
sublist: true
},
{
text: 'Tag',
text: this.$strings.LabelTag,
value: 'tags',
sublist: true
},
{
text: 'Series',
text: this.$strings.LabelSeries,
value: 'series',
sublist: true
},
{
text: 'Authors',
text: this.$strings.LabelAuthor,
value: 'authors',
sublist: true
},
{
text: 'Narrator',
text: this.$strings.LabelNarrator,
value: 'narrators',
sublist: true
},
{
text: 'Language',
text: this.$strings.LabelLanguage,
value: 'languages',
sublist: true
},
{
text: 'Progress',
text: this.$strings.LabelProgress,
value: 'progress',
sublist: true
},
{
text: 'Issues',
text: this.$strings.LabelEbooks,
value: 'ebooks',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',
sublist: false
}
],
podcastItems: [
{
text: 'All',
text: this.$strings.LabelAll,
value: 'all'
},
{
text: 'Genre',
text: this.$strings.LabelGenre,
value: 'genres',
sublist: true
},
{
text: 'Tag',
text: this.$strings.LabelTag,
value: 'tags',
sublist: true
}
@@ -176,19 +181,31 @@ export default {
return [
{
id: 'finished',
name: 'Finished'
name: this.$strings.LabelFinished
},
{
id: 'in-progress',
name: 'In Progress'
name: this.$strings.LabelInProgress
},
{
id: 'not-started',
name: 'Not Started'
name: this.$strings.LabelNotStarted
},
{
id: 'not-finished',
name: 'Not Finished'
name: this.$strings.LabelNotFinished
}
]
},
ebooks() {
return [
{
id: 'ebook',
name: this.$strings.LabelHasEbook
},
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
}
]
},
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="halfScreen ? 'h-[50vh]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
<div ref="content" class="relative text-white h-full w-full bg-bg">
<div ref="content" class="relative text-fg h-full w-full bg-bg">
<slot />
</div>
</div>
+4 -4
View File
@@ -2,17 +2,17 @@
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<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">{{ $strings.HeaderDetails }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border p-2" style="max-height: 75%" @click.stop>
<p class="mb-2">{{ mediaMetadata.title }}</p>
<div v-if="size" class="text-sm mb-2">Size: {{ $bytesPretty(size) }}</div>
<div v-if="size" class="text-sm mb-2">{{ $strings.LabelSize }}: {{ $bytesPretty(size) }}</div>
<p class="mb-1 text-xs text-gray-200">ID: {{ _libraryItem.id }}</p>
<p class="mb-1 text-xs text-fg">ID: {{ _libraryItem.id }}</p>
</div>
</div>
</modals-modal>
+17 -27
View File
@@ -2,7 +2,7 @@
<div>
<modals-dialog v-model="show" :items="moreMenuItems" @action="moreMenuAction" />
<modals-item-details-modal v-model="showDetailsModal" :library-item="libraryItem" />
<modals-dialog v-model="showSendEbookDevicesModal" title="Select a device" :items="ereaderDeviceItems" @action="sendEbookToDeviceAction" />
<modals-dialog v-model="showSendEbookDevicesModal" :title="$strings.LabelSelectADevice" :items="ereaderDeviceItems" @action="sendEbookToDeviceAction" />
</div>
</template>
@@ -52,7 +52,7 @@ export default {
// TODO: Implement on iOS
if (this.$platform !== 'ios' && !this.isPodcast) {
items.push({
text: 'History',
text: this.$strings.ButtonHistory,
value: 'history',
icon: 'history'
})
@@ -61,7 +61,7 @@ export default {
if (!this.isPodcast || this.episode) {
if (!this.userIsFinished) {
items.push({
text: 'Mark as Finished',
text: this.$strings.MessageMarkAsFinished,
value: 'markFinished',
icon: 'beenhere'
})
@@ -69,7 +69,7 @@ export default {
if (this.progressPercent > 0) {
items.push({
text: 'Discard Progress',
text: this.$strings.MessageDiscardProgress,
value: 'discardProgress',
icon: 'backspace'
})
@@ -78,14 +78,14 @@ export default {
if ((!this.isPodcast && this.serverLibraryItemId) || (this.episode && this.serverEpisodeId)) {
items.push({
text: 'Add to Playlist',
text: this.$strings.LabelAddToPlaylist,
value: 'playlist',
icon: 'playlist_add'
})
if (this.ereaderDeviceItems.length) {
items.push({
text: 'Send ebook to device',
text: this.$strings.ButtonSendEbookToDevice,
value: 'sendEbook',
icon: 'send'
})
@@ -94,7 +94,7 @@ export default {
if (this.showRSSFeedOption) {
items.push({
text: this.rssFeed ? 'RSS Feed' : 'Open RSS Feed',
text: this.rssFeed ? this.$strings.HeaderRSSFeed : this.$strings.HeaderOpenRSSFeed,
value: 'rssFeed',
icon: 'rss_feed'
})
@@ -102,20 +102,20 @@ export default {
if (this.localLibraryItemId) {
items.push({
text: 'Manage Local Files',
text: this.$strings.ButtonManageLocalFiles,
value: 'manageLocal',
icon: 'folder'
})
if (!this.isPodcast) {
items.push({
text: 'Delete Local Item',
text: this.$strings.ButtonDeleteLocalItem,
value: 'deleteLocal',
icon: 'delete'
})
} else if (this.localEpisodeId) {
items.push({
text: 'Delete Local Episode',
text: this.$strings.ButtonDeleteLocalEpisode,
value: 'deleteLocalEpisode',
icon: 'delete'
})
@@ -124,7 +124,7 @@ export default {
if (!this.episode) {
items.push({
text: 'More Info',
text: this.$strings.LabelMoreInfo,
value: 'details',
icon: 'info'
})
@@ -279,7 +279,7 @@ export default {
if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to mark this item as Finished?'
message: this.$strings.MessageConfirmMarkAsFinished
})
if (!value) return
}
@@ -304,7 +304,7 @@ export default {
}
await this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
}
this.$emit('update:processing', false)
@@ -335,7 +335,7 @@ export default {
}
await this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}/${this.serverEpisodeId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
}
this.$emit('update:processing', false)
@@ -345,7 +345,7 @@ export default {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to reset your progress?'
message: this.$strings.MessageConfirmDiscardProgress
})
if (value) {
this.$emit('update:processing', true)
@@ -381,18 +381,13 @@ export default {
return
}
let confirmMessage = `Remove local episode "${localFile.basePath}" from your device?`
if (this.serverLibraryItemId) {
confirmMessage += ' The file on the server will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
})
if (value) {
const res = await AbsFileSystem.deleteTrackFromItem({ id: this.localLibraryItemId, trackLocalFileId: localFile.id, trackContentUrl: localEpisodeAudioTrack.contentUrl })
if (res?.id) {
this.$toast.success('Deleted episode successfully')
if (this.isLocal) {
// If this is local episode then redirect to server episode when available
if (this.serverEpisodeId) {
@@ -411,18 +406,13 @@ export default {
async deleteLocalItem() {
await this.$hapticsImpact()
let confirmMessage = 'Remove local files of this item from your device?'
if (this.serverLibraryItemId) {
confirmMessage += ' The files on the server and your progress will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
message: this.$strings.MessageConfirmDeleteLocalFiles
})
if (value) {
const res = await AbsFileSystem.deleteItem(this.localLibraryItem)
if (res?.success) {
this.$toast.success('Deleted successfully')
if (this.isLocal) {
// If local then redirect to server version when available
if (this.serverLibraryItemId) {
+3 -3
View File
@@ -2,15 +2,15 @@
<modals-modal v-model="show" :width="300" :processing="processing" height="100%">
<template #outer>
<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">{{ $strings.HeaderLibraries }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="library in libraries">
<li :key="library.id" class="text-gray-50 select-none relative py-3 cursor-pointer" :class="currentLibraryId === library.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(library)">
<li :key="library.id" class="text-fg select-none relative py-3 cursor-pointer" :class="currentLibraryId === library.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(library)">
<div v-show="currentLibraryId === library.id" class="absolute top-0 left-0 w-0.5 bg-warning h-full" />
<div class="flex items-center px-3">
<ui-library-icon :icon="library.icon" />
+1 -1
View File
@@ -6,7 +6,7 @@
<span class="material-icons text-4xl">close</span>
</div>
<slot name="outer" />
<div ref="content" style="min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth, maxWidth: maxWidth }" v-click-outside="clickBg">
<div ref="content" style="min-height: 200px" class="relative text-fg max-h-screen" :style="{ height: modalHeight, width: modalWidth, maxWidth: maxWidth }" v-click-outside="clickBg">
<slot />
</div>
</div>
+21 -21
View File
@@ -1,9 +1,9 @@
<template>
<modals-modal v-model="show" width="90%">
<div class="w-full h-full bg-primary rounded-lg border border-white border-opacity-20">
<div class="w-full h-full bg-primary rounded-lg border border-fg/20">
<ul class="w-full rounded-lg text-base" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate text-lg">{{ item.text }}</span>
</div>
@@ -29,83 +29,83 @@ export default {
return {
bookItems: [
{
text: 'Title',
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{
text: 'Author (First Last)',
text: this.$strings.LabelAuthorFirstLast,
value: 'media.metadata.authorName'
},
{
text: 'Author (Last, First)',
text: this.$strings.LabelAuthorLastFirst,
value: 'media.metadata.authorNameLF'
},
{
text: 'Published Year',
text: this.$strings.LabelPublishYear,
value: 'media.metadata.publishedYear'
},
{
text: 'Added At',
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: 'Size',
text: this.$strings.LabelSize,
value: 'size'
},
{
text: 'Duration',
text: this.$strings.LabelDuration,
value: 'media.duration'
},
{
text: 'File Birthtime',
text: this.$strings.LabelFileBirthtime,
value: 'birthtimeMs'
},
{
text: 'File Modified',
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
}
],
podcastItems: [
{
text: 'Title',
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{
text: 'Author',
text: this.$strings.LabelAuthor,
value: 'media.metadata.author'
},
{
text: 'Added At',
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: 'Size',
text: this.$strings.LabelSize,
value: 'size'
},
{
text: 'File Birthtime',
text: this.$strings.LabelFileBirthtime,
value: 'birthtimeMs'
},
{
text: 'File Modified',
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
}
],
episodeItems: [
{
text: 'Pub Date',
text: this.$strings.LabelPubDate,
value: 'publishedAt'
},
{
text: 'Title',
text: this.$strings.LabelTitle,
value: 'title'
},
{
text: 'Season',
text: this.$strings.LabelSeason,
value: 'season'
},
{
text: 'Episode',
text: this.$strings.LabelEpisode,
value: 'episode'
}
]
+6 -6
View File
@@ -2,29 +2,29 @@
<modals-modal v-model="show" @input="modalInput" :width="200" height="100%">
<template #outer>
<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">{{ $strings.LabelPlaybackSpeed }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<ul class="w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="rate in rates">
<li :key="rate" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(rate)">
<li :key="rate" class="text-fg select-none relative py-4" :class="rate === selected ? 'bg-bg-hover/50' : ''" role="option" @click="clickedOption(rate)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ rate }}x</span>
</div>
</li>
</template>
</ul>
<div class="flex items-center justify-center py-3 border-t border-white border-opacity-10">
<button :disabled="!canDecrement" @click="decrement" class="icon-num-btn w-8 h-8 text-white text-opacity-75 rounded border border-white border-opacity-20 flex items-center justify-center">
<div class="flex items-center justify-center py-3 border-t border-fg/10">
<button :disabled="!canDecrement" @click="decrement" class="icon-num-btn w-8 h-8 text-fg-muted rounded border border-border flex items-center justify-center">
<span class="material-icons">remove</span>
</button>
<div class="w-24 text-center">
<p class="text-xl">{{ playbackRate }}<span class="text-lg"></span></p>
</div>
<button :disabled="!canIncrement" @click="increment" class="icon-num-btn w-8 h-8 text-white text-opacity-75 rounded border border-white border-opacity-20 flex items-center justify-center">
<button :disabled="!canIncrement" @click="increment" class="icon-num-btn w-8 h-8 text-fg-muted rounded border border-border flex items-center justify-center">
<span class="material-icons">add</span>
</button>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" :width="300" height="100%">
<template #outer>
<div class="absolute top-10 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">Select Download Location</p>
<p class="text-white text-lg truncate">{{ $strings.HeaderSelectDownloadLocation }}</p>
</div>
</template>
@@ -71,7 +71,7 @@ export default {
if (!localFolders.some((lf) => lf.id === `internal-${this.mediaType}`)) {
localFolders.push({
id: `internal-${this.mediaType}`,
name: 'Internal App Storage',
name: this.$strings.LabelInternalAppStorage,
mediaType: this.mediaType
})
}
+7 -7
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<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">{{ $strings.HeaderSleepTimer }}</p>
</div>
</template>
@@ -23,24 +23,24 @@
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
<li :key="timeout" class="text-fg select-none relative py-4" role="option" @click="clickedOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ timeout }} min</span>
</div>
</li>
</template>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedChapterOption">
<li class="text-fg select-none relative py-4" role="option" @click="clickedChapterOption">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">End of Chapter</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelEndOfChapter }}</span>
</div>
</li>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<li class="text-fg select-none relative py-4" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelCustomTime }}</span>
</div>
</li>
</ul>
+9 -9
View File
@@ -2,7 +2,7 @@
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<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">{{ $strings.HeaderSleepTimer }}</p>
</div>
</template>
@@ -13,7 +13,7 @@
manualTimerModal = false
"
>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<div v-if="manualTimerModal" class="p-4">
<div class="flex mb-4" @click="manualTimerModal = false">
<span class="material-icons text-3xl">arrow_back</span>
@@ -23,24 +23,24 @@
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
<ul v-else-if="!sleepTimerRunning" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
<li :key="timeout" class="text-fg select-none relative py-4" role="option" @click="clickedOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ timeout }} min</span>
</div>
</li>
</template>
<li v-if="currentEndOfChapterTime" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedChapterOption(timeout)">
<li v-if="currentEndOfChapterTime" class="text-fg select-none relative py-4" role="option" @click="clickedChapterOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">End of Chapter</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelEndOfChapter }}</span>
</div>
</li>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<li class="text-fg select-none relative py-4" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
<span class="font-normal block truncate text-lg text-center">{{ $strings.LabelCustomTime }}</span>
</div>
</li>
</ul>
@@ -51,7 +51,7 @@
<ui-btn @click="increaseSleepTime" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="cancelSleepTimer" class="w-full">{{ isAuto ? 'Disable Auto Timer' : 'Cancel Timer' }}</ui-btn>
<ui-btn @click="cancelSleepTimer" class="w-full">{{ isAuto ? $strings.ButtonDisableAutoTimer : $strings.ButtonCancelTimer }}</ui-btn>
</div>
</div>
</div>
+5 -5
View File
@@ -1,17 +1,17 @@
<template>
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-1 py-4 justify-start cursor-pointer hover:bg-bg relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-1 py-4 justify-start relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
<div class="flex-grow overflow-hidden px-2">
<div class="flex items-center mb-0.5">
<i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-white/60'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i>
<i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-fg-muted'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i>
<p class="truncate text-sm">
{{ bookmark.title }}
</p>
</div>
<p class="text-sm font-mono text-gray-300 flex items-center"><span class="material-icons text-base text-white/60 pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time) }}</p>
<p class="text-sm font-mono text-fg-muted flex items-center"><span class="material-icons text-base pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time) }}</p>
</div>
<div class="h-full flex items-center justify-end transform w-16 pr-2" @click.stop>
<span class="material-icons text-2xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
<span class="material-icons text-2xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
<span class="material-icons text-2xl mr-2 text-fg hover:text-yellow-400" @click.stop="editClick">edit</span>
<span class="material-icons text-2xl text-fg hover:text-error" @click.stop="deleteClick">delete</span>
</div>
</div>
</template>
+13 -14
View File
@@ -1,46 +1,46 @@
<template>
<modals-fullscreen-modal v-model="show" :processing="processing">
<div class="flex items-end justify-between h-16 px-4 pb-2">
<h1 class="text-lg">Add to Playlist</h1>
<div class="flex items-end justify-between h-24 px-4 pb-2">
<h1 class="text-lg">{{ $strings.LabelAddToPlaylist }}</h1>
<button class="flex" @click="show = false">
<span class="material-icons">close</span>
</button>
</div>
<!-- create new playlist form -->
<div v-if="showPlaylistNameInput" class="w-full h-full max-h-[calc(100vh-128px)] flex items-center">
<div v-if="showPlaylistNameInput" class="w-full h-full max-h-[calc(100vh-176px)] flex items-center">
<div class="w-full px-4">
<div class="flex mb-4 items-center">
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showPlaylistNameInput = false">
<div class="w-9 h-9 flex items-center justify-center rounded-full cursor-pointer" @click.stop="showPlaylistNameInput = false">
<span class="material-icons text-3xl">arrow_back</span>
</div>
<p class="text-xl pl-2">New Playlist</p>
<p class="text-xl pl-2 leading-none">{{ $strings.HeaderNewPlaylist }}</p>
<div class="flex-grow" />
</div>
<ui-text-input-with-label v-model="newPlaylistName" label="Name" />
<ui-text-input-with-label v-model="newPlaylistName" :label="$strings.LabelName" />
<div class="flex justify-end mt-6">
<ui-btn color="success" :loading="processing" class="w-full" @click.stop="submitCreatePlaylist">Create</ui-btn>
<ui-btn color="success" :loading="processing" class="w-full" @click.stop="submitCreatePlaylist">{{ $strings.ButtonCreate }}</ui-btn>
</div>
</div>
</div>
<!-- playlists list -->
<div v-if="!showPlaylistNameInput" class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-128px)]">
<div v-if="!showPlaylistNameInput" class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-176px)]">
<div class="w-full h-full" v-show="!showPlaylistNameInput">
<template v-for="playlist in sortedPlaylists">
<modals-playlists-playlist-row :key="playlist.id" :in-playlist="playlist.isItemIncluded" :playlist="playlist" @click="clickPlaylist" @close="show = false" />
</template>
<div v-if="!playlists.length" class="flex h-full items-center justify-center">
<p class="text-xl">{{ loading ? 'Loading..' : 'No Playlists' }}</p>
<p class="text-xl">{{ loading ? $strings.MessageLoading : $strings.MessageNoUserPlaylists }}</p>
</div>
</div>
</div>
<!-- create playlist btn -->
<div v-if="!showPlaylistNameInput" class="flex items-start justify-between h-16 pt-2 absolute bottom-0 left-0 w-full">
<ui-btn :loading="processing" color="success" class="w-full h-full flex items-center justify-center" @click.stop="createPlaylist">
<p class="text-base">Create New Playlist</p>
<div v-if="!showPlaylistNameInput" class="flex items-start justify-between h-20 pt-2 absolute bottom-0 left-0 w-full">
<ui-btn :loading="processing" color="success" class="w-full h-14 flex items-center justify-center" @click.stop="createPlaylist">
<p class="text-base">{{ $strings.ButtonCreateNewPlaylist }}</p>
</ui-btn>
</div>
</modals-fullscreen-modal>
@@ -193,8 +193,7 @@ export default {
})
.catch((error) => {
console.error('Failed to create playlist', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(`Failed to create playlist: ${errMsg}`)
this.$toast.error(this.$strings.ToastPlaylistCreateFailed)
})
.finally(() => {
this.processing = false
+2 -2
View File
@@ -8,8 +8,8 @@
<p class="px-2 truncate text-sm">{{ playlist.name }}</p>
</div>
<div class="w-24 min-w-[96px] px-1">
<ui-btn v-if="inPlaylist" small class="w-full" @click.stop="click">Remove</ui-btn>
<ui-btn v-else small class="w-full" @click.stop="click">Add</ui-btn>
<ui-btn v-if="inPlaylist" small class="w-full" @click.stop="click">{{ $strings.ButtonRemove }}</ui-btn>
<ui-btn v-else small class="w-full" @click.stop="click">{{ $strings.ButtonAdd }}</ui-btn>
</div>
</div>
</template>
@@ -1,22 +1,22 @@
<template>
<div class="w-full py-2 text-sm">
<div class="flex -mb-px">
<div class="w-1/2 h-6 rounded-tl-md relative border border-white/10 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg border-b-bg' : 'text-gray-400 bg-primary bg-opacity-70'" @click="showAdvancedView = false">
<div class="w-1/2 h-6 rounded-tl-md relative border border-fg/10 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-fg bg-bg border-b-bg' : 'text-fg-muted bg-primary bg-opacity-70'" @click="showAdvancedView = false">
<p class="text-sm">{{ $strings.HeaderRSSFeedGeneral }}</p>
</div>
<div class="w-1/2 h-6 rounded-tr-md relative border border-white/10 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg border-b-bg' : 'text-gray-400 bg-primary bg-opacity-70'" @click="showAdvancedView = true">
<div class="w-1/2 h-6 rounded-tr-md relative border border-fg/10 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-fg bg-bg border-b-bg' : 'text-fg-muted bg-primary bg-opacity-70'" @click="showAdvancedView = true">
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
</div>
</div>
<div class="px-2 py-4 md:p-4 border border-white/10 rounded-b-md mr-px" style="min-height: 220px">
<div class="px-2 py-4 md:p-4 border border-fg/10 rounded-b-md mr-px" style="min-height: 220px">
<template v-if="!showAdvancedView">
<div class="flex-grow pt-2 mb-2">
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="border" label-class="pl-2" />
</div>
</template>
<template v-else>
<div class="flex-grow pt-2 mb-2">
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="border" label-class="pl-2" />
</div>
<div class="w-full relative mb-1">
<ui-text-input-with-label v-model="ownerName" :label="$strings.LabelRSSFeedCustomOwnerName" />
+12 -13
View File
@@ -1,38 +1,38 @@
<template>
<modals-fullscreen-modal v-model="show" :processing="processing">
<div class="flex items-end justify-end h-16 pr-4 pb-2 mb-2">
<div class="flex items-end justify-end h-24 pr-4 pb-2">
<!-- <h1 class="text-lg">RSS Feed</h1> -->
<button class="flex" @click="show = false">
<span class="material-icons">close</span>
</button>
</div>
<div class="w-full px-2 h-[calc(100%-138px)] overflow-y-auto">
<div class="w-full px-2 h-[calc(100%-176px)] overflow-y-auto">
<div v-if="currentFeed" class="w-full">
<div class="w-full relative">
<h1 class="text-lg mb-4">RSS feed is open</h1>
<h1 class="text-lg mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</h1>
<ui-text-input v-model="currentFeed.feedUrl" class="text-sm" readonly />
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base" :class="linkCopied ? 'text-success' : 'text-gray-300'" @click="copyToClipboard(currentFeed.feedUrl)">{{ linkCopied ? 'done' : 'content_copy' }}</span>
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base" :class="linkCopied ? 'text-success' : 'text-fg-muted'" @click="copyToClipboard(currentFeed.feedUrl)">{{ linkCopied ? 'done' : 'content_copy' }}</span>
</div>
<div v-if="currentFeed.meta" class="mt-5">
<div class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
<span class="text-fg-muted uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
</div>
<div>{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
<div>{{ currentFeed.meta.preventIndexing ? $strings.ButtonYes : $strings.LabelNo }}</div>
</div>
<div v-if="currentFeed.meta.ownerName" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
<span class="text-fg-muted uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
</div>
<div>{{ currentFeed.meta.ownerName }}</div>
</div>
<div v-if="currentFeed.meta.ownerEmail" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
<span class="text-fg-muted uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
</div>
<div>{{ currentFeed.meta.ownerEmail }}</div>
</div>
@@ -41,7 +41,7 @@
<div v-else class="w-full">
<div class="w-full relative mb-2">
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
<p class="text-xs text-fg-muted py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
</div>
<modals-rssfeeds-rss-feed-metadata-builder v-model="metadataDetails" />
@@ -50,10 +50,9 @@
</div>
</div>
<div v-show="userIsAdminOrUp" class="flex items-center px-2 h-16">
<div class="flex-grow" />
<ui-btn v-if="currentFeed" color="error" class="w-full" @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
<ui-btn v-else color="success" class="w-full" @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
<div v-show="userIsAdminOrUp" class="flex items-start pt-2 h-20">
<ui-btn v-if="currentFeed" color="error" class="w-full h-14" @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
<ui-btn v-else color="success" class="w-full h-14" @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
</div>
</modals-fullscreen-modal>
</template>
+2 -2
View File
@@ -2,7 +2,7 @@
<div id="comic-reader" class="w-full h-full relative">
<modals-modal v-model="showInfoMenu" height="90%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click.stop="showInfoMenu = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div class="w-full overflow-x-hidden overflow-y-auto bg-bg rounded-lg border border-border text-fg" style="max-height: 75%" @click.stop>
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
<p class="text-xs">
<strong>{{ key }}</strong>
@@ -23,7 +23,7 @@
</div>
</div>
<div class="fixed left-0 h-8 w-full bg-primary px-4 flex items-center text-white/80" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="fixed left-0 h-8 w-full bg-bg px-4 flex items-center text-fg-muted" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="flex-grow" />
<p class="text-xs">{{ page }} / {{ numPages }}</p>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<div id="epub-frame" class="w-full">
<div id="viewer" class="h-full w-full"></div>
<div class="fixed left-0 h-8 w-full px-4 flex items-center" :class="isLightTheme ? 'bg-white text-black' : 'bg-primary text-white/80'" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="fixed left-0 h-8 w-full px-4 flex items-center" :class="isLightTheme ? 'bg-white text-black' : 'bg-[#232323] text-white/80'" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<p v-if="totalLocations" class="text-xs text-slate-600">Location {{ currentLocationNum }} of {{ totalLocations }}</p>
<div class="flex-grow" />
<p class="text-xs">{{ progress }}%</p>
+1 -1
View File
@@ -18,7 +18,7 @@
</div>
</div>
<div class="fixed left-0 h-8 w-full bg-primary px-4 flex items-center text-white/80" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="fixed left-0 h-8 w-full bg-bg px-4 flex items-center text-fg-muted" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
<div class="flex-grow" />
<p class="text-xs">{{ page }} / {{ numPages }}</p>
</div>
+35 -33
View File
@@ -1,20 +1,20 @@
<template>
<div v-if="show" :data-theme="ereaderTheme" class="group fixed top-0 left-0 right-0 layout-wrapper w-full z-40 pt-8 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': isPlayerOpen }">
<div v-if="show" :data-theme="ereaderTheme" class="group fixed top-0 left-0 right-0 layout-wrapper w-full z-40 pt-8 data-[theme=dark]:bg-[#232323] data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': isPlayerOpen }">
<!-- toolbar -->
<div class="h-32 pt-10 w-full px-2 fixed top-0 left-0 z-30 transition-transform bg-bg text-white" :class="showingToolbar ? 'translate-y-0' : '-translate-y-32'" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop>
<div class="h-32 pt-10 w-full px-2 fixed top-0 left-0 z-30 transition-transform bg-bg text-fg" :class="showingToolbar ? 'translate-y-0' : '-translate-y-32'" :style="{ boxShadow: showingToolbar ? '0px 8px 8px #11111155' : '' }" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop>
<div class="flex items-center mb-2">
<button type="button" class="inline-flex mx-2" @click.stop="show = false">
<span class="material-icons-outlined text-3xl text-white">chevron_left</span>
<span class="material-icons-outlined text-3xl text-fg">chevron_left</span>
</button>
<div class="flex-grow" />
<button v-if="isComic || isEpub" type="button" class="inline-flex mx-2" @click.stop="clickTOCBtn">
<span class="material-icons-outlined text-2xl text-white">format_list_bulleted</span>
<span class="material-icons-outlined text-2xl text-fg">format_list_bulleted</span>
</button>
<button v-if="isEpub" type="button" class="inline-flex mx-2" @click.stop="clickSettingsBtn">
<span class="material-icons text-2xl text-white">settings</span>
<span class="material-icons text-2xl text-fg">settings</span>
</button>
<button v-if="comicHasMetadata" type="button" class="inline-flex mx-2" @click.stop="clickMetadataBtn">
<span class="material-icons text-2xl text-white">more</span>
<span class="material-icons text-2xl text-fg">more</span>
</button>
</div>
@@ -27,7 +27,7 @@
<!-- table of contents modal -->
<modals-fullscreen-modal v-model="showTOCModal" :theme="ereaderTheme">
<div class="flex items-end justify-between h-20 px-4 pb-2">
<h1 class="text-lg">Table of Contents</h1>
<h1 class="text-lg">{{ $strings.HeaderTableOfContents }}</h1>
<button class="flex" @click.stop="showTOCModal = false">
<span class="material-icons">close</span>
</button>
@@ -47,7 +47,7 @@
</li>
</ul>
<div v-if="!chapters.length" class="flex h-full items-center justify-center">
<p class="text-xl">{{ 'No Chapters' }}</p>
<p class="text-xl">{{ $strings.MessageNoChapters }}</p>
</div>
</div>
</div>
@@ -55,31 +55,33 @@
<!-- ereader settings modal -->
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen>
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-8">
<h1 class="text-lg">Ereader Settings</h1>
<button class="flex" @click="showSettingsModal = false">
<span class="material-icons">close</span>
</button>
</div>
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
<div class="w-full h-full px-4">
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">Theme:</p>
<div style="box-shadow: 0px -8px 8px #11111155">
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-8">
<h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1>
<button class="flex" @click="showSettingsModal = false">
<span class="material-icons">close</span>
</button>
</div>
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
<div class="w-full h-full px-4">
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">{{ $strings.LabelTheme }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
</div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">Font scale:</p>
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">{{ $strings.LabelFontScale }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">Line spacing:</p>
<div class="flex items-center mb-8">
<div class="w-32">
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
</div>
</div>
@@ -150,16 +152,16 @@ export default {
},
ereaderTheme() {
if (this.isEpub) return this.ereaderSettings.theme
return 'dark'
return document.documentElement.dataset.theme || 'dark'
},
themeItems() {
return [
{
text: 'Dark',
text: this.$strings.LabelThemeDark,
value: 'dark'
},
{
text: 'Light',
text: this.$strings.LabelThemeLight,
value: 'light'
}
]
+9 -9
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
<div class="relative w-96 h-72">
<div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels">
@@ -32,24 +32,24 @@
</div>
<div class="flex justify-between pt-12">
<div>
<p class="text-sm text-center">Week Listening</p>
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Daily Average</p>
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Best Day</p>
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
<p class="text-sm text-center">minutes</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">Days</p>
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
<p class="text-sm text-center">in a row</p>
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
</div>
</div>
</div>
+4 -4
View File
@@ -1,8 +1,8 @@
<template>
<div class="w-full my-4">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="expanded ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">Chapters</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<p class="pr-2">{{ $strings.HeaderChapters }}</p>
<div class="h-6 w-6 rounded-full bg-fg/10 flex items-center justify-center">
<span class="text-xs font-mono">{{ chapters.length }}</span>
</div>
<div class="flex-grow" />
@@ -13,8 +13,8 @@
<transition name="slide">
<table class="text-xs tracksTable" v-show="expanded">
<tr>
<th class="text-left">Title</th>
<th class="text-center w-16">Start</th>
<th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center w-16">{{ $strings.LabelStart }}</th>
</tr>
<tr v-for="chapter in chapters" :key="chapter.id">
<td>
+4 -8
View File
@@ -1,8 +1,8 @@
<template>
<div class="w-full my-4">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="showTracks ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">{{ title }}</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<p class="pr-2">{{ $strings.HeaderAudioTracks }}</p>
<div class="h-6 w-6 rounded-full bg-fg/10 flex items-center justify-center">
<span class="text-xs font-mono">{{ tracks.length }}</span>
</div>
<div class="flex-grow" />
@@ -14,8 +14,8 @@
<div class="w-full" v-show="showTracks">
<table class="text-xs tracksTable">
<tr>
<th class="text-left">Filename</th>
<th class="text-center w-16">Duration</th>
<th class="text-left">{{ $strings.LabelFilename }}</th>
<th class="text-center w-16">{{ $strings.LabelDuration }}</th>
</tr>
<template v-for="track in tracks">
<tr :key="track.index">
@@ -34,10 +34,6 @@
<script>
export default {
props: {
title: {
type: String,
default: 'Audio Tracks'
},
tracks: {
type: Array,
default: () => []
@@ -7,8 +7,8 @@
<div class="book-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ bookTitle }}</p>
<p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p>
<p v-if="media.duration" class="text-xxs text-gray-500">{{ bookDuration }}</p>
<p class="truncate block text-fg-muted text-xs">{{ bookAuthor }}</p>
<p v-if="media.duration" class="text-xxs text-fg-muted">{{ bookDuration }}</p>
</div>
</div>
<div class="w-8 min-w-8 flex justify-center">
@@ -1,14 +1,14 @@
<template>
<div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 bg-primary">
<p class="pr-4">Collection List</p>
<p class="pr-4">{{ $strings.HeaderCollectionItems }}</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<div class="w-6 h-6 md:w-7 md:h-7 bg-fg/10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
</div>
<div class="flex-grow" />
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
<p v-if="totalDuration" class="text-sm text-fg">{{ totalDurationPretty }}</p>
</div>
<template v-for="book in booksCopy">
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item collection-book-item" @edit="editBook" />
+4 -4
View File
@@ -1,8 +1,8 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 py-2 flex items-center" :class="showFiles ? 'rounded-t-md' : 'rounded-md'" @click.stop="clickBar">
<p class="pr-2">Ebook Files</p>
<div class="h-6 w-6 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<p class="pr-2">{{ $strings.HeaderEbookFiles }}</p>
<div class="h-6 w-6 rounded-full bg-fg/10 flex items-center justify-center">
<span class="text-xs font-mono">{{ ebookFiles.length }}</span>
</div>
<div class="flex-grow" />
@@ -14,8 +14,8 @@
<div class="w-full" v-show="showFiles">
<table class="text-sm tracksTable">
<tr>
<th class="text-left px-4">Filename</th>
<th class="text-left px-4 w-16">Read</th>
<th class="text-left px-4">{{ $strings.LabelFilename }}</th>
<th class="text-left px-4 w-16">{{ $strings.LabelRead }}</th>
<th v-if="userCanUpdate && !libraryIsAudiobooksOnly" class="text-center w-16"></th>
</tr>
<template v-for="file in ebookFiles">
@@ -40,7 +40,7 @@ export default {
const items = []
if (this.userCanUpdate && !this.libraryIsAudiobooksOnly) {
items.push({
text: this.isPrimary ? 'Set as supplementary' : 'Set as primary',
text: this.isPrimary ? this.$strings.LabelSetEbookAsSupplementary : this.$strings.LabelSetEbookAsPrimary,
value: 'updateStatus'
})
}
+23 -4
View File
@@ -8,13 +8,16 @@
<div class="item-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ itemTitle }} <span v-if="localLibraryItem" class="material-icons text-success text-base align-text-bottom">download_done</span></p>
<p v-if="authorName" class="truncate block text-gray-300 text-xs">{{ authorName }}</p>
<p class="text-xxs text-gray-400">{{ itemDuration }}</p>
<p v-if="authorName" class="truncate block text-fg-muted text-xs">{{ authorName }}</p>
<p class="text-xxs text-fg-muted">{{ itemDuration }}</p>
</div>
</div>
<div class="w-8 min-w-8 flex justify-center">
<button v-if="showPlayBtn" class="w-8 h-8 rounded-full border border-white border-opacity-20 flex items-center justify-center" @click.stop.prevent="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<button v-if="showPlayBtn" class="w-8 h-8 rounded-full border border-white/20 flex items-center justify-center" @click.stop.prevent="playClick">
<span v-if="!playerIsStartingForThisMedia" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<svg v-else class="animate-spin" style="width: 18px; height: 18px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</button>
</div>
<div class="w-8 min-w-8 flex justify-center">
@@ -117,6 +120,17 @@ export default {
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.$store.state.playerStartingPlaybackMediaId
if (!mediaId) return false
let thisMediaId = this.episodeId || this.libraryItem.id
return mediaId === thisMediaId
},
userItemProgress() {
return this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, this.episodeId)
},
@@ -142,10 +156,14 @@ export default {
this.$emit('showMore', playlistItem)
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
let mediaId = this.episodeId || this.libraryItem.id
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else if (this.localLibraryItem) {
this.$store.commit('setPlayerIsStartingPlayback', mediaId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItem.id,
episodeId: this.localEpisode?.id,
@@ -153,6 +171,7 @@ export default {
serverEpisodeId: this.episodeId
})
} else {
this.$store.commit('setPlayerIsStartingPlayback', mediaId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItem.id,
episodeId: this.episodeId
@@ -1,14 +1,14 @@
<template>
<div class="w-full bg-primary/50 rounded-lg">
<div class="w-full h-14 flex items-center px-3">
<p class="pr-2">Playlist Items</p>
<p class="pr-2">{{ $strings.HeaderPlaylistItems }}</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<div class="w-6 h-6 md:w-7 md:h-7 bg-fg bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
</div>
<div class="flex-grow" />
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
<p v-if="totalDuration" class="text-sm text-fg">{{ totalDurationPretty }}</p>
</div>
<template v-for="item in items">
<tables-playlist-item-table-row :key="item.id" :item="item" :playlist-id="playlistId" @showMore="showMore" />
+37 -25
View File
@@ -9,27 +9,26 @@
<p v-else>No Server Media Progress</p>
<p v-if="localMediaProgress">Local Media Progress {{ Math.round(localMediaProgress.progress * 100) }}</p>
<p v-else>No Local Media Progress</p>
</template> -->
</template>-->
<p v-if="publishedAt" class="text-xs text-gray-400 mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p v-if="publishedAt" class="text-xs text-fg-muted mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p class="text-sm font-semibold">
{{ title }}
</p>
<p class="text-sm font-semibold">{{ title }}</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<p class="text-sm text-fg episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<div v-if="episodeNumber || season || episodeType" class="flex py-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg capitalize">{{ episodeType }}</div>
</div>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<div class="h-8 px-4 border border-border rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span v-if="!playerIsStartingForThisMedia" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<svg v-else class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</div>
@@ -41,7 +40,7 @@
<div v-if="userCanDownload">
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="(downloadItem || startingDownload) ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div>
@@ -58,7 +57,6 @@
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
export default {
@@ -78,7 +76,8 @@ export default {
data() {
return {
isProcessingReadUpdate: false,
processing: false
processing: false,
startingDownload: false
}
},
computed: {
@@ -119,6 +118,16 @@ export default {
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.$store.state.playerStartingPlaybackMediaId
if (!mediaId) return false
return mediaId === this.episode?.id
},
itemProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
@@ -169,7 +178,13 @@ export default {
return folderObj
},
async downloadClick() {
if (this.downloadItem) return
if (this.downloadItem || this.startingDownload) return
this.startingDownload = true
setTimeout(() => {
this.startingDownload = false
}, 1000)
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
@@ -210,14 +225,7 @@ export default {
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
this.startDownload(localFolder)
},
async startDownload(localFolder) {
var payload = {
@@ -235,10 +243,14 @@ export default {
}
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
if (this.localEpisode && this.localLibraryItemId) {
console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId)
+54 -50
View File
@@ -1,40 +1,40 @@
<template>
<div class="w-full">
<!-- Podcast episode downloads queue -->
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 my-2 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative w-full">
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 my-2 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-fg relative w-full">
<div class="flex items-center">
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode(s) queued for download</p>
<p class="text-sm py-1">{{ $getString('MessageEpisodesQueuedForDownload', [episodeDownloadsQueued.length]) }}</p>
<div class="flex-grow" />
<span v-if="isAdminOrUp" class="material-icons text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
</div>
</div>
<!-- Podcast episodes currently downloading -->
<div v-if="episodesDownloading.length" class="px-4 py-2 my-2 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative w-full">
<div v-if="episodesDownloading.length" class="px-4 py-2 my-2 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-fg relative w-full">
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
<widgets-loading-spinner />
<p class="text-sm py-1 pl-4">Downloading episode "{{ episode.episodeDisplayTitle }}"</p>
<p class="text-sm py-1 pl-4">{{ $strings.MessageDownloadingEpisode }} "{{ episode.episodeDisplayTitle }}"</p>
</div>
</div>
<div class="flex items-center">
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodesFiltered.length }})</p>
<p class="text-lg mb-1 font-semibold">{{ $strings.HeaderEpisodes }} ({{ episodesFiltered.length }})</p>
<div class="flex-grow" />
<button v-if="isAdminOrUp && !fetchingRSSFeed" class="outline:none mx-1 pt-0.5 relative" @click="searchEpisodes">
<span class="material-icons text-xl text-gray-200">search</span>
<span class="material-icons text-xl text-fg">search</span>
</button>
<widgets-loading-spinner v-else-if="fetchingRSSFeed" class="mx-1" />
<button class="outline:none mx-3 pt-0.5 relative" @click="showFilters">
<span class="material-icons text-xl text-gray-200">filter_alt</span>
<span class="material-icons text-xl text-fg">filter_alt</span>
<div v-show="filterKey !== 'all' && episodesAreFiltered" class="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
</button>
<div class="flex items-center border border-white border-opacity-25 rounded px-2" @click="clickSort">
<p class="text-sm text-gray-200">{{ sortText }}</p>
<span class="material-icons ml-1 text-gray-200">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
<p class="text-sm text-fg">{{ sortText }}</p>
<span class="material-icons ml-1 text-fg">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</div>
@@ -82,46 +82,6 @@ export default {
sortKey: 'publishedAt',
sortDesc: true,
filterKey: 'incomplete',
episodeSortItems: [
{
text: 'Pub Date',
value: 'publishedAt'
},
{
text: 'Title',
value: 'title'
},
{
text: 'Season',
value: 'season'
},
{
text: 'Episode',
value: 'episode'
}
],
filterItems: [
{
text: 'Show All',
value: 'all'
},
{
text: 'Incomplete',
value: 'incomplete'
},
{
text: 'In Progress',
value: 'inProgress'
},
{
text: 'Complete',
value: 'complete'
},
{
text: 'Downloaded',
value: 'downloaded'
}
],
fetchingRSSFeed: false,
podcastFeedEpisodes: [],
showPodcastEpisodeFeed: false,
@@ -156,6 +116,50 @@ export default {
episodesAreFiltered() {
return this.episodesFiltered.length !== this.episodesCopy.length
},
episodeSortItems() {
return [
{
text: this.$strings.LabelPubDate,
value: 'publishedAt'
},
{
text: this.$strings.LabelTitle,
value: 'title'
},
{
text: this.$strings.LabelSeason,
value: 'season'
},
{
text: this.$strings.LabelEpisode,
value: 'episode'
}
]
},
filterItems() {
return [
{
text: this.$strings.LabelShowAll,
value: 'all'
},
{
text: this.$strings.LabelIncomplete,
value: 'incomplete'
},
{
text: this.$strings.LabelInProgress,
value: 'inProgress'
},
{
text: this.$strings.LabelComplete,
value: 'complete'
},
{
text: this.$strings.LabelDownloaded,
value: 'downloaded'
}
]
},
episodesFiltered() {
return this.episodesCopy.filter((ep) => {
if (this.filterKey === 'downloaded') {
@@ -230,7 +234,7 @@ export default {
},
async searchEpisodes() {
if (!this.networkConnected) {
return this.$toast.error('No network connection')
return this.$toast.error(this.$strings.MessageNoNetworkConnection)
}
if (!this.mediaMetadata.feedUrl) {
+42 -30
View File
@@ -1,5 +1,5 @@
<template>
<div class="w-full py-4 overflow-hidden relative border-b border-white border-opacity-10" @click.stop="goToEpisodePage">
<div class="w-full py-4 overflow-hidden relative border-b border-border" @click.stop="goToEpisodePage">
<div v-if="episode" class="w-full px-1">
<div class="flex mb-2">
<div class="w-10 min-w-10">
@@ -7,29 +7,28 @@
</div>
<div class="flex-grow px-2">
<div class="-mt-0.5 mb-0.5" @click.stop>
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-gray-200 underline">{{ podcast.metadata.title }}</nuxt-link>
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-fg underline">{{ podcast.metadata.title }}</nuxt-link>
</div>
<p v-if="publishedAt" class="text-xs text-gray-300">{{ $dateDistanceFromNow(publishedAt) }}</p>
<p v-if="publishedAt" class="text-xs text-fg-muted">{{ $dateDistanceFromNow(publishedAt) }}</p>
</div>
</div>
<p class="text-sm font-semibold">
{{ title }}
</p>
<p class="text-sm font-semibold">{{ title }}</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<p class="text-sm text-fg episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<div v-if="episodeNumber || season || episodeType" class="flex pt-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">{{ $strings.LabelEpisode }} #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg">{{ $strings.LabelSeason }} #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-fg capitalize">{{ episodeType }}</div>
</div>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<div class="h-8 px-4 border border-border hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-fg text-opacity-40' : ''" @click.stop="playClick">
<span v-if="!playerIsStartingForThisMedia" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<svg v-else class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</div>
@@ -41,7 +40,7 @@
<div v-if="userCanDownload">
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem || pendingDownload ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem || pendingDownload ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div>
@@ -58,7 +57,6 @@
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
export default {
@@ -78,6 +76,7 @@ export default {
data() {
return {
isProcessingReadUpdate: false,
pendingDownload: false,
processing: false
}
},
@@ -122,6 +121,15 @@ export default {
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
if (!this.episode?.id) return false
const mediaId = this.$store.state.playerStartingPlaybackMediaId
return mediaId === this.episode.id
},
itemProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
@@ -135,10 +143,10 @@ export default {
}
},
itemProgressPercent() {
return this.itemProgress ? this.itemProgress.progress : 0
return this.itemProgress?.progress || 0
},
userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false
return !!this.itemProgress?.isFinished
},
timeRemaining() {
if (this.streamIsPlaying) return 'Playing'
@@ -154,7 +162,7 @@ export default {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId, this.episode.id)
},
localEpisodeId() {
return this.localEpisode ? this.localEpisode.id : null
return this.localEpisode?.id || null
},
podcast() {
return this.episode.podcast || {}
@@ -175,14 +183,19 @@ export default {
return folderObj
},
async downloadClick() {
if (this.downloadItem) return
if (this.downloadItem || this.pendingDownload) return
this.pendingDownload = true
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
this.startDownload()
await this.startDownload()
} else {
this.download()
await this.download()
}
setTimeout(() => {
this.pendingDownload = false
}, 1000)
},
async download(selectedLocalFolder = null) {
let localFolder = selectedLocalFolder
@@ -216,14 +229,7 @@ export default {
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
return this.startDownload(localFolder)
},
async startDownload(localFolder) {
var payload = {
@@ -238,13 +244,19 @@ export default {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
this.$toast.error(errorMsg)
} else {
console.log('Download completed', JSON.stringify(downloadRes))
}
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
if (this.localEpisode && this.localLibraryItemId) {
console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId)
+7 -5
View File
@@ -1,15 +1,15 @@
<template>
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-border text-center" :disabled="disabled || loading" :class="classList">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</nuxt-link>
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<button v-else class="btn outline-none rounded-md shadow-md relative border border-border" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
@@ -41,7 +41,9 @@ export default {
classList() {
var list = []
if (this.loading) list.push('text-opacity-0')
list.push('text-white')
if (this.color === 'success') {
list.push('text-white')
}
list.push(`bg-${this.color}`)
if (this.small) {
list.push('text-sm')
+1 -1
View File
@@ -4,7 +4,7 @@
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
</div>
<div v-if="label" class="select-none text-gray-100" :class="labelClassname">{{ label }}</div>
<div v-if="label" class="select-none text-fg" :class="labelClassname">{{ label }}</div>
</label>
</template>
+7 -7
View File
@@ -1,8 +1,8 @@
<template>
<div class="relative w-full" v-click-outside="clickedOutside">
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center" :class="!selectedText ? 'text-gray-300' : 'text-white'">
<p class="text-sm font-semibold" :class="disabled ? 'text-fg-muted' : ''">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative w-full border border-border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center" :class="!selectedText ? 'text-fg-muted' : 'text-fg'">
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText || placeholder || '' }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
@@ -11,9 +11,9 @@
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-gray-600 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-border shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-bg ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox">
<template v-for="item in items">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans text-sm">{{ item.text }}</span>
</div>
@@ -65,8 +65,8 @@ export default {
if (this.small) classes.push('h-9')
else classes.push('h-10')
if (this.disabled) classes.push('cursor-not-allowed border-gray-600 bg-primary bg-opacity-70 border-opacity-70 text-gray-400')
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
if (this.disabled) classes.push('cursor-not-allowed border-border bg-primary bg-opacity-70 border-opacity-70 text-fg-muted')
else classes.push('cursor-pointer border-border bg-primary text-fg')
return classes.join(' ')
}
+3 -3
View File
@@ -4,17 +4,17 @@
<slot />
</div>
<transition name="menu">
<ul ref="menu" v-show="showMenu" class="absolute z-50 -mt-px bg-primary border border-gray-600 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" style="width: 160px">
<ul ref="menu" v-show="showMenu" class="absolute z-50 -mt-px bg-primary border border-border shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-bg ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" style="width: 160px">
<template v-for="item in items">
<nuxt-link :key="item.value" v-if="item.to" :to="item.to">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-2" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center px-2">
<span v-if="item.icon" class="material-icons-outlined text-lg mr-2" :class="item.iconClass ? item.iconClass : ''">{{ item.icon }}</span>
<span class="font-normal block truncate font-sans text-center">{{ item.text }}</span>
</div>
</li>
</nuxt-link>
<li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<li v-else :key="item.value" class="text-fg select-none relative py-2" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center px-2">
<span v-if="item.icon" class="material-icons-outlined text-lg mr-2" :class="item.iconClass ? item.iconClass : ''">{{ item.icon }}</span>
<span class="font-normal block truncate font-sans text-center">{{ item.text }}</span>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
+2 -2
View File
@@ -1,13 +1,13 @@
<template>
<div class="w-40">
<div class="bg-bg border border-gray-500 py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
<div class="bg-bg border border-border py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
<div class="loader-dots block relative w-20 h-5 mt-2">
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
</div>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ text }}</div>
<div class="text-fg text-xs font-light mt-2 text-center">{{ text }}</div>
</div>
</div>
</template>
+5 -5
View File
@@ -1,25 +1,25 @@
<template>
<div class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="showMenu = !showMenu">
<button type="button" class="relative w-full bg-fg border border-border rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="showMenu = !showMenu">
<span class="flex items-center">
<span class="block truncate">{{ label }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span class="material-icons text-gray-100">person</span>
<span class="material-icons text-fg">person</span>
</span>
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-gray-600 shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border bordesr-border shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-bg ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
<template v-for="item in items">
<nuxt-link :key="item.value" v-if="item.to" :to="item.to">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-fg select-none relative py-2" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
</div>
</li>
</nuxt-link>
<li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<li v-else :key="item.value" class="text-fg select-none relative py-2" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
</div>
+8 -8
View File
@@ -49,7 +49,7 @@ input[type='range']:focus {
/* chromium */
input[type='range']::-webkit-slider-runnable-track {
background-color: rgb(0 0 0 / 0.25);
background-color: rgb(var(--color-track) / 0.5);
border-radius: 9999px;
height: 0.75rem;
}
@@ -58,19 +58,19 @@ input[type='range']::-webkit-slider-thumb {
appearance: none;
margin-top: -0.25rem;
border-radius: 9999px;
background-color: rgb(255 255 255 / 0.7);
background-color: rgb(var(--color-track-cursor));
height: 1.25rem;
width: 1.25rem;
}
input[type='range']:focus::-webkit-slider-thumb {
border: 1px solid #6b6b6b;
outline: 3px solid #6b6b6b;
border: 1px solid rgb(var(--color-track));
outline: 3px solid rgb(var(--color-track));
outline-offset: 0.125rem;
}
/* firefox */
input[type='range']::-moz-range-track {
background-color: rgb(0 0 0 / 0.25);
background-color: rgb(var(--color-track) / 0.5);
border-radius: 9999px;
height: 0.75rem;
}
@@ -78,13 +78,13 @@ input[type='range']::-moz-range-thumb {
border: none;
border-radius: 9999px;
margin-top: -0.25rem;
background-color: rgb(255 255 255 / 0.7);
background-color: rgb(var(--color-track-cursor));
height: 1.25rem;
width: 1.25rem;
}
input[type='range']:focus::-moz-range-thumb {
border: 1px solid #6b6b6b;
outline: 3px solid #6b6b6b;
border: 1px solid rgb(var(--color-track));
outline: 3px solid rgb(var(--color-track));
outline-offset: 0.125rem;
}
</style>
+7 -4
View File
@@ -1,6 +1,6 @@
<template>
<div class="relative">
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" :readonly="readonly" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="py-2 w-full outline-none bg-primary" :class="inputClass" @keyup="keyup" />
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" :readonly="readonly" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="py-2 w-full outline-none bg-primary disabled:text-fg-muted" :class="inputClass" @keyup="keyup" />
<div v-if="prependIcon" class="absolute top-0 left-0 h-full px-2 flex items-center justify-center">
<span class="material-icons text-lg">{{ prependIcon }}</span>
</div>
@@ -54,13 +54,13 @@ export default {
},
inputClass() {
var classes = [`bg-${this.bg}`, `rounded-${this.rounded}`]
if (this.disabled) classes.push('text-gray-300')
else classes.push('text-white')
if (this.disabled) classes.push('text-fg-muted')
else classes.push('text-fg')
if (this.prependIcon) classes.push('pl-10 pr-2')
else classes.push('px-2')
if (!this.borderless) classes.push('border border-gray-600')
if (!this.borderless) classes.push('border border-border')
return classes.join(' ')
}
},
@@ -88,4 +88,7 @@ export default {
input[type='time']::-webkit-calendar-picker-indicator {
filter: invert(100%);
}
html[data-theme='light'] input[type='time']::-webkit-calendar-picker-indicator {
filter: unset;
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
<textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-fg focus:outline-none" :class="transparent ? '' : 'border border-border'" @change="change" />
</template>
<script>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-full">
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-fg-muted' : ''">{{ label }}</p>
<ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" />
</div>
</template>
+5 -30
View File
@@ -1,6 +1,6 @@
<template>
<div class="inline-flex toggle-btn-wrapper shadow-md">
<button v-for="item in items" :key="item.value" type="button" class="toggle-btn outline-none relative border border-gray-600 px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
<button v-for="item in items" :key="item.value" type="button" class="toggle-btn outline-none relative border border-border px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
{{ item.text }}
</button>
</div>
@@ -10,9 +10,6 @@
export default {
props: {
value: String,
/**
* [{ "text", "", "value": "" }]
*/
items: {
type: Array,
default: Object
@@ -52,34 +49,12 @@ export default {
.toggle-btn-wrapper .toggle-btn:not(:first-child) {
margin-left: -1px;
}
.toggle-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
.toggle-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
.toggle-btn:hover:not(:disabled) {
color: white;
}
.toggle-btn {
color: rgba(255, 255, 255, 0.75);
background-color: rgb(var(--color-bg-toggle));
color: rgb(var(--color-fg) / 0.5);
}
.toggle-btn.selected {
color: white;
}
.toggle-btn.selected::before {
background-color: rgba(255, 255, 255, 0.1);
}
button.toggle-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
background-color: rgb(var(--color-bg-toggle-selected));
color: rgb(var(--color-fg));
}
</style>
-92
View File
@@ -1,92 +0,0 @@
<template>
<div ref="box" class="inline-block" @click.stop="click">
<slot />
</div>
</template>
<script>
export default {
props: {
text: {
type: [String, Number],
required: true
},
direction: {
type: String,
default: 'right'
},
disabled: Boolean
},
data() {
return {
tooltip: null,
tooltipTextEl: null,
tooltipId: null,
isShowing: false,
hideTimeout: null
}
},
watch: {
text() {
this.updateText()
},
disabled(newVal) {
if (newVal && this.isShowing) {
this.hideTooltip()
}
}
},
methods: {
updateText() {
if (this.tooltipTextEl) {
this.tooltipTextEl.innerHTML = this.text
}
},
createTooltip() {
if (!this.$refs.box) return
const tooltip = document.createElement('div')
this.tooltipId = String(Math.floor(Math.random() * 10000))
tooltip.id = this.tooltipId
tooltip.className = 'fixed inset-0 w-screen h-screen bg-black/25 text-xs flex items-center justify-center p-2'
tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.addEventListener('click', this.hideTooltip)
const innerDiv = document.createElement('div')
innerDiv.className = 'w-full p-2 border border-white/20 pointer-events-none text-white bg-primary'
innerDiv.innerHTML = this.text
tooltip.appendChild(innerDiv)
this.tooltipTextEl = innerDiv
this.tooltip = tooltip
},
showTooltip() {
if (this.disabled) return
if (!this.tooltip) {
this.createTooltip()
if (!this.tooltip) return
}
if (!this.$refs.box) return // Ensure element is not destroyed
try {
document.body.appendChild(this.tooltip)
} catch (error) {
console.error(error)
}
this.isShowing = true
},
hideTooltip() {
if (!this.tooltip) return
this.tooltip.remove()
this.isShowing = false
},
click() {
if (!this.isShowing) this.showTooltip()
}
},
beforeDestroy() {
this.hideTooltip()
}
}
</script>
-4
View File
@@ -5,10 +5,6 @@
<circle cx="20" cy="20" r="17.5" class="progressbar__svg-circlebg"></circle>
</svg>
<p class="progressbar__text text-sm text-warning">{{ count }}</p>
<!-- <span class="material-icons progressbar__text text-xl">arrow_downward</span> -->
<!-- <div class="w-4 h-4 rounded-full bg-red-600 absolute bottom-1 right-1 flex items-center justify-center transform rotate-90">
<p class="text-xs text-white">4</p>
</div> -->
</div>
</template>
+6 -7
View File
@@ -59,20 +59,19 @@ export default {
methods: {
showAlertDialog() {
var msg = ''
var meteredString = this.isNetworkUnmetered ? 'unmetered' : 'metered'
if (this.attemptingConnection) {
msg = 'Attempting server connection'
msg = this.$strings.MessageAttemptingServerConnection
} else if (!this.networkConnected) {
msg = 'No internet'
msg = this.$strings.MessageNoNetworkConnection
} else if (!this.socketConnected) {
msg = 'Socket not connected'
msg = this.$strings.MessageSocketNotConnected
} else if (this.isCellular) {
msg = `Socket connected over ${meteredString} cellular`
msg = this.isNetworkUnmetered ? this.$strings.MessageSocketConnectedOverUnmeteredCellular : this.$strings.MessageSocketConnectedOverMeteredCellular
} else {
msg = `Socket connected over ${meteredString} wifi`
msg = this.isNetworkUnmetered ? this.$strings.MessageSocketConnectedOverUnmeteredWifi : this.$strings.MessageSocketConnectedOverMeteredWifi
}
Dialog.alert({
title: 'Connection Status',
title: this.$strings.HeaderConnectionStatus,
message: msg
})
}
@@ -54,9 +54,8 @@ export default {
}
if (!data.localLibraryItem) {
this.$toast.error('Item download complete but failed to create library item')
this.$toast.error(this.$strings.MessageItemDownloadCompleteFailedToCreate)
} else {
this.$toast.success(`Item "${data.localLibraryItem.media.metadata.title}" download finished`)
this.$eventBus.$emit('new-local-library-item', data.localLibraryItem)
}
+4 -4
View File
@@ -728,12 +728,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 27;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.68;
MARKETING_VERSION = 0.9.70;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -752,12 +752,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 27;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.68;
MARKETING_VERSION = 0.9.70;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+7 -1
View File
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch.
let configuration = Realm.Configuration(
schemaVersion: 14,
schemaVersion: 15,
migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
@@ -42,6 +42,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
newObject?["hapticFeedback"] = "LIGHT"
}
}
if (oldSchemaVersion < 15) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding languageCode setting")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["languageCode"] = "en-us"
}
}
}
)
Realm.Configuration.defaultConfiguration = configuration
+2
View File
@@ -241,6 +241,7 @@ public class AbsDatabase: CAPPlugin {
let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10
let lockOrientation = call.getString("lockOrientation") ?? "NONE"
let hapticFeedback = call.getString("hapticFeedback") ?? "LIGHT"
let languageCode = call.getString("languageCode") ?? "en-us"
let settings = DeviceSettings()
settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView
@@ -248,6 +249,7 @@ public class AbsDatabase: CAPPlugin {
settings.jumpForwardTime = jumpForwardTime
settings.lockOrientation = lockOrientation
settings.hapticFeedback = hapticFeedback
settings.languageCode = languageCode
Database.shared.setDeviceSettings(deviceSettings: settings)
+3 -1
View File
@@ -15,6 +15,7 @@ class DeviceSettings: Object {
@Persisted var jumpForwardTime: Int = 10
@Persisted var lockOrientation: String = "NONE"
@Persisted var hapticFeedback: String = "LIGHT"
@Persisted var languageCode: String = "en-us"
}
func getDefaultDeviceSettings() -> DeviceSettings {
@@ -28,6 +29,7 @@ func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> {
"jumpBackwardsTime": settings.jumpBackwardsTime,
"jumpForwardTime": settings.jumpForwardTime,
"lockOrientation": settings.lockOrientation,
"hapticFeedback": settings.hapticFeedback
"hapticFeedback": settings.hapticFeedback,
"languageCode": settings.languageCode
]
}
@@ -164,7 +164,7 @@ extension LocalLibraryItem {
func getPlaybackSession(episode: PodcastEpisode?) -> PlaybackSession {
let localEpisodeId = episode?.id
let sessionId = "play_local_\(UUID().uuidString)"
let sessionId = UUID().uuidString
// Get current progress from local media
let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id
+4
View File
@@ -371,6 +371,10 @@ class AudioPlayer: NSObject {
// Reconstruct queue if seeking to a different track
if (self.currentTrackIndex != indexOfSeek) {
// When we seek to a different track, we need to make sure to seek the old track to 0
// or we will get jumps to the old position when fading over into a new track
self.audioPlayer.seek(to: CMTime(seconds: 0, preferredTimescale: 1000))
self.currentTrackIndex = indexOfSeek
try? playbackSession.update {
+1 -1
View File
@@ -21,7 +21,7 @@ class PlayerHandler {
cleanupOldSessions(currentSessionId: sessionId)
// Set now playing info
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil, isLocal: session.isLocal))
// Create the audio player
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
+1 -2
View File
@@ -14,6 +14,7 @@ struct NowPlayingMetadata {
var title: String
var author: String?
var series: String?
var isLocal: Bool
var coverUrl: URL? {
if self.isLocal {
@@ -25,8 +26,6 @@ struct NowPlayingMetadata {
return url
}
}
var isLocal: Bool { id.starts(with: "play_local_") }
}
class NowPlayingInfo {
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="w-full layout-wrapper bg-bg text-white">
<div class="w-full layout-wrapper bg-bg">
<Nuxt />
</div>
</template>
+23 -15
View File
@@ -1,15 +1,15 @@
<template>
<div class="w-full layout-wrapper bg-bg text-white">
<div class="w-full layout-wrapper bg-bg">
<app-appbar />
<div id="content" class="overflow-hidden relative" :class="isPlayerOpen ? 'playerOpen' : ''">
<Nuxt />
<Nuxt :key="currentLang" />
</div>
<app-audio-player-container ref="streamContainer" />
<modals-libraries-modal />
<modals-playlists-add-create-modal />
<modals-select-local-folder-modal />
<modals-rssfeeds-rss-feed-modal />
<app-side-drawer />
<app-side-drawer :key="currentLang" />
<readers-reader />
</div>
</template>
@@ -23,7 +23,8 @@ export default {
inittingLibraries: false,
hasMounted: false,
disconnectTime: 0,
timeLostFocus: 0
timeLostFocus: 0,
currentLang: null
}
},
watch: {
@@ -220,23 +221,22 @@ export default {
const prog = payload.data // MediaProgress
console.log(`[default] userMediaProgressUpdate checking for local media progress ${payload.id}`)
// Update local media progress if exists
// Check if this media item is currently open in the player, paused, and this progress update is coming from a different session
const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId)
if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) {
console.log('[default] userMediaProgressUpdated for current open media item', payload.data.currentTime)
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
}
// Get local media progress if exists
const localProg = await this.$db.getLocalMediaProgressForServerItem({ libraryItemId: prog.libraryItemId, episodeId: prog.episodeId })
let newLocalMediaProgress = null
// Progress update is more recent then local progress
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) {
console.log('[default] syncing progress server lastUpdate > local lastUpdate but currentTime and isFinished is equal')
return
} else {
console.log(`[default] syncing progress server lastUpdate > local lastUpdate. server currentTime=${prog.currentTime} local currentTime=${localProg.currentTime} | server/local isFinished=${prog.isFinished}/${localProg.isFinished}`)
}
// Check if this media item is currently open in the player, paused, and this progress update is coming from a different session
const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId)
if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) {
console.log('[default] userMediaProgressUpdated for current open media item', payload.data.currentTime)
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
}
// Server progress is more up-to-date
@@ -300,9 +300,15 @@ export default {
this.timeLostFocus = Date.now()
this.$eventBus.$emit('device-focus-update', false)
}
},
changeLanguage(code) {
console.log('Changed lang', code)
this.currentLang = code
document.documentElement.lang = code
}
},
async mounted() {
this.$eventBus.$on('change-lang', this.changeLanguage)
document.addEventListener('visibilitychange', this.visibilityChanged)
this.$socket.on('user_updated', this.userUpdated)
@@ -311,6 +317,8 @@ export default {
if (this.$store.state.isFirstLoad) {
this.$store.commit('setIsFirstLoad', false)
this.loadSavedSettings()
const deviceData = await this.$db.getDeviceData()
this.$store.commit('setDeviceData', deviceData)
@@ -329,7 +337,6 @@ export default {
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalSessions()
this.loadSavedSettings()
this.hasMounted = true
console.log('[default] fully initialized')
@@ -337,6 +344,7 @@ export default {
}
},
beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage)
document.removeEventListener('visibilitychange', this.visibilityChanged)
this.$socket.off('user_updated', this.userUpdated)
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-app",
"version": "0.9.68-beta",
"version": "0.9.70-beta",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-app",
"version": "0.9.68-beta",
"version": "0.9.70-beta",
"dependencies": {
"@capacitor/android": "^5.0.0",
"@capacitor/app": "^5.0.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.68-beta",
"version": "0.9.70-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
+6 -6
View File
@@ -1,14 +1,14 @@
<template>
<div class="w-full h-full p-4">
<ui-text-input-with-label :value="serverAddress" label="Host" disabled class="my-2" />
<ui-text-input-with-label :value="serverAddress" :label="$strings.LabelHost" disabled class="my-2" />
<ui-text-input-with-label :value="username" label="Username" disabled class="my-2" />
<ui-text-input-with-label :value="username" :label="$strings.LabelUsername" disabled class="my-2" />
<ui-btn color="primary flex items-center justify-between gap-2 ml-auto text-base mt-8" @click="logout">Switch Server/User<span class="material-icons" style="font-size: 1.1rem">logout</span></ui-btn>
<ui-btn color="primary flex items-center justify-between gap-2 ml-auto text-base mt-8" @click="logout">{{ $strings.ButtonSwitchServerUser }}<span class="material-icons" style="font-size: 1.1rem">logout</span></ui-btn>
<div class="flex justify-end items-center m-4 gap-3 right-0 bottom-0 absolute">
<p class="text-smtext-yellow-400 text-right">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf-app" target="_blank">GitHub</a></p>
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
<div class="flex justify-center items-center my-4 left-0 right-0 bottom-0 absolute">
<p class="text-sm text-fg">{{ $strings.MessageReportBugsAndContribute }} <a class="underline" href="https://github.com/advplyr/audiobookshelf-app" target="_blank">GitHub</a></p>
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-fg hover:scale-150 hover:rotate-6 transform duration-500 ml-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="24" height="24" viewBox="0 0 24 24">
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
+1 -1
View File
@@ -24,7 +24,7 @@ export default {
return this.$route.name === 'bookshelf-latest'
},
isPodcastSearch() {
return this.$route.name === 'bookshelf-search'
return this.$route.name === 'bookshelf-add-podcast'
}
}
}
@@ -8,12 +8,12 @@
</div>
<div v-if="!networkConnected" class="w-full text-center py-6">
<p class="text-lg text-error">No network connection</p>
<p class="text-lg text-error">{{ $strings.MessageNoNetworkConnection }}</p>
</div>
<div v-else class="w-full mx-auto pb-2 overflow-y-auto overflow-x-hidden h-[calc(100%-85px)]">
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No Podcasts Found</p>
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">{{ $strings.MessageNoPodcastsFound }}</p>
<template v-for="podcast in results">
<div :key="podcast.id" class="p-2 border-b border-white border-opacity-10" @click="selectPodcast(podcast)">
<div :key="podcast.id" class="p-2 border-b border-fg border-opacity-10" @click="selectPodcast(podcast)">
<div class="flex">
<div class="w-8 min-w-8 py-1">
<div class="h-8 w-full bg-primary">
@@ -21,13 +21,13 @@
</div>
</div>
<div class="flex-grow pl-2">
<p class="text-xs text-gray-100 whitespace-nowrap truncate">{{ podcast.artistName }}</p>
<p class="text-xxs text-gray-300 leading-5">{{ podcast.trackCount }} Episodes</p>
<p class="text-xs text-fg whitespace-nowrap truncate">{{ podcast.artistName }}</p>
<p class="text-xxs text-fg leading-5">{{ podcast.trackCount }} {{ $strings.HeaderEpisodes }}</p>
</div>
</div>
<p class="text-sm text-gray-200 mb-1">{{ podcast.title }}</p>
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
<p class="text-sm text-fg mb-1">{{ podcast.title }}</p>
<p class="text-xs text-fg-muted leading-5">{{ podcast.genres.join(', ') }}</p>
</div>
</template>
</div>
@@ -35,8 +35,8 @@
<template v-else>
<div class="flex items-center px-2 h-16">
<div class="flex items-center" @click="clearSelected">
<span class="material-icons text-2xl text-gray-300">arrow_back</span>
<p class="pl-2 uppercase text-sm font-semibold text-gray-300 leading-4 pb-px">Back</p>
<span class="material-icons text-2xl text-fg-muted">arrow_back</span>
<p class="pl-2 uppercase text-sm font-semibold text-fg-muted leading-4 pb-px">{{ $strings.ButtonBack }}</p>
</div>
</div>
+15 -15
View File
@@ -2,41 +2,37 @@
<div class="w-full h-full min-h-full relative">
<div v-if="attemptingConnection" class="w-full pt-4 flex items-center justify-center">
<widgets-loading-spinner />
<p class="pl-4">Attempting server connection...</p>
<p class="pl-4">{{ $strings.MessageAttemptingServerConnection }}</p>
</div>
<div v-if="shelves.length && isLoading" class="w-full pt-4 flex items-center justify-center">
<widgets-loading-spinner />
<p class="pl-4">Loading server data...</p>
<p class="pl-4">{{ $strings.MessageLoadingServerData }}</p>
</div>
<div class="w-full" :class="{ 'py-6': altViewEnabled }">
<template v-for="(shelf, index) in shelves">
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
<bookshelf-shelf :key="shelf.id" :label="getShelfLabel(shelf)" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
</template>
</div>
<div v-if="!shelves.length && !isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<div>
<p class="mb-4 text-center text-xl">
Bookshelf empty
<span v-show="user">
for library
<strong>{{ currentLibraryName }}</strong>
</span>
{{ $strings.MessageBookshelfEmpty }}
</p>
<div class="w-full" v-if="!user">
<div class="flex justify-center items-center mb-3">
<span class="material-icons text-error text-lg">cloud_off</span>
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
<p class="pl-2 text-error text-sm">{{ $strings.MessageAudiobookshelfServerNotConnected }}</p>
</div>
</div>
<div class="flex justify-center">
<ui-btn v-if="!user" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn>
<ui-btn v-if="!user" small @click="$router.push('/connect')" class="w-32">{{ $strings.ButtonConnect }}</ui-btn>
</div>
</div>
</div>
<div v-else-if="!shelves.length && isLoading && !attemptingConnection" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
<ui-loading-indicator text="Loading..." />
<ui-loading-indicator :text="$strings.MessageLoading" />
</div>
</div>
</template>
@@ -113,6 +109,10 @@ export default {
}
},
methods: {
getShelfLabel(shelf) {
if (shelf.labelStringKey && this.$strings[shelf.labelStringKey]) return this.$strings[shelf.labelStringKey]
return shelf.label
},
getLocalMediaItemCategories() {
const localMedia = this.localLibraryItems
if (!localMedia?.length) return []
@@ -147,7 +147,7 @@ export default {
if (booksContinueListening.length) {
categories.push({
id: 'local-books-continue',
label: 'Continue Books',
label: this.$strings.LabelContinueBooks,
type: 'book',
localOnly: true,
entities: booksContinueListening.sort((a, b) => {
@@ -161,7 +161,7 @@ export default {
if (podcastEpisodesContinueListening.length) {
categories.push({
id: 'local-episodes-continue',
label: 'Continue Episodes',
label: this.$strings.LabelContinueEpisodes,
type: 'episode',
localOnly: true,
entities: podcastEpisodesContinueListening.sort((a, b) => {
@@ -177,7 +177,7 @@ export default {
if (books.length) {
categories.push({
id: 'local-books',
label: 'Local Books',
label: this.$strings.LabelLocalBooks,
type: 'book',
entities: books.sort((a, b) => {
if (a.progress && a.progress.isFinished) return 1
@@ -192,7 +192,7 @@ export default {
if (podcasts.length) {
categories.push({
id: 'local-podcasts',
label: 'Local Podcasts',
label: this.$strings.LabelLocalPodcasts,
type: 'podcast',
entities: podcasts
})
+37 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-full p-4">
<h1 class="text-xl mb-2 font-semibold">Latest Episodes</h1>
<h1 class="text-xl mb-2 font-semibold">{{ $strings.HeaderLatestEpisodes }}</h1>
<template v-for="episode in recentEpisodes">
<tables-podcast-latest-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="episode.libraryItemId" :local-library-item-id="null" :is-local="isLocal" :key="episode.id" @addToPlaylist="addEpisodeToPlaylist" />
@@ -16,7 +16,7 @@ export default {
recentEpisodes: [],
totalEpisodes: 0,
currentPage: 0,
localEpisodeMap: {},
localLibraryItems: [],
isLocal: false,
loadedLibraryId: null
}
@@ -25,6 +25,24 @@ export default {
computed: {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
localEpisodes() {
const episodes = []
this.localLibraryItems.forEach((li) => {
if (li.media.episodes?.length) {
episodes.push(...li.media.episodes)
}
})
return episodes
},
localEpisodeMap() {
var epmap = {}
this.localEpisodes.forEach((localEp) => {
if (localEp.serverEpisodeId) {
epmap[localEp.serverEpisodeId] = localEp
}
})
return epmap
}
},
methods: {
@@ -61,14 +79,31 @@ export default {
this.$router.replace('/bookshelf')
}
}
},
async loadLocalPodcastLibraryItems() {
this.localLibraryItems = await this.$db.getLocalLibraryItems('podcast')
},
newLocalLibraryItem(item) {
if (item.mediaType !== 'podcast') {
return
}
const matchingLocalLibraryItem = this.localLibraryItems.find((lli) => lli.id === item.id)
if (matchingLocalLibraryItem) {
matchingLocalLibraryItem.media.episodes = item.media.episodes
} else {
this.localLibraryItems.push(item)
}
}
},
mounted() {
this.loadRecentEpisodes()
this.loadLocalPodcastLibraryItems()
this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
},
beforeDestroy() {
this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
}
}
</script>
+18 -4
View File
@@ -12,14 +12,14 @@
{{ collectionName }}
</h1>
<div class="flex-grow" />
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center justify-center h-9 mr-2 w-24" @click="clickPlay">
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small :loading="playerIsStartingForThisMedia" class="flex items-center justify-center h-9 mr-2 w-24" @click="clickPlay">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Streaming' : 'Play' }}
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
</ui-btn>
</div>
<div class="my-8 max-w-2xl px-2">
<p class="text-base text-gray-100">{{ description }}</p>
<p class="text-base text-fg">{{ description }}</p>
</div>
<tables-collection-books-table :books="bookItems" :collection-id="collection.id" />
@@ -53,6 +53,7 @@ export default {
},
data() {
return {
mediaIdStartingPlayback: null,
processingRemove: false
}
},
@@ -77,17 +78,30 @@ export default {
streaming() {
return !!this.playableBooks.find((b) => this.$store.getters['getIsMediaStreaming'](b.id))
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
if (!this.mediaIdStartingPlayback) return false
const mediaId = this.$store.state.playerStartingPlaybackMediaId
return mediaId === this.mediaIdStartingPlayback
},
showPlayButton() {
return this.playableBooks.length
}
},
methods: {
clickPlay() {
if (this.playerIsStartingPlayback) return
var nextBookNotRead = this.playableBooks.find((pb) => {
var prog = this.$store.getters['user/getUserMediaProgress'](pb.id)
return !prog || !prog.isFinished
return !prog?.isFinished
})
if (nextBookNotRead) {
this.mediaIdStartingPlayback = nextBookNotRead.id
this.$store.commit('setPlayerIsStartingPlayback', nextBookNotRead.id)
this.$eventBus.$emit('play-item', { libraryItemId: nextBookNotRead.id })
}
}
+3 -3
View File
@@ -8,15 +8,15 @@
<img src="/Logo.png" class="h-20 w-20 mb-2" />
<h1 class="text-2xl">audiobookshelf</h1>
</div>
<p class="hidden absolute short:block top-1.5 left-12 p-2text-xl">audiobookshelf</p>
<p class="hidden absolute short:block top-1.5 left-12 p-2 text-xl">audiobookshelf</p>
<connection-server-connect-form v-if="deviceData" />
</div>
<div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0">
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-sm pr-2">Follow the project on Github</a>
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-sm pr-2">{{ $strings.MessageFollowTheProjectOnGithub }}</a>
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank"
><svg class="w-8 h-8 text-gray-100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
><svg class="w-8 h-8 text-fg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
<path
d="M12 2.247a10 10 0 0 0-3.162 19.487c.5.088.687-.212.687-.475c0-.237-.012-1.025-.012-1.862c-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 0 0-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 0 1 1.538 1.025a2.137 2.137 0 0 0 2.912.825a2.104 2.104 0 0 1 .638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 0 1 1.025-2.688a3.594 3.594 0 0 1 .1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 0 1 5 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 0 1 .1 2.65a3.869 3.869 0 0 1 1.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 0 1 .675 1.85c0 1.338-.012 2.413-.012 2.75c0 .263.187.575.687.475A10.005 10.005 0 0 0 12 2.247z"
fill="currentColor"
+3 -3
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-full h-full py-6 px-4 overflow-y-auto">
<p class="mb-4 text-base text-white">Downloading Files ({{ downloadItemParts.length }})</p>
<p class="mb-4 text-base text-fg">{{ $strings.HeaderDownloads }} ({{ downloadItemParts.length }})</p>
<div v-if="!downloadItemParts.length" class="py-6 text-center text-lg">No download item parts</div>
<template v-for="(itemPart, num) in downloadItemParts">
@@ -8,14 +8,14 @@
<div class="flex">
<div class="w-14">
<span v-if="itemPart.completed" class="material-icons text-success">check_circle_outline</span>
<span v-else class="font-semibold text-gray-200">{{ Math.round(itemPart.progress) }}%</span>
<span v-else class="font-semibold text-fg">{{ Math.round(itemPart.progress) }}%</span>
</div>
<div class="flex-grow px-2">
<p class="truncate">{{ itemPart.filename }}</p>
</div>
</div>
<div v-if="num + 1 < downloadItemParts.length" class="flex border-t border-white border-opacity-10 my-3" />
<div v-if="num + 1 < downloadItemParts.length" class="flex border-t border-border my-3" />
</div>
</template>
</div>
+6 -6
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-full h-full py-6 px-4 overflow-y-auto">
<p class="mb-2 text-base text-white">Downloads ({{ localLibraryItems.length }})</p>
<p class="mb-2 text-base text-fg">{{ $strings.HeaderDownloads }} ({{ localLibraryItems.length }})</p>
<div class="w-full">
<template v-for="(mediaItem, num) in localLibraryItems">
@@ -11,15 +11,15 @@
</div>
<div class="px-2 flex-grow">
<p class="text-sm">{{ mediaItem.media.metadata.title }}</p>
<p v-if="mediaItem.mediaType == 'book'" class="text-xs text-gray-300">{{ mediaItem.media.tracks.length }} Track{{ mediaItem.media.tracks.length == 1 ? '' : 's' }}</p>
<p v-else-if="mediaItem.mediaType == 'podcast'" class="text-xs text-gray-300">{{ mediaItem.media.episodes.length }} Episode{{ mediaItem.media.episodes.length == 1 ? '' : 's' }}</p>
<p v-if="mediaItem.size" class="text-xs text-gray-300">{{ $bytesPretty(mediaItem.size) }}</p>
<p v-if="mediaItem.mediaType == 'book'" class="text-xs text-fg-muted">{{ mediaItem.media.tracks.length }} {{ $strings.LabelTracks }}</p>
<p v-else-if="mediaItem.mediaType == 'podcast'" class="text-xs text-fg-muted">{{ mediaItem.media.episodes.length }} {{ $strings.HeaderEpisodes }}</p>
<p v-if="mediaItem.size" class="text-xs text-fg-muted">{{ $bytesPretty(mediaItem.size) }}</p>
</div>
<div class="w-12 h-12 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-400">chevron_right</span>
<span class="material-icons text-2xl text-fg-muted">chevron_right</span>
</div>
</nuxt-link>
<div v-if="num+1 < localLibraryItems.length" class="flex border-t border-white border-opacity-10 my-3" />
<div v-if="num + 1 < localLibraryItems.length" class="flex border-t border-fg/10 my-3" />
</div>
</template>
</div>
+46 -38
View File
@@ -6,42 +6,42 @@
</div>
<div class="flex-grow px-2">
<div class="-mt-0.5 mb-0.5">
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-gray-200 underline">{{ podcast.metadata.title }}</nuxt-link>
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-fg underline">{{ podcast.metadata.title }}</nuxt-link>
</div>
<p v-if="publishedAt" class="text-xs text-gray-300">{{ $dateDistanceFromNow(publishedAt) }}</p>
<p v-if="publishedAt" class="text-xs text-fg-muted">{{ $dateDistanceFromNow(publishedAt) }}</p>
</div>
</div>
<p class="text-lg font-semibold">{{ title }}</p>
<div v-if="episodeNumber || season || episodeType" class="flex py-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-gray-200 capitalize">{{ episodeType }}</div>
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-fg">{{ $strings.LabelEpisode }} #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-fg">{{ $strings.LabelSeason }} #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-fg capitalize">{{ episodeType }}</div>
</div>
<!-- user progress card -->
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-fg mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="progressPercent < 1" class="text-fg-muted text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
<p v-else class="text-fg-muted text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt) }}</p>
</div>
<!-- action buttons -->
<div class="flex mt-4 -mx-1">
<ui-btn color="success" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<ui-btn color="success" class="flex items-center justify-center flex-grow mx-1" :loading="playerIsStartingForThisMedia" :padding-x="4" @click="playClick">
<span class="material-icons">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? 'Pause' : localEpisodeId ? 'Play' : 'Stream' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? $strings.ButtonPause : localEpisodeId ? $strings.ButtonPlay : $strings.ButtonStream }}</span>
</ui-btn>
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mx-1" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span class="material-icons" :class="(downloadItem || startingDownload) ? 'animate-pulse' : ''">{{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}</span>
</ui-btn>
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="showMoreMenu = true">
<span class="material-icons">more_vert</span>
</ui-btn>
</div>
<p class="text-sm text-gray-200 mt-1.5 mb-0.5 default-style" v-html="description" />
<p class="text-sm text-fg mt-1.5 mb-0.5 default-style" v-html="description" />
<!-- loading overlay -->
<div v-if="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-30 flex items-center justify-center">
@@ -111,7 +111,8 @@ export default {
return {
showMoreMenu: false,
processing: false,
resettingProgress: false
resettingProgress: false,
startingDownload: false
}
},
computed: {
@@ -210,6 +211,16 @@ export default {
playerIsPlaying() {
return this.$store.state.playerIsPlaying && this.isPlaying
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.$store.state.playerStartingPlaybackMediaId
if (!mediaId) return false
return mediaId === this.localEpisodeId || mediaId === this.serverEpisodeId
},
userItemProgress() {
if (this.isLocal) return this.localItemProgress
return this.serverItemProgress
@@ -257,7 +268,7 @@ export default {
if (!this.userIsFinished) {
items.push({
text: 'Mark as Finished',
text: this.$strings.MessageMarkAsFinished,
value: 'markFinished',
icon: 'beenhere'
})
@@ -265,7 +276,7 @@ export default {
if (this.progressPercent > 0) {
items.push({
text: 'Discard Progress',
text: this.$strings.MessageDiscardProgress,
value: 'discardProgress',
icon: 'backspace'
})
@@ -273,7 +284,7 @@ export default {
if (!this.isLocal) {
items.push({
text: 'Add to Playlist',
text: this.$strings.LabelAddToPlaylist,
value: 'playlist',
icon: 'playlist_add'
})
@@ -281,7 +292,7 @@ export default {
if (this.localEpisodeId) {
items.push({
text: 'Delete Local Episode',
text: this.$strings.ButtonDeleteLocalEpisode,
value: 'deleteLocal',
icon: 'delete'
})
@@ -289,7 +300,7 @@ export default {
if (this.isAdminOrUp && this.serverEpisodeId) {
items.push({
text: 'Remove from Server',
text: this.$strings.ButtonRemoveFromServer,
value: 'remove_from_server',
icon: 'delete_forever'
})
@@ -309,18 +320,13 @@ export default {
return
}
let confirmMessage = `Remove local episode "${localFile.basePath}" from your device?`
if (this.serverLibraryItemId) {
confirmMessage += ' The file on the server will be unaffected.'
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: confirmMessage
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
})
if (value) {
const res = await AbsFileSystem.deleteTrackFromItem({ id: this.localLibraryItemId, trackLocalFileId: localFile.id, trackContentUrl: localEpisodeAudioTrack.contentUrl })
if (res?.id) {
this.$toast.success('Deleted episode successfully')
if (this.isLocal) {
// If this is local episode then redirect to server episode when available
if (this.serverEpisodeId) {
@@ -337,10 +343,14 @@ export default {
}
},
async playClick() {
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
if (this.playerIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
if (this.localEpisodeId && this.localLibraryItemId && !this.isLocal) {
console.log('Play local episode', this.localEpisodeId, this.localLibraryItemId)
@@ -361,7 +371,13 @@ export default {
}
},
async downloadClick() {
if (this.downloadItem) return
if (this.downloadItem || this.startingDownload) return
this.startingDownload = true
setTimeout(() => {
this.startingDownload = false
}, 1000)
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
@@ -384,7 +400,7 @@ export default {
if (!foldersWithMediaType.length) {
localFolder = {
id: `internal-${this.mediaType}`,
name: 'Internal App Storage',
name: this.$strings.LabelInternalAppStorage,
mediaType: this.mediaType
}
} else if (foldersWithMediaType.length === 1 && internalStorageFolder) {
@@ -402,14 +418,7 @@ export default {
console.log('Local folder', JSON.stringify(localFolder))
const startDownloadMessage = `Start download for "${this.title}" to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
this.startDownload(localFolder)
},
async selectFolder() {
const folderObj = await AbsFileSystem.selectFolder({ mediaType: this.mediaType })
@@ -453,7 +462,7 @@ export default {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to reset your progress?'
message: this.$strings.MessageConfirmDiscardProgress
})
if (value) {
this.resettingProgress = true
@@ -505,7 +514,7 @@ export default {
}
this.$nativeHttp.patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
}
},
@@ -522,7 +531,6 @@ export default {
this.$nativeHttp
.delete(`/api/podcasts/${this.serverLibraryItemId}/episode/${this.serverEpisodeId}?hard=1`)
.then(() => {
this.$toast.success('Episode deleted from server')
this.$router.replace(`/item/${this.serverLibraryItemId}`)
})
.catch((error) => {
+61 -42
View File
@@ -2,7 +2,7 @@
<div id="item-page" 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%)" />
<div class="w-full h-full absolute top-0 left-0" style="background: var(--gradient-item-page)" />
</div>
<div class="z-10 relative">
@@ -24,110 +24,104 @@
<!-- title -->
<div class="text-center mb-2">
<h1 class="text-xl font-semibold">{{ title }}</h1>
<p v-if="subtitle" class="text-gray-100 text-base">{{ subtitle }}</p>
<p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p>
</div>
<div v-if="hasLocal" class="mx-1">
<div v-if="isLocalOnly" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">Media is not linked to an Audiobookshelf server. No progress will be synced.</p>
<p class="text-sm">{{ $strings.MessageMediaNotLinkedToServer }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingServerAddress" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">Media is linked to an Audiobookshelf server on a different address ({{ localLibraryItem.serverAddress }}). Progress will be synced when connected to this server address.</p>
<p class="text-sm">{{ $getString('MessageMediaLinkedToADifferentServer', [localLibraryItem.serverAddress]) }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingUser" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">Media is linked to this server but was downloaded by a different user. Progress will only be synced to the user that downloaded it.</p>
<p class="text-sm">{{ $strings.MessageMediaLinkedToADifferentUser }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingConnectionConfig" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">Media is linked to a different server connection config. Downloaded User Id: {{ localLibraryItem.serverUserId }}. Downloaded Server Address: {{ localLibraryItem.serverAddress }}. Currently connected User Id: {{ user.id }}. Currently connected server address: {{ currentServerAddress }}.</p>
</div>
<div v-else-if="isLocalMatchingConnectionConfig" class="w-full rounded-md bg-success/10 border border-success p-4">
<p class="text-sm">Downloaded media is linked to this server</p>
</div>
<div v-else-if="isLocal && libraryItem.serverAddress" class="w-full rounded-md bg-slate-300/10 border border-slate-300 p-4">
<p class="text-sm">Linked to server {{ libraryItem.serverAddress }}</p>
</div>
</div>
<!-- action buttons -->
<div class="col-span-full">
<div v-if="showPlay || showRead" class="flex mt-4 -mx-1">
<ui-btn v-if="showPlay" color="success" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<ui-btn v-if="showPlay" color="success" class="flex items-center justify-center flex-grow mx-1" :loading="playerIsStartingForThisMedia" :padding-x="4" @click="playClick">
<span class="material-icons">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? 'Pause' : isPodcast ? 'Next Episode' : hasLocal ? 'Play' : 'Stream' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? $strings.ButtonPause : isPodcast ? $strings.ButtonNextEpisode : hasLocal ? $strings.ButtonPlay : $strings.ButtonStream }}</span>
</ui-btn>
<ui-btn v-if="showRead" color="info" class="flex items-center justify-center mx-1" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
<span v-if="!showPlay" class="px-2 text-base">{{ $strings.ButtonRead }} {{ ebookFormat }}</span>
</ui-btn>
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mx-1" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span class="material-icons" :class="(downloadItem || startingDownload) ? 'animate-pulse' : ''">{{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}</span>
</ui-btn>
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="moreButtonPress">
<span class="material-icons">more_vert</span>
</ui-btn>
</div>
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 text-center">
<p>Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="!useEBookProgress && !userIsFinished" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else-if="userIsFinished" class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-fg mt-4 text-center">
<p>{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="!useEBookProgress && !userIsFinished" class="text-fg-muted text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
<p v-else-if="userIsFinished" class="text-fg-muted text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt) }}</p>
</div>
</div>
<div v-if="downloadItem" class="py-3">
<p v-if="downloadItem.itemProgress == 1" class="text-center text-lg">Download complete. Processing...</p>
<p v-else class="text-center text-lg">Downloading! ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
<p v-if="downloadItem.itemProgress == 1" class="text-center text-lg">{{ $strings.MessageDownloadCompleteProcessing }}</p>
<p v-else class="text-center text-lg">{{ $strings.MessageDownloading }} ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
</div>
<!-- metadata -->
<div id="metadata" class="grid gap-2 my-2" style>
<div v-if="podcastAuthor || (bookAuthors && bookAuthors.length)" class="text-white text-opacity-60 uppercase text-sm">Author</div>
<div v-if="podcastAuthor || (bookAuthors && bookAuthors.length)" class="text-fg-muted uppercase text-sm">{{ $strings.LabelAuthor }}</div>
<div v-if="podcastAuthor" class="text-sm">{{ podcastAuthor }}</div>
<div v-else-if="bookAuthors && bookAuthors.length" class="text-sm">
<template v-for="(author, index) in bookAuthors">
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline">{{ author.name }}</nuxt-link
><span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">, </span>
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline">{{ author.name }}</nuxt-link>
<span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">,</span>
</template>
</div>
<div v-if="podcastType" class="text-white text-opacity-60 uppercase text-sm">Type</div>
<div v-if="podcastType" class="text-fg-muted uppercase text-sm">{{ $strings.LabelType }}</div>
<div v-if="podcastType" class="text-sm capitalize">{{ podcastType }}</div>
<div v-if="series && series.length" class="text-white text-opacity-60 uppercase text-sm">Series</div>
<div v-if="series && series.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelSeries }}</div>
<div v-if="series && series.length" class="truncate text-sm">
<template v-for="(series, index) in seriesList">
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline">{{ series.text }}</nuxt-link
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">, </span>
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline">{{ series.text }}</nuxt-link>
<span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">,</span>
</template>
</div>
<div v-if="numTracks" class="text-white text-opacity-60 uppercase text-sm">Duration</div>
<div v-if="numTracks" class="text-fg-muted uppercase text-sm">{{ $strings.LabelDuration }}</div>
<div v-if="numTracks" class="text-sm">{{ $elapsedPretty(duration) }}</div>
<div v-if="narrators && narrators.length" class="text-white text-opacity-60 uppercase text-sm">{{ narrators.length === 1 ? 'Narrator' : 'Narrators' }}</div>
<div v-if="narrators && narrators.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelNarrators }}</div>
<div v-if="narrators && narrators.length" class="truncate text-sm">
<template v-for="(narrator, index) in narrators">
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline">{{ narrator }}</nuxt-link
><span :key="index" v-if="index < narrators.length - 1">, </span>
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline">{{ narrator }}</nuxt-link>
<span :key="index" v-if="index < narrators.length - 1">,</span>
</template>
</div>
<div v-if="genres.length" class="text-white text-opacity-60 uppercase text-sm">{{ genres.length === 1 ? 'Genre' : 'Genres' }}</div>
<div v-if="genres.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelGenres }}</div>
<div v-if="genres.length" class="truncate text-sm">
<template v-for="(genre, index) in genres">
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline">{{ genre }}</nuxt-link
><span :key="index" v-if="index < genres.length - 1">, </span>
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline">{{ genre }}</nuxt-link>
<span :key="index" v-if="index < genres.length - 1">,</span>
</template>
</div>
<div v-if="publishedYear" class="text-white text-opacity-60 uppercase text-sm">Published</div>
<div v-if="publishedYear" class="text-fg-muted uppercase text-sm">{{ $strings.LabelPublishYear }}</div>
<div v-if="publishedYear" class="text-sm">{{ publishedYear }}</div>
</div>
<div v-if="description" class="w-full py-2">
<p ref="description" class="text-sm text-justify whitespace-pre-line font-light" :class="{ 'line-clamp-4': !showFullDescription }" style="hyphens: auto">{{ description }}</p>
<div v-if="descriptionClamped" class="text-white text-sm py-2" @click="showFullDescription = !showFullDescription">
<div v-if="descriptionClamped" class="text-fg text-sm py-2" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }}
<span class="material-icons align-middle text-base -mt-px">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</div>
@@ -211,7 +205,9 @@ export default {
coverBgIsLight: false,
windowWidth: 0,
descriptionClamped: false,
showFullDescription: false
showFullDescription: false,
episodeStartingPlayback: null,
startingDownload: false
}
},
computed: {
@@ -399,6 +395,20 @@ export default {
playerIsPlaying() {
return this.$store.state.playerIsPlaying && (this.isStreaming || this.isPlaying)
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.$store.state.playerStartingPlaybackMediaId
if (!mediaId) return false
if (this.isPodcast) {
return mediaId === this.episodeStartingPlayback
} else {
return mediaId === this.serverLibraryItemId || mediaId === this.localLibraryItemId
}
},
tracks() {
return this.media.tracks || []
},
@@ -494,6 +504,8 @@ export default {
}
},
async play(startTime = null) {
if (this.playerIsStartingPlayback) return
if (this.isPodcast) {
this.episodes.sort((a, b) => {
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
@@ -506,7 +518,7 @@ export default {
} else {
podcastProgress = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, ep.id)
}
return !podcastProgress || !podcastProgress.isFinished
return !podcastProgress?.isFinished
})
if (!episode) episode = this.episodes[0]
@@ -521,6 +533,8 @@ export default {
}
const serverEpisodeId = !this.isLocal ? episodeId : localEpisode?.serverEpisodeId || null
this.episodeStartingPlayback = serverEpisodeId
this.$store.commit('setPlayerIsStartingPlayback', serverEpisodeId)
if (serverEpisodeId && this.serverLibraryItemId && this.isCasting) {
// If casting and connected to server for local library item then send server library item id
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId, episodeId: serverEpisodeId })
@@ -549,6 +563,7 @@ export default {
if (!value) return
}
this.$store.commit('setPlayerIsStartingPlayback', libraryItemId)
this.$eventBus.$emit('play-item', { libraryItemId, serverLibraryItemId: this.serverLibraryItemId, startTime })
}
},
@@ -572,9 +587,14 @@ export default {
this.download(localFolder)
},
async downloadClick() {
if (this.downloadItem) {
if (this.downloadItem || this.startingDownload) {
return
}
this.startingDownload = true
setTimeout(() => {
this.startingDownload = false
}, 1000)
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
@@ -598,7 +618,7 @@ export default {
if (!foldersWithMediaType.length) {
localFolder = {
id: `internal-${this.mediaType}`,
name: 'Internal App Storage',
name: this.$strings.LabelInternalAppStorage,
mediaType: this.mediaType
}
} else if (foldersWithMediaType.length === 1 && internalStorageFolder) {
@@ -615,7 +635,6 @@ export default {
}
console.log('Local folder', JSON.stringify(localFolder))
let startDownloadMessage = `Start download for "${this.title}" with ${this.numTracks} audio track${this.numTracks == 1 ? '' : 's'} to folder ${localFolder.name}?`
if (!this.isIos && this.showRead) {
if (this.numTracks > 0) {

Some files were not shown because too many files have changed in this diff Show More