Skip to content

Commit 6772289

Browse files
Merge pull request #2029 from frankrousseau/main
[players] Frame and annotation fixes, profile 2FA card, doc cleanup
2 parents 98a976a + c784cf8 commit 6772289

10 files changed

Lines changed: 83 additions & 42 deletions

File tree

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ If you find any issue with Kitsu
4747

4848
## Translations
4949

50-
If you want to contribute to translations, we have an account on the POEditor platform that you can use to participate.
51-
52-
[Contribute to the translation](https://poeditor.com/join/project?hash=fpUejpWDVo)
50+
The source strings live in `src/locales/en.js`. The other locale files in
51+
`src/locales/` are kept in sync with it. If you spot a missing or incorrect
52+
translation, open an issue or a pull request editing the relevant file in
53+
`src/locales/`.
5354

5455

5556
## Improve the documentation

docs/translation.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
## Add a language file
55

6-
1. Get the json file from POEditor.
7-
2. Store it in `src/locales`.
8-
3. Add corresponding entry in the `src/locales/index.js` file.
9-
4. Add an entry in the profile file at the option level.
10-
Be careful, you must use a locale name available in Python Babel or it will
6+
1. Create a `<lang>.json` file in `src/locales`, mirroring the structure of
7+
`en.js` (the source of truth) or an existing locale.
8+
2. Add the corresponding entry in the `src/locales/index.js` file.
9+
3. Add an entry in the profile file at the option level.
10+
Be careful, you must use a locale name available in Python Babel or it will
1111
break the person entry.
1212

1313

src/components/mixins/task.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ export const taskMixin = {
88
currentFps() {
99
const task = this.getTask()
1010
if (!task) return 25
11+
// An entity can override the production fps via data.fps; use it so
12+
// the player builds its frame model on the rate the video was
13+
// actually rendered at (otherwise frames get duplicated/dropped).
14+
// The entity may be a Shot, Edit, Sequence, Episode or Asset, each
15+
// in its own store map — task.entity is only { id } here.
16+
const entityFps = parseFloat(this.getTaskEntity(task)?.data?.fps)
17+
if (entityFps) return entityFps
1118
return parseInt(this.productionMap.get(task.project_id)?.fps) || 25
1219
},
1320

@@ -26,6 +33,19 @@ export const taskMixin = {
2633
return this.currentTask || this.task
2734
},
2835

36+
getTaskEntity(task) {
37+
const getterByType = {
38+
Shot: 'shotMap',
39+
Episode: 'episodeMap',
40+
Sequence: 'sequenceMap',
41+
Edit: 'editMap',
42+
Asset: 'assetMap'
43+
}
44+
const getterName = getterByType[task?.entity_type_name]
45+
if (!getterName || !task?.entity?.id) return null
46+
return this.$store.getters[getterName]?.get(task.entity.id) || null
47+
},
48+
2949
getComments() {
3050
return this.currentTaskComments || this.taskComments
3151
},

src/components/modals/ShortcutModal.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ const shortcutGroups = computed(() => [
112112
icon: markRaw(Pencil),
113113
shortcuts: [
114114
{ keys: ['d'], text: t('keyboard.draw') },
115+
{ keys: ['Shift', 'Mouse Drag'], text: t('keyboard.straight_line') },
116+
{ keys: ['Ctrl', 'Mouse Drag'], text: t('keyboard.constant_width') },
115117
{ keys: ['Ctrl', 'z'], text: t('keyboard.undo') },
116118
{ keys: ['Alt', 'r'], text: t('keyboard.redo') },
117119
{ keys: ['Ctrl', 'c'], text: t('keyboard.copy_annotation') },

src/components/pages/Profile.vue

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,10 @@
156156
type="password"
157157
v-model="passwordForm.password2"
158158
/>
159+
</card>
159160

160-
<div class="two-factor">
161-
<h3 class="card-subtitle">
162-
{{ $t('profile.two_factor_authentication.title') }}
163-
</h3>
164-
<two-factor-authentication-setup />
165-
</div>
161+
<card :title="$t('profile.two_factor_authentication.title')">
162+
<two-factor-authentication-setup />
166163
</card>
167164
</div>
168165

@@ -435,13 +432,6 @@ useHead({ title: computed(() => `${t('profile.title')} - Kitsu`) })
435432
}
436433
}
437434
438-
.card-subtitle {
439-
color: var(--text);
440-
font-size: 1rem;
441-
font-weight: 600;
442-
margin: 1.5rem 0 0.75rem;
443-
}
444-
445435
.grid-two {
446436
display: grid;
447437
gap: 0 1rem;
@@ -458,12 +448,6 @@ useHead({ title: computed(() => `${t('profile.title')} - Kitsu`) })
458448
margin-top: 0.5rem;
459449
}
460450
461-
.two-factor {
462-
border-top: 1px solid var(--border);
463-
margin-top: 1.5rem;
464-
padding-top: 0.5rem;
465-
}
466-
467451
@media screen and (max-width: 768px) {
468452
.profile-content {
469453
margin: 1.5rem auto;

src/components/pages/Task.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
ref="preview-player"
174174
:entity-preview-files="taskEntityPreviews"
175175
:extra-wide="true"
176+
:fps="currentFps"
176177
:last-preview-files="taskPreviews || []"
177178
:link="currentPreviewComment?.links?.[0]"
178179
:previews="currentPreview.previews"

src/components/players/players/PreviewPlayer.vue

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
class="preview-viewer"
4242
:current-frame="currentFrame"
4343
:default-height="defaultHeight"
44+
:fps="fps"
4445
:is-big="big"
4546
:is-comparing="isComparing && isComparisonEnabled"
4647
:is-comparison-overlay="isComparisonOverlay"
@@ -76,6 +77,7 @@
7677
name="comparison-preview-viewer"
7778
:current-frame="currentFrame"
7879
:default-height="defaultHeight"
80+
:fps="fps"
7981
:is-big="big"
8082
:is-comparing="isComparing && isComparisonEnabled"
8183
:is-full-screen="fullScreen"
@@ -475,6 +477,10 @@ const props = defineProps({
475477
type: Boolean,
476478
default: false
477479
},
480+
fps: {
481+
type: Number,
482+
default: null
483+
},
478484
isAssigned: {
479485
type: Boolean,
480486
default: false
@@ -752,7 +758,9 @@ const defaultHeight = computed(() => {
752758
return screen.width > 1300 && (!props.light || props.big) ? bigHeight : 200
753759
})
754760
755-
const fps = computed(() => parseFloat(currentProduction.value?.fps) || 25)
761+
const fps = computed(
762+
() => props.fps || parseFloat(currentProduction.value?.fps) || 25
763+
)
756764
757765
const frameDuration = computed(
758766
() => Math.round((1 / fps.value) * 10000) / 10000
@@ -1013,20 +1021,23 @@ const goNextFrame = () => {
10131021
}
10141022
10151023
const goPreviousDrawing = () => {
1016-
const time = getPreviousAnnotationTime(currentTimeRaw.value)
1017-
jumpToAnnotationFrame(time)
1024+
jumpToAnnotationFrame(getPreviousAnnotationTime(currentTimeRaw.value))
10181025
}
10191026
10201027
const goNextDrawing = () => {
1021-
const time = getNextAnnotationTime(currentTimeRaw.value)
1022-
jumpToAnnotationFrame(time)
1028+
jumpToAnnotationFrame(getNextAnnotationTime(currentTimeRaw.value))
10231029
}
10241030
1025-
const jumpToAnnotationFrame = time => {
1026-
if (time) {
1027-
const annotationTime = time.frame - 1
1031+
const jumpToAnnotationFrame = annotation => {
1032+
if (annotation) {
1033+
// Jump by the annotation's time, not its stored frame. The find and
1034+
// the on-screen display both key off time, whereas annotation.frame
1035+
// can be stale or off by a frame (sometimes even a zero-padded
1036+
// string), which landed 1-2 frames past the drawing and lost the
1037+
// next step.
1038+
const frame = Math.round(annotation.time / frameDuration.value)
10281039
clearCanvas()
1029-
setCurrentFrame(annotationTime)
1040+
setCurrentFrame(frame)
10301041
syncComparisonViewer()
10311042
}
10321043
}

src/components/players/viewers/PreviewViewer.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<video-viewer
2727
ref="videoViewer"
2828
class="video-viewer"
29+
:fps="fps"
2930
:name="name"
3031
:big="isBig"
3132
:default-height="defaultHeight"
@@ -153,6 +154,10 @@ const props = defineProps({
153154
type: Number,
154155
default: 0
155156
},
157+
fps: {
158+
type: Number,
159+
default: null
160+
},
156161
isBig: {
157162
type: Boolean,
158163
default: false

src/components/players/viewers/VideoViewer.vue

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ const props = defineProps({
5353
type: Number,
5454
default: 0
5555
},
56+
fps: {
57+
type: Number,
58+
default: null
59+
},
5660
fullScreen: {
5761
type: Boolean,
5862
default: false
@@ -143,7 +147,9 @@ const video = computed(() => movie.value)
143147
144148
const extension = computed(() => (props.preview ? props.preview.extension : ''))
145149
146-
const fps = computed(() => parseFloat(currentProduction.value?.fps) || 25)
150+
const fps = computed(
151+
() => props.fps || parseFloat(currentProduction.value?.fps) || 25
152+
)
147153
148154
const frameDuration = computed(
149155
() => Math.round((1 / fps.value) * 10000) / 10000
@@ -203,8 +209,17 @@ const getLastPushedCurrentTime = () => {
203209
}
204210
}
205211
212+
// Seek to the middle of the target frame rather than its boundary.
213+
// frame / fps lands right on the edge between two frames, and the rounding
214+
// in frameDuration (0.0833 instead of 0.083333… at 12fps) drifts ~1ms per
215+
// frame, so past ~frame 30 the seek tips into the previous frame and the
216+
// player renders a duplicate. Half a frame of slack dwarfs any rounding
217+
// error, so the correct frame is always decoded — and fps (unrounded) is
218+
// used so there is no drift to begin with.
219+
const frameToTime = frame => (frame + 0.5) / fps.value
220+
206221
const setCurrentFrame = frame => {
207-
setCurrentTime(frame * frameDuration.value)
222+
setCurrentTime(frameToTime(frame))
208223
}
209224
210225
const setCurrentTimeRaw = currentTime => {
@@ -218,7 +233,7 @@ const runSetCurrentTime = () => {
218233
} else {
219234
const currentTime = currentTimeCalls.shift()
220235
if (video.value.currentTime !== currentTime) {
221-
video.value.currentTime = Number(currentTime.toPrecision(4)) + 0.001
236+
video.value.currentTime = currentTime
222237
}
223238
setTimeout(() => {
224239
runSetCurrentTime()
@@ -313,7 +328,7 @@ const play = () => {
313328
const pause = () => {
314329
video.value.pause()
315330
clearInterval(playLoop)
316-
video.value.currentTime = props.currentFrame * frameDuration.value
331+
video.value.currentTime = frameToTime(props.currentFrame)
317332
emit('frame-update', props.currentFrame)
318333
}
319334
@@ -324,15 +339,15 @@ const toggleMute = () => {
324339
const goPreviousFrame = () => {
325340
const nextFrame = props.currentFrame - 1
326341
if (nextFrame < 0) return
327-
video.value.currentTime = nextFrame * frameDuration.value + 0.001
342+
video.value.currentTime = frameToTime(nextFrame)
328343
emit('frame-update', nextFrame)
329344
return nextFrame
330345
}
331346
332347
const goNextFrame = () => {
333348
const nextFrame = props.currentFrame + 1
334349
if (nextFrame >= props.nbFrames) return
335-
video.value.currentTime = nextFrame * frameDuration.value + 0.001
350+
video.value.currentTime = frameToTime(nextFrame)
336351
emit('frame-update', nextFrame)
337352
return nextFrame
338353
}

src/locales/en.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@ export default {
622622
copy_annotation: 'Copy selected annotation',
623623
paste_annotation: 'Paste annotation',
624624
pan_image: 'Pan the image',
625+
straight_line: 'Draw a straight line',
626+
constant_width: 'Draw at a constant width (no pressure)',
625627
move_entity_left: 'Move selected entity to the left',
626628
move_entity_right: 'Move selected entity to the right'
627629
},

0 commit comments

Comments
 (0)