Skip to content
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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ module.exports = {
'no-console': isProduction ? 'error' : 'warn',
'no-debugger': isProduction ? 'error' : 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'vue/multi-word-component-names': 'off',
},
overrides: [
{
Expand Down
26,031 changes: 12,236 additions & 13,795 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@icon-park/vue-next": "^1.4.0",
"@icon-park/vue-next": "^1.4.2",
"animate.css": "^4.1.1",
"ant-design-vue": "^3.1.0",
"ant-design-vue": "4.2.6",
"chartist": "^0.11.4",
"clipboard": "^2.0.8",
"core-js": "^3.6.5",
Expand All @@ -23,7 +23,7 @@
"lodash": "^4.17.20",
"mitt": "^3.0.0",
"nanoid": "^3.3.3",
"pinia": "^2.0.11",
"pinia": "3.0.3",
"pptxgenjs": "^3.10.0",
"prosemirror-commands": "^1.1.7",
"prosemirror-dropcursor": "^1.3.2",
Expand All @@ -38,7 +38,7 @@
"svg-arc-to-cubic-bezier": "^3.2.0",
"svg-pathdata": "^6.0.0",
"tinycolor2": "^1.4.2",
"vue": "^3.2.31",
"vue": "3.5.16",
"vuedraggable": "^4.0.1"
},
"devDependencies": {
Expand All @@ -60,24 +60,24 @@
"@types/resize-observer-browser": "^0.1.4",
"@types/svg-arc-to-cubic-bezier": "^3.2.0",
"@types/tinycolor2": "^1.4.2",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.2.31",
"@vue/eslint-config-typescript": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-plugin-vuex": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/compiler-sfc": "3.5.16",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/test-utils": "^2.0.0-0",
"babel-plugin-import": "^1.13.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.1.0",
"eslint": "^7.5.0",
"eslint-plugin-vue": "^8.0.0",
"husky": "^7.0.2",
"less": "^4.1.1",
"less-loader": "^7.1.0",
"less-loader": "^10.0.0",
"sass": "^1.32.13",
"sass-loader": "^8.0.2",
"sass-loader": "^12.0.0",
"stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-webpack-plugin": "^2.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/shims-vue.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
const component: DefineComponent<Record<string, any>, Record<string, any>, any>
export default component
}
87 changes: 74 additions & 13 deletions src/store/slides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,35 +51,65 @@ export const useSlidesStore = defineStore('slides', {
},

// 格式化的当前页动画
// 将触发条件为“与上一动画同时”的项目向上合并到序列中的同一位置
// 为触发条件为“上一动画之后”项目的上一项添加自动向下执行标记
// 优先处理带有 startTime 的动画,按时间排序并分组
// 然后处理没有 startTime 的动画,沿用旧的 click, meantime, auto 逻辑
formatedAnimations(state) {
const currentSlide = state.slides[state.slideIndex]
if (!currentSlide?.animations) return []

const els = currentSlide.elements
const elIds = els.map(el => el.id)
const animations = currentSlide.animations.filter(animation => elIds.includes(animation.elId))
const allAnimations = currentSlide.animations.filter(animation => elIds.includes(animation.elId))

const formatedAnimations: FormatedAnimation[] = []
for (const animation of animations) {
if (animation.trigger === 'click' || !formatedAnimations.length) {
formatedAnimations.push({ animations: [animation], autoNext: false })
const timedAnimations: PPTAnimation[] = []
const untimedAnimations: PPTAnimation[] = []

for (const anim of allAnimations) {
if (typeof anim.startTime === 'number') {
timedAnimations.push(anim)
}
else {
untimedAnimations.push(anim)
}
}

// Sort timed animations by startTime
timedAnimations.sort((a, b) => (a.startTime || 0) - (b.startTime || 0))

const formatedTimedAnimations: FormatedAnimation[] = []
if (timedAnimations.length > 0) {
let currentGroup: PPTAnimation[] = [timedAnimations[0]]
for (let i = 1; i < timedAnimations.length; i++) {
if (timedAnimations[i].startTime === currentGroup[0].startTime) {
currentGroup.push(timedAnimations[i])
}
else {
formatedTimedAnimations.push({ animations: currentGroup, autoNext: false })
currentGroup = [timedAnimations[i]]
}
}
formatedTimedAnimations.push({ animations: currentGroup, autoNext: false })
}

const formatedUntimedAnimations: FormatedAnimation[] = []
for (const animation of untimedAnimations) {
if (animation.trigger === 'click' || !formatedUntimedAnimations.length) {
formatedUntimedAnimations.push({ animations: [animation], autoNext: false })
}
else if (animation.trigger === 'meantime') {
const last = formatedAnimations[formatedAnimations.length - 1]
const last = formatedUntimedAnimations[formatedUntimedAnimations.length - 1]
last.animations = last.animations.filter(item => item.elId !== animation.elId)
last.animations.push(animation)
formatedAnimations[formatedAnimations.length - 1] = last
formatedUntimedAnimations[formatedUntimedAnimations.length - 1] = last
}
else if (animation.trigger === 'auto') {
const last = formatedAnimations[formatedAnimations.length - 1]
const last = formatedUntimedAnimations[formatedUntimedAnimations.length - 1]
last.autoNext = true
formatedAnimations[formatedAnimations.length - 1] = last
formatedAnimations.push({ animations: [animation], autoNext: false })
formatedUntimedAnimations[formatedUntimedAnimations.length - 1] = last
formatedUntimedAnimations.push({ animations: [animation], autoNext: false })
}
}
return formatedAnimations
return [...formatedTimedAnimations, ...formatedUntimedAnimations]
},

layouts(state) {
Expand Down Expand Up @@ -186,5 +216,36 @@ export const useSlidesStore = defineStore('slides', {
})
this.slides[slideIndex].elements = (elements as PPTElement[])
},

updateAnimationStartTime(data: { animationId: string, startTime: number }) {
const { animationId, startTime } = data
const slideIndex = this.slideIndex
const animations = this.slides[slideIndex].animations
if (!animations) return

const animationIndex = animations.findIndex(anim => anim.id === animationId)
if (animationIndex > -1) {
animations[animationIndex].startTime = startTime
// Optionally, trigger could be changed here, e.g.:
// animations[animationIndex].trigger = 'timed';
this.slides[slideIndex].animations = [...animations]
// Consider adding history snapshot here if undo/redo is implemented
}
},

updateAnimationDuration(data: { animationId: string, duration: number }) {
const { animationId, duration } = data
const slideIndex = this.slideIndex
const slide = this.slides[slideIndex]
if (!slide.animations) return

const animationIndex = slide.animations.findIndex(anim => anim.id === animationId)
if (animationIndex > -1) {
slide.animations[animationIndex].duration = duration
// Make sure reactivity is triggered if just updating a property in an array item
this.slides[slideIndex].animations = [...slide.animations]
}
// Consider adding history snapshot here if needed
},
},
})
10 changes: 8 additions & 2 deletions src/store/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,16 @@ export const useSnapshotStore = defineStore('snapshot', {
// 快照数大于1时,需要保证撤回操作后维持页面焦点不变:也就是将倒数第二个快照对应的索引设置为当前页的索引
// https://github.com/pipipi-pikachu/PPTist/issues/27
if (snapshotLength >= 2) {
db.snapshots.update(allKeys[snapshotLength - 2] as number, { index: slidesStore.slideIndex })
const updateKey = allKeys[snapshotLength - 2]
if (typeof updateKey === 'number') {
db.snapshots.update(updateKey, { index: slidesStore.slideIndex })
}
}

await db.snapshots.bulkDelete(needDeleteKeys)
const numericKeysToDelete = needDeleteKeys.filter(key => typeof key === 'number') as number[]
if (numericKeysToDelete.length > 0) {
await db.snapshots.bulkDelete(numericKeysToDelete)
}

this.setSnapshotCursor(snapshotLength - 1)
this.setSnapshotLength(snapshotLength)
Expand Down
1 change: 1 addition & 0 deletions src/types/slides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ export interface PPTAnimation {
type: 'in' | 'out' | 'attention';
duration: number;
trigger: 'click' | 'meantime' | 'auto';
startTime?: number;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/utils/prosemirror/plugins/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
selectParentNode,
joinUp,
joinDown,
Command,
} from 'prosemirror-commands'
import { Command } from 'prosemirror-state'

export const buildKeymap = (schema: Schema) => {
const keys = {}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/prosemirror/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ export const getMarkAttrs = (view: EditorView) => {
return node?.marks || []
}

export const getAttrValue = (marks: Mark[], markType: string, attr: string) => {
export const getAttrValue = (marks: readonly Mark[], markType: string, attr: string) => {
for (const mark of marks) {
if (mark.type.name === markType && mark.attrs[attr]) return mark.attrs[attr]
}
return null
}

export const isActiveMark = (marks: Mark[], markType: string) => {
export const isActiveMark = (marks: readonly Mark[], markType: string) => {
for (const mark of marks) {
if (mark.type.name === markType) return true
}
Expand Down
143 changes: 143 additions & 0 deletions src/views/Editor/TimelineEditor/TimelineEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div class="timeline-editor-panel">
<h3>Animation Timeline</h3>
<div class="timeline-controls">
<button @click="zoomIn">Zoom In</button>
<button @click="zoomOut">Zoom Out</button>
<span>Scale: {{ scale }} px/s</span>
</div>
<div class="timeline-grid" ref="timelineGridRef">
<div
v-for="animation in currentAnimations"
:key="animation.id"
class="timeline-animation-item"
:style="getAnimationStyle(animation)"
@mousedown="onDragStart(animation, $event)"
>
{{ getElementShortName(animation.elId) }} - {{ animation.effect }} ({{ animation.startTime || 0 }}ms - {{ (animation.startTime || 0) + animation.duration }}ms)
</div>
</div>
<div class="timeline-axis">
<!-- Axis markers will be generated here -->
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store/slides'
import { useMainStore } from '@/store/main'
import type { PPTAnimation } from '@/types/slides'
import { ENTER_ANIMATIONS, EXIT_ANIMATIONS, ATTENTION_ANIMATIONS } from '@/configs/animation'

const slidesStore = useSlidesStore()
const mainStore = useMainStore()

const { currentSlide } = storeToRefs(slidesStore)
// const { currentSlideAnimations } = storeToRefs(slidesStore) // Unused for now
// const { elements: currentSlideElements } = storeToRefs(mainStore) // Unused for now

const scale = ref(50) // pixels per second
const timelineGridRef = ref<HTMLDivElement | null>(null)

// Flatten all known animation effects for easy lookup (currently unused, but might be useful later)
// const allAnimationEffects = computed(() => {
// const effects: Record<string, string> = {};
// [...ENTER_ANIMATIONS, ...EXIT_ANIMATIONS, ...ATTENTION_ANIMATIONS].forEach(group => {
// group.children.forEach(anim => {
// effects[anim.value] = anim.name
// })
// })
// return effects
// })

const currentAnimations = computed(() => {
return currentSlide.value?.animations || []
})

const getElementShortName = (elId: string) => {
const element = currentSlide.value?.elements.find(el => el.id === elId)
if (!element) return 'Unknown'
return `${element.type.substring(0, 4)}...${elId.substring(0, 3)}`
}

const getAnimationStyle = (animation: PPTAnimation) => {
const startTimeMs = animation.startTime || 0
const durationMs = animation.duration
return {
left: `${(startTimeMs / 1000) * scale.value}px`,
width: `${(durationMs / 1000) * scale.value}px`,
backgroundColor: getAnimationColor(animation.type),
}
}

const getAnimationColor = (type: 'in' | 'out' | 'attention') => {
if (type === 'in') return '#68a490' // Greenish
if (type === 'out') return '#d86344' // Reddish
if (type === 'attention') return '#e8b76a' // Yellowish
return '#ccc'
}

const zoomIn = () => {
scale.value = Math.min(500, scale.value + 20)
}

const zoomOut = () => {
scale.value = Math.max(10, scale.value - 20)
}

// Placeholder for drag functionality
const onDragStart = (animation: PPTAnimation, event: MouseEvent) => {
// console.log('Attempting to drag:', animation.id, event.clientX)
// Drag logic will be more complex, involving tracking mouse movement,
// calculating new startTime, and dispatching store actions.
// This will be implemented in a subsequent subtask.
}

// TODO: Implement timeline axis rendering
// TODO: Implement drag-and-drop to change startTime
// TODO: Implement resizing to change duration
// TODO: Implement action call for duration change

</script>

<style scoped>
.timeline-editor-panel {
padding: 10px;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
height: 300px; /* Example height */
overflow-x: auto;
}
.timeline-controls {
margin-bottom: 10px;
}
.timeline-grid {
position: relative;
height: 200px; /* Example height */
background-image: linear-gradient(to right, #e0e0e0 1px, transparent 1px);
background-size: calc(1s * v-bind(scale + 'px')) 100%; /* Dynamic grid lines */
border: 1px solid #ccc;
}
.timeline-animation-item {
position: absolute;
height: 30px;
line-height: 30px;
color: white;
padding: 0 5px;
border-radius: 3px;
cursor: grab;
font-size: 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
border: 1px solid #333;
}
.timeline-axis {
height: 30px;
background-color: #ddd;
/* Needs markers for time */
}
</style>
Loading