Skip to content

Commit ec46e42

Browse files
committed
Merge remote-tracking branch 'upstream/dev' into ui
# Conflicts: # ui/prototype/index.html # ui/prototype/package-lock.json # ui/prototype/package.json
2 parents c3ed3b0 + 9147a63 commit ec46e42

28 files changed

Lines changed: 2806 additions & 1264 deletions

File tree

docs/openapi.yaml

Lines changed: 2289 additions & 954 deletions
Large diffs are not rendered by default.

spx-gui/.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ VITE_CASDOOR_ORGANIZATION_NAME=""
5050
VITE_DISABLE_AIGC="false"
5151

5252
# Version of spx, keep in sync with the version in `install-spx.sh`.
53-
VITE_SPX_VERSION="2.0.0"
53+
VITE_SPX_VERSION="2.0.1"
5454

5555
# Whether to show the license information (including copyright) in the footer.
5656
VITE_SHOW_LICENSE="false"

spx-gui/install-spx.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -e
44
cd "$(dirname "$0")"
55

66
# Keep this version in sync with `VITE_SPX_VERSION` in `.env`.
7-
SPX_VERSION="2.0.0"
7+
SPX_VERSION="2.0.1"
88

99
SPX_NAME="spx_${SPX_VERSION}"
1010
SPX_RELEASE_URL="https://github.com/goplus/spx/releases/download/v${SPX_VERSION}/spx_web.zip"

spx-gui/src/apis/sb2xbp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { DefaultException } from '@/utils/exception'
22
import { client } from './common'
33
import { humanizeFileSize } from '@/utils/utils'
44

5-
const maxFileSize = 32 * 1024 * 1024 // 32MB
5+
const maxFileSize = 64 * 1024 * 1024 // 64MB
66
const convertTimeout = 60 * 1000 // 60s
77

