mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-31 23:57:15 +02:00
Update:Add epub ereader settings and table of contents
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="modal modal-bg w-screen h-screen fixed top-0 left-0 flex items-center justify-center z-50">
|
<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 bg-bg h-full w-full">
|
<div ref="content" class="relative text-white h-full w-full bg-bg">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
processing: Boolean
|
processing: Boolean,
|
||||||
|
halfScreen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div id="epub-frame" class="w-full">
|
<div id="epub-frame" class="w-full">
|
||||||
<div id="viewer" class="h-full w-full"></div>
|
<div id="viewer" class="h-full w-full"></div>
|
||||||
|
|
||||||
<div class="fixed left-0 h-8 w-full bg-primary px-4 flex items-center text-white/80" :style="{ bottom: playerLibraryItemId ? '120px' : '0px' }">
|
<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: playerLibraryItemId ? '120px' : '0px' }">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="text-xs">{{ progress }}%</p>
|
<p class="text-xs">{{ progress }}%</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,12 @@ export default {
|
|||||||
book: null,
|
book: null,
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
rendition: null,
|
rendition: null,
|
||||||
progress: 0
|
progress: 0,
|
||||||
|
ereaderSettings: {
|
||||||
|
theme: 'dark',
|
||||||
|
fontScale: 100,
|
||||||
|
lineSpacing: 115
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -89,9 +94,46 @@ export default {
|
|||||||
// Validate ebookLocation is an epubcfi
|
// Validate ebookLocation is an epubcfi
|
||||||
if (!String(this.userItemProgress.ebookLocation).startsWith('epubcfi')) return null
|
if (!String(this.userItemProgress.ebookLocation).startsWith('epubcfi')) return null
|
||||||
return this.userItemProgress.ebookLocation
|
return this.userItemProgress.ebookLocation
|
||||||
|
},
|
||||||
|
isLightTheme() {
|
||||||
|
return this.ereaderSettings.theme === 'light'
|
||||||
|
},
|
||||||
|
themeRules() {
|
||||||
|
const isDark = this.ereaderSettings.theme === 'dark'
|
||||||
|
const fontColor = isDark ? '#fff' : '#000'
|
||||||
|
const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)'
|
||||||
|
|
||||||
|
const lineSpacing = this.ereaderSettings.lineSpacing / 100
|
||||||
|
|
||||||
|
const fontScale = this.ereaderSettings.fontScale / 100
|
||||||
|
|
||||||
|
return {
|
||||||
|
'*': {
|
||||||
|
color: `${fontColor}!important`,
|
||||||
|
'background-color': `${backgroundColor}!important`,
|
||||||
|
'line-height': lineSpacing * fontScale + 'rem!important'
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
color: `${fontColor}!important`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateSettings(settings) {
|
||||||
|
this.ereaderSettings = settings
|
||||||
|
|
||||||
|
if (!this.rendition) return
|
||||||
|
|
||||||
|
this.applyTheme()
|
||||||
|
|
||||||
|
const fontScale = settings.fontScale || 100
|
||||||
|
this.rendition.themes.fontSize(`${fontScale}%`)
|
||||||
|
this.rendition.spread(settings.spread || 'auto')
|
||||||
|
},
|
||||||
|
goToChapter(href) {
|
||||||
|
return this.rendition?.display(href)
|
||||||
|
},
|
||||||
updateHeight() {
|
updateHeight() {
|
||||||
if (this.rendition && this.rendition.resize) {
|
if (this.rendition && this.rendition.resize) {
|
||||||
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
|
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
|
||||||
@@ -262,9 +304,6 @@ export default {
|
|||||||
flow: 'paginated'
|
flow: 'paginated'
|
||||||
})
|
})
|
||||||
|
|
||||||
// load style
|
|
||||||
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' }, a: { color: '#fff!important' } })
|
|
||||||
|
|
||||||
reader.book.ready.then(() => {
|
reader.book.ready.then(() => {
|
||||||
// load saved progress
|
// load saved progress
|
||||||
// when not checking spine first uncaught exception is thrown
|
// when not checking spine first uncaught exception is thrown
|
||||||
@@ -274,6 +313,10 @@ export default {
|
|||||||
reader.rendition.display(reader.book.locations.start)
|
reader.rendition.display(reader.book.locations.start)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader.rendition.on('rendered', () => {
|
||||||
|
this.applyTheme()
|
||||||
|
})
|
||||||
|
|
||||||
// set up event listeners
|
// set up event listeners
|
||||||
reader.rendition.on('relocated', reader.relocated)
|
reader.rendition.on('relocated', reader.relocated)
|
||||||
|
|
||||||
@@ -298,6 +341,12 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
applyTheme() {
|
||||||
|
if (!this.rendition) return
|
||||||
|
this.rendition.getContents().forEach((c) => {
|
||||||
|
c.addStylesheetRules(this.themeRules)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|||||||
@@ -1,18 +1,81 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="fixed top-0 left-0 right-0 layout-wrapper w-full bg-primary z-40 pt-8" :class="{ 'reader-player-open': !!playerLibraryItemId }">
|
<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': !!playerLibraryItemId }">
|
||||||
<div class="h-28 pt-8 w-full bg-bg px-2 fixed top-0 left-0 z-30 transition-transform" :class="showingToolbar ? 'translate-y-0' : '-translate-y-28'" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop>
|
<!-- 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="flex items-center mb-2">
|
<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></button>
|
<button type="button" class="inline-flex mx-2" @click.stop="show = false"><span class="material-icons-outlined text-3xl text-white">chevron_left</span></button>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<button v-if="isComic" type="button" class="inline-flex mx-2" @click.stop="clickTOCBtn"><span class="material-icons-outlined text-2xl text-white">format_list_bulleted</span></button>
|
<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></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></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></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></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></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-center truncate">{{ title }}</p>
|
<p class="text-center truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" :library-item="selectedLibraryItem" :is-local="isLocal" :keep-progress="keepProgress" @touchstart="touchstart" @touchend="touchend" @loaded="readerLoaded" />
|
<!-- ereader -->
|
||||||
|
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" :library-item="selectedLibraryItem" :is-local="isLocal" :keep-progress="keepProgress" @touchstart="touchstart" @touchend="touchend" @loaded="readerLoaded" @hook:mounted="readerMounted" />
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
<button class="flex" @click.stop="showTOCModal = false">
|
||||||
|
<span class="material-icons">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- chapters list -->
|
||||||
|
<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">
|
||||||
|
<ul>
|
||||||
|
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
||||||
|
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||||
|
<ul v-if="chapter.subitems.length">
|
||||||
|
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
|
||||||
|
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.label }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-if="!chapters.length" class="flex h-full items-center justify-center">
|
||||||
|
<p class="text-xl">{{ 'No Chapters' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-fullscreen-modal>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-fullscreen-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -32,7 +95,12 @@ export default {
|
|||||||
showTOCModal: false,
|
showTOCModal: false,
|
||||||
showSettingsModal: false,
|
showSettingsModal: false,
|
||||||
comicHasMetadata: false,
|
comicHasMetadata: false,
|
||||||
chapters: []
|
chapters: [],
|
||||||
|
ereaderSettings: {
|
||||||
|
theme: 'dark',
|
||||||
|
fontScale: 100,
|
||||||
|
lineSpacing: 115
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -70,6 +138,22 @@ export default {
|
|||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media?.metadata || {}
|
return this.media?.metadata || {}
|
||||||
},
|
},
|
||||||
|
ereaderTheme() {
|
||||||
|
if (this.isEpub) return this.ereaderSettings.theme
|
||||||
|
return 'dark'
|
||||||
|
},
|
||||||
|
themeItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Dark',
|
||||||
|
value: 'dark'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Light',
|
||||||
|
value: 'light'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
readerComponentName() {
|
readerComponentName() {
|
||||||
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
@@ -140,6 +224,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
settingsUpdated() {
|
||||||
|
this.$refs.readerComponent?.updateSettings?.(this.ereaderSettings)
|
||||||
|
localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings))
|
||||||
|
},
|
||||||
|
goToChapter(href) {
|
||||||
|
this.showTOCModal = false
|
||||||
|
this.$refs.readerComponent?.goToChapter(href)
|
||||||
|
},
|
||||||
|
readerMounted() {
|
||||||
|
if (this.isEpub) {
|
||||||
|
this.loadEreaderSettings()
|
||||||
|
}
|
||||||
|
},
|
||||||
readerLoaded(data) {
|
readerLoaded(data) {
|
||||||
if (this.isComic) {
|
if (this.isComic) {
|
||||||
this.comicHasMetadata = data.hasMetadata
|
this.comicHasMetadata = data.hasMetadata
|
||||||
@@ -149,6 +246,7 @@ export default {
|
|||||||
this.$refs.readerComponent?.clickShowInfoMenu()
|
this.$refs.readerComponent?.clickShowInfoMenu()
|
||||||
},
|
},
|
||||||
clickTOCBtn() {
|
clickTOCBtn() {
|
||||||
|
this.hideToolbar()
|
||||||
if (this.isComic) {
|
if (this.isComic) {
|
||||||
this.$refs.readerComponent?.clickShowPageMenu?.()
|
this.$refs.readerComponent?.clickShowPageMenu?.()
|
||||||
} else {
|
} else {
|
||||||
@@ -157,6 +255,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickSettingsBtn() {
|
clickSettingsBtn() {
|
||||||
|
this.hideToolbar()
|
||||||
this.showSettingsModal = true
|
this.showSettingsModal = true
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
@@ -181,7 +280,11 @@ export default {
|
|||||||
const touchDistanceY = Math.abs(this.touchendY - this.touchstartY)
|
const touchDistanceY = Math.abs(this.touchendY - this.touchstartY)
|
||||||
const touchDistance = Math.sqrt(Math.pow(this.touchstartX - this.touchendX, 2) + Math.pow(this.touchstartY - this.touchendY, 2))
|
const touchDistance = Math.sqrt(Math.pow(this.touchstartX - this.touchendX, 2) + Math.pow(this.touchstartY - this.touchendY, 2))
|
||||||
if (touchDistance < 60) {
|
if (touchDistance < 60) {
|
||||||
this.toggleToolbar()
|
if (this.showSettingsModal) {
|
||||||
|
this.showSettingsModal = false
|
||||||
|
} else {
|
||||||
|
this.toggleToolbar()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +316,7 @@ export default {
|
|||||||
if (this.touchstartTime && Date.now() - this.touchstartTime < 250) {
|
if (this.touchstartTime && Date.now() - this.touchstartTime < 250) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log('touchstart', e)
|
||||||
this.touchstartX = e.touches[0].screenX
|
this.touchstartX = e.touches[0].screenX
|
||||||
this.touchstartY = e.touches[0].screenY
|
this.touchstartY = e.touches[0].screenY
|
||||||
this.touchstartTime = Date.now()
|
this.touchstartTime = Date.now()
|
||||||
@@ -223,6 +326,7 @@ export default {
|
|||||||
if (this.touchIdentifier !== e.changedTouches[0].identifier) {
|
if (this.touchIdentifier !== e.changedTouches[0].identifier) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log('touchend', e)
|
||||||
this.touchendX = e.changedTouches[0].screenX
|
this.touchendX = e.changedTouches[0].screenX
|
||||||
this.touchendY = e.changedTouches[0].screenY
|
this.touchendY = e.changedTouches[0].screenY
|
||||||
this.handleGesture()
|
this.handleGesture()
|
||||||
@@ -230,6 +334,17 @@ export default {
|
|||||||
closeEvt() {
|
closeEvt() {
|
||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
|
loadEreaderSettings() {
|
||||||
|
try {
|
||||||
|
const settings = localStorage.getItem('ereaderSettings')
|
||||||
|
if (settings) {
|
||||||
|
this.ereaderSettings = JSON.parse(settings)
|
||||||
|
this.settingsUpdated()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load ereader settings', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
registerListeners() {
|
registerListeners() {
|
||||||
this.$eventBus.$on('close-ebook', this.closeEvt)
|
this.$eventBus.$on('close-ebook', this.closeEvt)
|
||||||
document.body.addEventListener('touchstart', this.touchstart)
|
document.body.addEventListener('touchstart', this.touchstart)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="inline-flex">
|
||||||
|
<input v-model="input" type="range" :min="min" :max="max" :step="step" :style="{ width: inputWidth }" />
|
||||||
|
|
||||||
|
<p class="text-xs ml-2">{{ input }}%</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: [String, Number],
|
||||||
|
min: Number,
|
||||||
|
max: Number,
|
||||||
|
step: Number,
|
||||||
|
inputWidth: {
|
||||||
|
type: String,
|
||||||
|
default: 'unset'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
input: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
input[type='range'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type='range']:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* chromium */
|
||||||
|
input[type='range']::-webkit-slider-runnable-track {
|
||||||
|
background-color: rgb(0 0 0 / 0.25);
|
||||||
|
border-radius: 9999px;
|
||||||
|
height: 0.75rem;
|
||||||
|
}
|
||||||
|
input[type='range']::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin-top: -0.25rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: rgb(255 255 255 / 0.7);
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
input[type='range']:focus::-webkit-slider-thumb {
|
||||||
|
border: 1px solid #6b6b6b;
|
||||||
|
outline: 3px solid #6b6b6b;
|
||||||
|
outline-offset: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* firefox */
|
||||||
|
input[type='range']::-moz-range-track {
|
||||||
|
background-color: rgb(0 0 0 / 0.25);
|
||||||
|
border-radius: 9999px;
|
||||||
|
height: 0.75rem;
|
||||||
|
}
|
||||||
|
input[type='range']::-moz-range-thumb {
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
margin-top: -0.25rem;
|
||||||
|
background-color: rgb(255 255 255 / 0.7);
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
input[type='range']:focus::-moz-range-thumb {
|
||||||
|
border: 1px solid #6b6b6b;
|
||||||
|
outline: 3px solid #6b6b6b;
|
||||||
|
outline-offset: 0.125rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<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)">
|
||||||
|
{{ item.text }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: String,
|
||||||
|
/**
|
||||||
|
* [{ "text", "", "value": "" }]
|
||||||
|
*/
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
clickBtn(value) {
|
||||||
|
this.$emit('input', value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toggle-btn-wrapper .toggle-btn:first-child {
|
||||||
|
border-top-left-radius: 0.375rem /* 6px */;
|
||||||
|
border-bottom-left-radius: 0.375rem /* 6px */;
|
||||||
|
}
|
||||||
|
.toggle-btn-wrapper .toggle-btn:last-child {
|
||||||
|
border-top-right-radius: 0.375rem /* 6px */;
|
||||||
|
border-bottom-right-radius: 0.375rem /* 6px */;
|
||||||
|
}
|
||||||
|
.toggle-btn-wrapper .toggle-btn:first-child::before {
|
||||||
|
border-top-left-radius: 0.375rem /* 6px */;
|
||||||
|
border-bottom-left-radius: 0.375rem /* 6px */;
|
||||||
|
}
|
||||||
|
.toggle-btn-wrapper .toggle-btn:last-child::before {
|
||||||
|
border-top-right-radius: 0.375rem /* 6px */;
|
||||||
|
border-bottom-right-radius: 0.375rem /* 6px */;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,6 +17,7 @@ if (Capacitor.getPlatform() != 'web') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$showHideStatusBar = async (show) => {
|
Vue.prototype.$showHideStatusBar = async (show) => {
|
||||||
|
if (Capacitor.getPlatform() === 'web') return
|
||||||
if (show) {
|
if (show) {
|
||||||
StatusBar.show()
|
StatusBar.show()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user