Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 新增演讲者模式 #297

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


<script lang="ts" setup>
import { onMounted } from 'vue'
import { onMounted, nextTick } from 'vue'
import { storeToRefs } from 'pinia'
import { useScreenStore, useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
import { LOCALSTORAGE_KEY_DISCARDED_DB } from '@/configs/storage'
Expand All @@ -37,6 +37,8 @@ if (import.meta.env.MODE !== 'development') {
window.onbeforeunload = () => false
}

const screenStore = useScreenStore()

onMounted(async () => {
api.getFileData('slides').then((slides: Slide[]) => {
slidesStore.setSlides(slides)
Expand All @@ -47,6 +49,20 @@ onMounted(async () => {

await deleteDiscardedDB()
snapshotStore.initSnapshotDatabase()

mainStore.setAvailableFonts()

// 进入演讲者模式
const currentUrl = window.location.href
const url = new URL(currentUrl)
const params = new URLSearchParams(url.search)
const viewMode = params.get('viewMode')
if (viewMode === 'presenter') {
screenStore.setScreening(true)
}
nextTick(() => {
screenStore.changeScreeningMode(viewMode)
})
})

// 应用注销时向 localStorage 中记录下本次 indexedDB 的数据库ID,用于之后清除数据库
Expand Down
18 changes: 16 additions & 2 deletions src/hooks/useScreening.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@ export default () => {
// 进入放映状态(从当前页开始)
const enterScreening = () => {
enterFullscreen()
screenStore.changeScreeningMode('base')
screenStore.setScreening(true)
}

// 进入放映状态(从第一页开始)
const enterScreeningFromStart = () => {
screenStore.changeScreeningMode('base')
slidesStore.updateSlideIndex(0)
enterScreening()
}

// 退出放映状态
const exitScreening = () => {
screenStore.setScreening(false)
if (isFullscreen()) exitFullscreen()
if (screenStore.screeningMode !== 'presenter') {
if (screenStore.presenterBCChannel) {
screenStore.presenterBCChannel.bc.postMessage({
origin: screenStore.presenterBCChannel.bcID,
message: {
action: 'exitScreeningByBC'
},
})
}

screenStore.changeScreeningMode(undefined)
screenStore.setScreening(false)
if (isFullscreen()) exitFullscreen()
}
}

return {
Expand Down
53 changes: 53 additions & 0 deletions src/store/screen.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,70 @@
import { defineStore } from 'pinia'
import { nanoid } from 'nanoid'
import { useSlidesStore } from '@/store'
import useScreening from '@/hooks/useScreening'

export interface screeningBCChannelType {
bcID: string;
bc: BroadcastChannel;
}
export interface ScreenState {
screening: boolean
presenterBCChannel: screeningBCChannelType | null;
screeningMode: 'base' | 'presenter' | undefined; // 演示模式、基础播放模式
}

export const useScreenStore = defineStore('screen', {
state: (): ScreenState => ({
screening: false, // 是否进入放映状态
presenterBCChannel: null,
screeningMode: undefined,
}),

actions: {
setScreening(screening: boolean) {
this.screening = screening
if (screening) {
this.setScreeningBCChannel()
}
else if (this.presenterBCChannel) {
this.presenterBCChannel.bc.close()
this.presenterBCChannel = null
}
},
setScreeningBCChannel() {
// 浏览器页面通讯
const bcID = nanoid()
this.presenterBCChannel = {
bcID,
bc: new BroadcastChannel('presenterMessage'),
}
const { exitScreening } = useScreening()
this.presenterBCChannel.bc.onmessage = function(e) {
const slidesStore = useSlidesStore()
const screenStore = useScreenStore()

const data = e.data
if (data.origin !== bcID) {
switch (data.message.action) {
case 'updateSlideIndexByBC':
slidesStore.updateSlideIndex(data.message.value, 'disableBCAction')
break
case 'exitScreeningByBC':
if (screenStore.screeningMode === 'base') {
exitScreening()
}
else if (screenStore.screeningMode === 'presenter') {
window.close()
}
break
default:
break
}
}
}
},
changeScreeningMode(mode: 'base' | 'presenter' | undefined) {
this.screeningMode = mode
}
},
})
16 changes: 15 additions & 1 deletion src/store/slides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { defineStore } from 'pinia'
import tinycolor from 'tinycolor2'
import { omit } from 'lodash'
import type { Slide, SlideTheme, PPTElement, PPTAnimation } from '@/types/slides'
import { slides } from '@/mocks/slides'
import { theme } from '@/mocks/theme'
import { layouts } from '@/mocks/layout'
import { useScreenStore } from '@/store'

interface RemovePropData {
id: string
Expand Down Expand Up @@ -203,7 +207,17 @@ export const useSlidesStore = defineStore('slides', {
this.slides = slides
},

updateSlideIndex(index: number) {
updateSlideIndex(index: number, disableBCAction?: string) {
const screenStore = useScreenStore()
if (screenStore.screening && !disableBCAction && screenStore.presenterBCChannel) {
screenStore.presenterBCChannel.bc.postMessage({
origin: screenStore.presenterBCChannel.bcID,
message: {
action: 'updateSlideIndexByBC',
value: index,
},
})
}
this.slideIndex = index
},

Expand Down
14 changes: 11 additions & 3 deletions src/views/Screen/BaseView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<IconWrite class="tool-btn" v-tooltip="'画笔工具'" @click="writingBoardToolVisible = true" />
<IconMagic class="tool-btn" v-tooltip="'激光笔'" :class="{ 'active': laserPen }" @click="laserPen = !laserPen" />
<IconStopwatchStart class="tool-btn" v-tooltip="'计时器'" :class="{ 'active': timerlVisible }" @click="timerlVisible = !timerlVisible" />
<IconListView class="tool-btn" v-tooltip="'演讲者视图'" @click="changeViewMode('presenter')" />
<IconListView class="tool-btn" v-tooltip="'演讲者视图'" @click="enterPresenterView" />
<IconOffScreenOne class="tool-btn" v-tooltip="'退出全屏'" v-if="fullscreenState" @click="manualExitFullscreen()" />
<IconFullScreenOne class="tool-btn" v-tooltip="'进入全屏'" v-else @click="enterFullscreen()" />
<IconPower class="tool-btn" v-tooltip="'结束放映'" @click="exitScreening()" />
Expand All @@ -57,7 +57,7 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { useSlidesStore, useScreenStore } from '@/store'
import type { ContextmenuItem } from '@/components/Contextmenu/types'
import { enterFullscreen } from '@/utils/fullscreen'
import useScreening from '@/hooks/useScreening'
Expand Down Expand Up @@ -105,7 +105,15 @@ const writingBoardToolVisible = ref(false)
const timerlVisible = ref(false)
const slideThumbnailModelVisible = ref(false)
const laserPen = ref(false)
const slidesStore = useSlidesStore()

// 演讲者
const enterPresenterView = () => {
const currentUrl = window.location.href
const url = new URL(currentUrl)
window.open(`${url.href}?viewMode=presenter`, '_blank', 'width=1400,height=800,resizable=yes,scrollbars=yes')
slidesStore.updateSlideIndex(0)
}
const contextmenus = (): ContextmenuItem[] => {
return [
{
Expand Down Expand Up @@ -177,7 +185,7 @@ const contextmenus = (): ContextmenuItem[] => {
},
{
text: '演讲者视图',
handler: () => props.changeViewMode('presenter'),
handler: enterPresenterView,
},
{ divider: true },
{
Expand Down
20 changes: 16 additions & 4 deletions src/views/Screen/PresenterView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<span>{{ fullscreenState ? '退出全屏' : '全屏' }}</span>
</div>
<Divider class="divider" />
<div class="tool-btn" @click="exitScreening()"><IconPower class="tool-icon" /><span>结束放映</span></div>
<div class="tool-btn" @click="exitPresenterView()"><IconPower class="tool-icon" /><span>结束放映</span></div>
</div>

<div class="content">
Expand Down Expand Up @@ -79,7 +79,7 @@
<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import { useSlidesStore, useScreenStore } from '@/store'
import type { ContextmenuItem } from '@/components/Contextmenu/types'
import { enterFullscreen } from '@/utils/fullscreen'
import { parseText2Paragraphs } from '@/utils/textParser'
Expand Down Expand Up @@ -119,9 +119,21 @@ const {
} = useExecPlay()

const { slideWidth, slideHeight } = useSlideSize(slideListWrapRef)
const { exitScreening } = useScreening()
const { slidesLoadLimit } = useLoadSlides()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
const screenStore = useScreenStore()

const exitPresenterView = () => {
if (screenStore.presenterBCChannel) {
screenStore.presenterBCChannel.bc.postMessage({
origin: screenStore.presenterBCChannel.bcID,
message: {
action: 'exitScreeningByBC'
},
})
}
window.close()
}

const remarkFontSize = ref(16)
const currentSlideRemark = computed(() => {
Expand Down Expand Up @@ -189,7 +201,7 @@ const contextmenus = (): ContextmenuItem[] => {
{
text: '结束放映',
subText: 'ESC',
handler: exitScreening,
handler: exitPresenterView,
},
]
}
Expand Down
24 changes: 2 additions & 22 deletions src/views/Screen/hooks/useFullscreen.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
import { onMounted, onUnmounted, ref } from 'vue'
import { isFullscreen, exitFullscreen } from '@/utils/fullscreen'
import useScreening from '@/hooks/useScreening'
import { ref } from 'vue'
import { exitFullscreen } from '@/utils/fullscreen'

export default () => {
const fullscreenState = ref(true)
const escExit = ref(true)

const { exitScreening } = useScreening()

const handleFullscreenChange = () => {
fullscreenState.value = isFullscreen()
if (!fullscreenState.value && escExit.value) exitScreening()

escExit.value = true
}

onMounted(() => {
fullscreenState.value = isFullscreen()
document.addEventListener('fullscreenchange', handleFullscreenChange)
document.addEventListener('webkitfullscreenchange', handleFullscreenChange) // Safari 兼容
})
onUnmounted(() => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
})

const manualExitFullscreen = () => {
if (!fullscreenState.value) return
escExit.value = false
Expand Down
13 changes: 8 additions & 5 deletions src/views/Screen/index.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
<template>
<div class="pptist-screen">
<BaseView :changeViewMode="changeViewMode" v-if="viewMode === 'base'" />
<PresenterView :changeViewMode="changeViewMode" v-else-if="viewMode === 'presenter'" />
<BaseView :changeViewMode="changeViewMode" v-if="screeningMode === 'base'" />
<PresenterView :changeViewMode="changeViewMode" v-else-if="screeningMode === 'presenter'" />
</div>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted } from 'vue'
import { KEYS } from '@/configs/hotkey'
import useScreening from '@/hooks/useScreening'

import BaseView from './BaseView.vue'
import PresenterView from './PresenterView.vue'
import { useScreenStore } from '@/store'
import { storeToRefs } from 'pinia'

const viewMode = ref<'base' | 'presenter'>('base')
const screenStore = useScreenStore()
const { screeningMode } = storeToRefs(screenStore)

const changeViewMode = (mode: 'base' | 'presenter') => {
viewMode.value = mode
screenStore.changeScreeningMode(mode)
}

const { exitScreening } = useScreening()
Expand Down