88
/**

spx-gui/src/components/asset/gen/animation/AnimationVideoPreview.vue

Lines changed: 142 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@ function useVideoPlayer(videoRef: Ref<HTMLVideoElement | null>, rangeRef: WatchS
1212
const duration = ref<number | null>(null)
1313
/** Current playback time in ms */
1414
const currentTime = ref(0)
15+
/** Version async play requests so pause/stop can invalidate stale play() continuations after await. */
16+
let playRequestVersion = 0
17+
/** Latest requested preview seek time in ms while a browser seek may still be in flight. */
18+
let pendingSeekTime: number | null = null
19+
20+
function flushPendingSeek() {
21+
const video = videoRef.value
22+
if (video == null || pendingSeekTime == null || video.seeking) return
23+
const nextTime = pendingSeekTime
24+
pendingSeekTime = null
25+
video.currentTime = nextTime / 1000
26+
}
27+
28+
function seek(timeInMs: number) {
29+
const nextTime = Math.max(0, timeInMs)
30+
currentTime.value = nextTime
31+
pendingSeekTime = nextTime
32+
flushPendingSeek()
33+
}
34+
35+
function pausePlayback() {
36+
playRequestVersion += 1
37+
stopTick()
38+
const video = videoRef.value
39+
if (video == null) return
40+
video.pause()
41+
}
1542
1643
const [startTick, stopTick] = useTick(() => {
1744
const video = videoRef.value
@@ -29,12 +56,13 @@ function useVideoPlayer(videoRef: Ref<HTMLVideoElement | null>, rangeRef: WatchS
2956
currentTime.value = video.currentTime * 1000
3057
})
3158
32-
// If range changes, replay from range start
59+
// Keep preview time inside the current play range when the range changes.
3360
watch(
3461
rangeRef,
3562
(newRange) => {
3663
if (newRange == null || videoRef.value == null) return
37-
videoRef.value.currentTime = newRange.start / 1000
64+
if (currentTime.value >= newRange.start && currentTime.value < newRange.end) return
65+
seek(newRange.start)
3866
},
3967
{ immediate: true }
4068
)
@@ -50,31 +78,36 @@ function useVideoPlayer(videoRef: Ref<HTMLVideoElement | null>, rangeRef: WatchS
5078
(video, _, onCleanup) => {
5179
if (video == null) return
5280
video.addEventListener('loadedmetadata', handleLoadedMetadata)
53-
onCleanup(() => video.removeEventListener('loadedmetadata', handleLoadedMetadata))
81+
video.addEventListener('seeked', flushPendingSeek)
82+
onCleanup(() => {
83+
video.removeEventListener('loadedmetadata', handleLoadedMetadata)
84+
video.removeEventListener('seeked', flushPendingSeek)
85+
})
5486
},
5587
{ immediate: true }
5688
)
5789
5890
async function play() {
5991
if (isPlaying.value) return
6092
isPlaying.value = true
93+
const requestVersion = ++playRequestVersion
6194
startTick()
6295
const video = await untilNotNull(videoRef)
96+
if (!isPlaying.value || requestVersion !== playRequestVersion) return
6397
return video.play()
6498
}
6599
66-
async function stop() {
100+
function stop() {
67101
if (!isPlaying.value) return
68102
isPlaying.value = false
69-
stopTick()
70-
const video = await untilNotNull(videoRef)
71-
video.pause()
103+
pausePlayback()
72104
}
73105
74106
return {
75107
isPlaying,
76108
duration,
77109
currentTime,
110+
seek,
78111
play,
79112
stop
80113
}
@@ -147,7 +180,7 @@ const playingRange = computed<PlayRange | null>(() => {
147180
}
148181
})
149182
150-
const { isPlaying, duration: videoDurationRef, currentTime, play, stop } = useVideoPlayer(videoRef, playingRange)
183+
const { isPlaying, duration: videoDurationRef, currentTime, seek, play, stop } = useVideoPlayer(videoRef, playingRange)
151184
152185
/** Playback progress in range [0, 1] */
153186
const progress = computed(() => {
@@ -175,7 +208,7 @@ watch(
175208
{ immediate: true }
176209
)
177210
178-
const trackRef = ref<HTMLDivElement | null>(null)
211+
const trackInnerRef = ref<HTMLDivElement | null>(null)
179212
// Shows nudge animation on mount, hides after first user hover
180213
const shouldNudge = ref(true)
181214
@@ -196,11 +229,12 @@ const currentTimeStyle = computed(() => {
196229
return { left }
197230
})
198231
199-
type DragTarget = 'start' | 'end'
232+
type DragTarget = 'start' | 'end' | 'preview'
200233
type Dragging = {
201234
target: DragTarget
202235
pointerId: number
203236
rect: DOMRect
237+
wasPlaying: boolean
204238
}
205239
206240
// TODO: Check if we can reuse `useDraggable` from utils
@@ -216,44 +250,100 @@ function stopDragging() {
216250
217251
onScopeDispose(stopDragging)
218252
253+
function getTimeFromPointer(clientX: number, rect: DOMRect, duration: number) {
254+
if (rect.width <= 0) return 0
255+
const ratio = (clientX - rect.left) / rect.width
256+
return clamp(ratio * duration, 0, duration)
257+
}
258+
259+
function getPreviewTimeForDragTarget(target: Exclude<DragTarget, 'preview'>) {
260+
if (target === 'start') return cutStartRef.value
261+
// end - 1ms: keep the right-handle preview just inside the play range so it doesn't immediately wrap to start.
262+
return Math.max(cutStartRef.value, cutEndRef.value - 1)
263+
}
264+
219265
function handleDragStart(target: DragTarget, e: PointerEvent) {
220-
const track = trackRef.value
221-
if (track == null) return
222-
const rect = track.getBoundingClientRect()
266+
// Ignore extra pointerdowns (for example from multi-touch) until the current drag finishes.
267+
if (dragging != null) return
268+
const rect = trackInnerRef.value?.getBoundingClientRect() ?? null
269+
if (rect == null) return
270+
const videoDuration = videoDurationRef.value
271+
if (videoDuration == null) return
272+
shouldNudge.value = false
273+
const wasPlaying = isPlaying.value
274+
stop()
223275
dragging = {
224276
target,
225277
pointerId: e.pointerId,
226-
rect
278+
rect,
279+
wasPlaying
280+
}
281+
if (target === 'preview') {
282+
const time = getTimeFromPointer(e.clientX, dragging.rect, videoDuration)
283+
seek(clamp(time, cutStartRef.value, cutEndRef.value))
284+
} else {
285+
seek(getPreviewTimeForDragTarget(target))
227286
}
228287
e.preventDefault()
229288
window.addEventListener('pointermove', handleDragMove)
230289
window.addEventListener('pointerup', handleDragEnd)
231290
window.addEventListener('pointercancel', handleDragEnd)
232291
}
233292
293+
function handleSegmentPointerDown(e: PointerEvent) {
294+
// Ignore pointerdown events from child handles (segment markers).
295+
if (e.target !== e.currentTarget) return
296+
handleDragStart('preview', e)
297+
}
298+
234299
function handleDragMove(e: PointerEvent) {
235300
if (dragging == null || e.pointerId !== dragging.pointerId) return
236301
const videoDuration = videoDurationRef.value
237302
if (videoDuration == null) return
238-
const ratio = (e.clientX - dragging.rect.left) / dragging.rect.width
239-
const time = snap(ratio * videoDuration)
240-
const minDuration = precision
303+
const time = getTimeFromPointer(e.clientX, dragging.rect, videoDuration)
304+
if (dragging.target === 'preview') {
305+
seek(clamp(time, cutStartRef.value, cutEndRef.value))
306+
return
307+
}
241308
if (dragging.target === 'start') {
242-
cutStartRef.value = clamp(time, 0, cutEndRef.value - minDuration)
309+
cutStartRef.value = adjustStartTime(time)
243310
} else {
244-
cutEndRef.value = clamp(time, cutStartRef.value + minDuration, videoDuration)
311+
cutEndRef.value = adjustEndTime(time, videoDuration)
245312
}
313+
seek(getPreviewTimeForDragTarget(dragging.target))
246314
}
247315
248316
function handleDragEnd(e: PointerEvent) {
249317
if (dragging == null || e.pointerId !== dragging.pointerId) return
318+
const { target, wasPlaying } = dragging
250319
stopDragging()
251-
notifyFramesConfigChanged()
252-
play()
320+
if (target !== 'preview') {
321+
if (target === 'start') {
322+
cutStartRef.value = adjustStartTime(cutStartRef.value, true)
323+
} else {
324+
cutEndRef.value = adjustEndTime(cutEndRef.value, videoDurationRef.value!, true)
325+
}
326+
seek(getPreviewTimeForDragTarget(target))
327+
notifyFramesConfigChanged()
328+
}
329+
if (wasPlaying) play()
330+
}
331+
332+
function adjustStartTime(newTime: number, withSnap = false) {
333+
const time = withSnap ? snap(newTime) : newTime
334+
return clamp(time, 0, cutEndRef.value - minDuration)
335+
}
336+
337+
function adjustEndTime(newTime: number, videoDuration: number, withSnap = false) {
338+
const time = withSnap ? snap(newTime) : newTime
339+
return clamp(time, cutStartRef.value + minDuration, videoDuration)
253340
}
254341
255342
/** Precision for snapping in ms */
256343
const precision = 100
344+
/** Minimum allowed segment duration in ms */
345+
const minDuration = precision
346+
257347
function snap(timeInMs: number) {
258348
return Math.round(timeInMs / precision) * precision
259349
}
@@ -288,9 +378,14 @@ function formatTime(timeInMs: number) {
288378
@stop="stop"
289379
/>
290380
<div class="timeline">
291-
<div ref="trackRef" class="track">
292-
<div class="track-inner" :class="{ nudge: shouldNudge }" @mouseenter.once="shouldNudge = false">
293-
<div class="segment" :style="segmentStyle">
381+
<div class="track">
382+
<div
383+
ref="trackInnerRef"
384+
class="track-inner"
385+
:class="{ nudge: shouldNudge }"
386+
@mouseenter.once="shouldNudge = false"
387+
>
388+
<div class="segment" :style="segmentStyle" @pointerdown="handleSegmentPointerDown">
294389
<button
295390
v-radar="{ name: 'Start marker', desc: 'Drag to adjust start time of extracted segment' }"
296391
class="segment-marker left"
@@ -310,9 +405,10 @@ function formatTime(timeInMs: number) {
310405
</div>
311406
</div>
312407
</div>
313-
<div class="time-row">
314-
<span class="time">{{ formatTime(0) }}</span>
315-
<span class="time">{{ formatTime(videoDurationRef) }}</span>
408+
<div class="flex items-center text-xs">
409+
<span class="w-9 text-grey-700">{{ formatTime(currentTime) }}</span>
410+
<span class="mr-1 text-grey-600">/</span>
411+
<span class="w-9 text-grey-600">{{ formatTime(videoDurationRef) }}</span>
316412
</div>
317413
</div>
318414
</div>
@@ -398,9 +494,12 @@ function formatTime(timeInMs: number) {
398494
position: relative;
399495
width: 100%;
400496
height: 20px;
401-
padding: 0 5px;
402497
background: var(--ui-color-grey-400);
403498
border-radius: 2px;
499+
--segment-marker-width: 10px;
500+
--playhead-width: 2px;
501+
--track-padding: calc(var(--segment-marker-width) + var(--playhead-width) / 2);
502+
padding: 0 var(--track-padding);
404503
}
405504
406505
.track-inner {
@@ -451,7 +550,7 @@ function formatTime(timeInMs: number) {
451550
452551
.segment {
453552
position: absolute;
454-
margin: 0 -5px;
553+
margin: 0 calc(-1 * var(--track-padding));
455554
top: 0;
456555
bottom: 0;
457556
background: var(--ui-color-primary-200);
@@ -460,13 +559,12 @@ function formatTime(timeInMs: number) {
460559
align-items: center;
461560
justify-content: space-between;
462561
padding: 0;
463-
transition:
464-
left 0.1s,
465-
right 0.1s;
562+
cursor: pointer;
563+
touch-action: none;
466564
}
467565
468566
.segment-marker {
469-
width: 10px;
567+
width: var(--segment-marker-width);
470568
height: 20px;
471569
background: var(--ui-color-primary-500);
472570
border-radius: 2px;
@@ -502,22 +600,19 @@ function formatTime(timeInMs: number) {
502600
position: absolute;
503601
top: 0;
504602
bottom: 0;
505-
width: 2px;
603+
width: 0;
506604
pointer-events: none;
507-
transform: translateX(-1px);
508-
background: var(--ui-color-primary-500);
509605
}
510606
511-
.time-row {
512-
display: flex;
513-
align-items: center;
514-
justify-content: space-between;
515-
font-size: 12px;
516-
line-height: 18px;
517-
color: var(--ui-color-grey-700);
518-
}
519-
520-
.time {
521-
white-space: nowrap;
607+
.current-time::before {
608+
content: '';
609+
position: absolute;
610+
top: 0;
611+
bottom: 0;
612+
left: calc(-1 * var(--playhead-width) / 2);
613+
width: var(--playhead-width);
614+
border-radius: 1px;
615+
background: var(--ui-color-yellow-500);
616+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
522617
}
523618
</style>

spx-gui/src/components/common/ListResultWrapper.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const slots = useSlots()
4242
<template v-if="!!slots.empty">
4343
<slot name="empty" :style="extraStyle"></slot>
4444
</template>
45-
<UIEmpty v-else size="large" :style="extraStyle" :img="contentType === 'project' ? 'game' : undefined">
45+
<UIEmpty v-else size="large" :style="extraStyle" :img="contentType === 'project' ? 'box' : 'document'">
4646
<template v-if="contentType === 'project'">
4747
{{ $t({ en: 'No projects', zh: '没有项目' }) }}
4848
</template>

spx-gui/src/components/editor/copilot/CodeChange.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export const description = 'Display a modification based on the existing code.'
1111
export const detailedDescription = `Display a modification based on the existing code. For example,
1212
1313
<code-change file="NiuXiaoQi.spx" line="10" remove-line-count="2">
14-
onStart => {
15-
say "Hello, world!"
14+
onClick => {
15+
say "Hello!"
1616
}
1717
</code-change>
1818

0 commit comments

Comments
 (0)