| 1 | <script setup lang="ts"> |
| 2 | import { computed, ref, shallowRef } from 'vue' |
| 3 | import CustomNavControls from '#slidev/custom-nav-controls' |
| 4 | import { useDrawings } from '../composables/useDrawings' |
| 5 | import { useNav } from '../composables/useNav' |
| 6 | import { configs } from '../env' |
| 7 | import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark' |
| 8 | import { activeElement, breakpoints, cursorStyle, fullscreen, hasViewerCssFilter, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterCursor, togglePresenterLayout } from '../state' |
| 9 | import { downloadPDF } from '../utils' |
| 10 | import IconButton from './IconButton.vue' |
| 11 | import MenuButton from './MenuButton.vue' |
| 12 | import Settings from './Settings.vue' |
| 13 | import SyncControls from './SyncControls.vue' |
| 14 | |
| 15 | import VerticalDivider from './VerticalDivider.vue' |
| 16 | |
| 17 | const props = defineProps({ |
| 18 | persist: { |
| 19 | default: false, |
| 20 | }, |
| 21 | }) |
| 22 | |
| 23 | const { |
| 24 | currentSlideNo, |
| 25 | hasNext, |
| 26 | hasPrev, |
| 27 | isEmbedded, |
| 28 | isPresenter, |
| 29 | isPresenterAvailable, |
| 30 | next, |
| 31 | prev, |
| 32 | total, |
| 33 | enterPresenter, |
| 34 | exitPresenter, |
| 35 | } = useNav() |
| 36 | const { |
| 37 | brush, |
| 38 | drawingEnabled, |
| 39 | } = useDrawings() |
| 40 | |
| 41 | const md = breakpoints.smaller('md') |
| 42 | const { isFullscreen, toggle: toggleFullscreen } = fullscreen |
| 43 | |
| 44 | const root = ref<HTMLDivElement>() |
| 45 | function onMouseLeave() { |
| 46 | if (root.value && activeElement.value && root.value.contains(activeElement.value)) |
| 47 | activeElement.value.blur() |
| 48 | } |
| 49 | |
| 50 | const barStyle = computed(() => props.persist |
| 51 | ? 'text-$slidev-controls-foreground bg-transparent' |
| 52 | : 'rounded-md bg-main shadow-xl border border-main') |
| 53 | |
| 54 | const RecordingControls = shallowRef<any>() |
| 55 | if (__SLIDEV_FEATURE_RECORD__) |
| 56 | import('./RecordingControls.vue').then(v => RecordingControls.value = v.default) |
| 57 | </script> |
| 58 | |
| 59 | <template> |
| 60 | <nav ref="root" class="flex flex-col"> |
| 61 | <div |
| 62 | class="flex flex-wrap-reverse text-xl gap-0.5 p-1 lg:p-2" |
| 63 | :class="barStyle" |
| 64 | @mouseleave="onMouseLeave" |
| 65 | > |
| 66 | <IconButton v-if="!isEmbedded" :title="isFullscreen ? 'Close fullscreen' : 'Enter fullscreen'" @click="toggleFullscreen"> |
| 67 | <div v-if="isFullscreen" class="i-carbon:minimize" /> |
| 68 | <div v-else class="i-carbon:maximize" /> |
| 69 | </IconButton> |
| 70 | <IconButton :disabled="!hasPrev" title="Go to previous slide" @click="prev"> |
| 71 | <div class="i-carbon:arrow-left" /> |
| 72 | </IconButton> |
| 73 | <IconButton :disabled="!hasNext" title="Go to next slide" @click="next"> |
| 74 | <div class="i-carbon:arrow-right" /> |
| 75 | </IconButton> |
| 76 | <IconButton v-if="!isEmbedded" title="Show slide overview" @click="toggleOverview()"> |
| 77 | <div class="i-carbon:apps" /> |
| 78 | </IconButton> |
| 79 | <IconButton |
| 80 | v-if="!isColorSchemaConfigured" |
| 81 | :title="isDark ? 'Switch to light mode theme' : 'Switch to dark mode theme'" |
| 82 | @click="toggleDark()" |
| 83 | > |
| 84 | <div v-if="isDark" class="i-carbon-moon" /> |
| 85 | <div v-else class="i-carbon-sun" /> |
| 86 | </IconButton> |
| 87 | |
| 88 | <VerticalDivider /> |
| 89 | |
| 90 | <template v-if="!isEmbedded"> |
| 91 | <template v-if="!isPresenter && !md && RecordingControls"> |
| 92 | <RecordingControls /> |
| 93 | <VerticalDivider /> |
| 94 | </template> |
| 95 | |
| 96 | <IconButton |
| 97 | v-if="isPresenter" |
| 98 | :title="cursorStyle === 'laser' |
| 99 | ? (showPresenterCursor ? 'Disable laser pointer' : 'Enable laser pointer') |
| 100 | : (showPresenterCursor ? 'Hide presenter cursor' : 'Show presenter cursor')" |
| 101 | :active="showPresenterCursor" |
| 102 | @click="togglePresenterCursor()" |
| 103 | > |
| 104 | <template v-if="cursorStyle === 'laser'"> |
| 105 | <div v-if="showPresenterCursor" class="i-carbon-magic-wand-filled color-red-600 dark:color-red-400" /> |
| 106 | <div v-else class="i-carbon-magic-wand" /> |
| 107 | </template> |
| 108 | <template v-else> |
| 109 | <div v-if="showPresenterCursor" class="i-ph-cursor-fill" /> |
| 110 | <div v-else class="i-ph-cursor-duotone" /> |
| 111 | </template> |
| 112 | </IconButton> |
| 113 | <IconButton |
| 114 | v-if="__SLIDEV_FEATURE_DRAWINGS__ && (!configs.drawings.presenterOnly || isPresenter)" |
| 115 | class="relative" |
| 116 | :title="drawingEnabled ? 'Hide drawing toolbar' : 'Show drawing toolbar'" |
| 117 | :active="drawingEnabled" |
| 118 | @click="drawingEnabled = !drawingEnabled" |
| 119 | > |
| 120 | <div class="i-carbon:pen" /> |
| 121 | <div |
| 122 | v-if="drawingEnabled" |
| 123 | class="absolute left-1 right-1 bottom-0 h-0.7 rounded-full" |
| 124 | :style="{ background: brush.color }" |
| 125 | /> |
| 126 | </IconButton> |
| 127 | |
| 128 | <VerticalDivider /> |
| 129 | |
| 130 | <IconButton v-if="isPresenter" title="Play Mode" @click="exitPresenter"> |
| 131 | <div class="i-carbon:presentation-file" /> |
| 132 | </IconButton> |
| 133 | <IconButton v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" title="Presenter Mode" @click="enterPresenter"> |
| 134 | <div class="i-carbon:user-speaker" /> |
| 135 | </IconButton> |
| 136 | <IconButton |
| 137 | v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__" |
| 138 | :title="showEditor ? 'Hide editor' : 'Show editor'" |
| 139 | class="lt-md:hidden" |
| 140 | @click="showEditor = !showEditor" |
| 141 | > |
| 142 | <div class="i-carbon:text-annotation-toggle" /> |
| 143 | </IconButton> |
| 144 | </template> |
| 145 | |
| 146 | <template v-if="!__DEV__"> |
| 147 | <IconButton v-if="configs.download" title="Download as PDF" @click="downloadPDF"> |
| 148 | <div class="i-carbon:download" /> |
| 149 | </IconButton> |
| 150 | </template> |
| 151 | |
| 152 | <template v-if="__SLIDEV_FEATURE_BROWSER_EXPORTER__ && !isEmbedded && !isPresenter"> |
| 153 | <IconButton title="Browser Exporter" to="/export"> |
| 154 | <div class="i-carbon:document-pdf" /> |
| 155 | </IconButton> |
| 156 | </template> |
| 157 | |
| 158 | <IconButton |
| 159 | v-if="!isPresenter && configs.info && !isEmbedded" |
| 160 | title="Show info" |
| 161 | @click="showInfoDialog = !showInfoDialog" |
| 162 | > |
| 163 | <div class="i-carbon:information" /> |
| 164 | </IconButton> |
| 165 | |
| 166 | <template v-if="!isEmbedded"> |
| 167 | <VerticalDivider /> |
| 168 | |
| 169 | <IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial flex items-center" @click="togglePresenterLayout"> |
| 170 | <div class="i-carbon:template" /> |
| 171 | {{ presenterLayout }} |
| 172 | </IconButton> |
| 173 | |
| 174 | <SyncControls v-if="__SLIDEV_FEATURE_PRESENTER__" /> |
| 175 | |
| 176 | <MenuButton> |
| 177 | <template #button="{ value }"> |
| 178 | <IconButton title="More Options" :active="value"> |
| 179 | <div class="i-carbon:settings-adjust" /> |
| 180 | <div v-if="hasViewerCssFilter" w-2 h-2 bg-primary rounded-full absolute top-0.5 right-0.5 /> |
| 181 | </IconButton> |
| 182 | </template> |
| 183 | <template #menu> |
| 184 | <Settings /> |
| 185 | </template> |
| 186 | </MenuButton> |
| 187 | </template> |
| 188 | |
| 189 | <VerticalDivider v-if="!isEmbedded" /> |
| 190 | |
| 191 | <div class="px2 my-auto"> |
| 192 | <span class="text-lg">{{ currentSlideNo }}</span> |
| 193 | <span class="opacity-50 text-sm"> / {{ total }}</span> |
| 194 | </div> |
| 195 | |
| 196 | <CustomNavControls /> |
| 197 | </div> |
| 198 | </nav> |
| 199 | </template> |
| 200 |