fix(short-video): prevent scroll jank on mobile views and persist sound toggle state

pull/805/head
wuqinchuan 2025-05-25 22:44:31 +08:00 committed by zanllp
parent 4f999abca5
commit 6c2c5e3120
11 changed files with 103 additions and 87 deletions

View File

@ -66,7 +66,8 @@ const emit = defineEmits<{
'dragend': [event: DragEvent, idx: number],
'previewVisibleChange': [value: boolean, last: boolean],
'contextMenuClick': [e: MenuInfo, file: FileNodeInfo, idx: number],
'close-icon-click': []
'close-icon-click': [],
'tiktokView': [file: FileNodeInfo, idx: number]
}>()
const customTags = computed(() => {
@ -152,7 +153,11 @@ const minShowDetailWidth = 160
</div>
<div :class="`idx-${idx} item-content video`" :url="toVideoCoverUrl(file)"
:style="{ 'background-image': `url('${file.cover_url ?? toVideoCoverUrl(file)}')` }" v-else-if="isVideoFile(file.name)"
@click="openVideoModal(file, (id) => emit('contextMenuClick', { key: `toggle-tag-${id}` } as any, file, idx))">
@click="openVideoModal(
file,
(id) => emit('contextMenuClick', { key: `toggle-tag-${id}` } as any, file, idx),
() => emit('tiktokView', file, idx)
)">
<div class="play-icon">
<img :src="play" style="width: 40px;height: 40px;">

View File

@ -11,6 +11,7 @@ import { addCustomTag, getDbBasicInfo, rebuildImageIndex, renameFile } from '@/a
import { useTagStore } from '@/store/useTagStore'
import { useGlobalStore } from '@/store/useGlobalStore'
import { base64ToFile, video2base64 } from '@/util/video'
import { closeImageFullscreenPreview } from '@/util/imagePreviewOperation'
export const openCreateFlodersModal = (base: string) => {
const floderName = ref('')
@ -42,7 +43,11 @@ export const MultiSelectTips = () => (
</p>
)
export const openVideoModal = (file: FileNodeInfo, onTagClick?: (id: string| number) => void) => {
export const openVideoModal = (
file: FileNodeInfo,
onTagClick?: (id: string| number) => void,
onTiktokView?: () => void
) => {
const tagStore = useTagStore()
const global = useGlobalStore()
const isSelected = (id: string | number) => {
@ -70,7 +75,8 @@ export const openVideoModal = (file: FileNodeInfo, onTagClick?: (id: string| num
transition: '.5s all ease',
'user-select': 'none',
}
Modal.confirm({
const modal = Modal.confirm({
width: '80vw',
title: file.name,
icon: null,
@ -110,6 +116,13 @@ export const openVideoModal = (file: FileNodeInfo, onTagClick?: (id: string| num
default: t('download')
}}
</Button>
{onTiktokView && (
<Button onClick={onTiktokViewWrapper} type="primary">
{{
default: t('tiktokView')
}}
</Button>
)}
<Button onClick={onSetCurrFrameAsVideoPoster}>
{{
default: t('setCurrFrameAsVideoPoster')
@ -121,6 +134,11 @@ export const openVideoModal = (file: FileNodeInfo, onTagClick?: (id: string| num
maskClosable: true,
wrapClassName: 'hidden-antd-btns-modal'
})
function onTiktokViewWrapper() {
onTiktokView?.()
closeImageFullscreenPreview()
modal.destroy()
}
}
export const openRebuildImageIndexModal = () => {

View File

@ -58,7 +58,7 @@ You can specify which snapshot to restore to when starting IIB in the global set
copyFilePath: 'Copy file path',
previewMaskBackgroundOpacity: 'Preview Mask Background Opacity',
experimentalLRLayout: 'Experimental Side-by-Side Layout',
experimentalLRLayout: 'Side-by-Side Layout',
width: 'Width',
alwaysOnTooltipInfo: 'If this is turned off, the info panel will be hidden until you move the mouse to the right side of the screen',
alwaysOn: 'Always On',

View File

@ -54,7 +54,7 @@ export const zhHans = {
copySuccess: '复制成功',
copyFilePath: '复制文件路径',
previewMaskBackgroundOpacity: '预览遮罩背景透明度',
experimentalLRLayout: '实验性并列布局',
experimentalLRLayout: '并列布局',
width: '宽度',
alwaysOnTooltipInfo: '若关闭此项,信息面板将收起,直至鼠标移动至屏幕右侧时才打开',
alwaysOn: '常驻',
@ -298,5 +298,5 @@ export const zhHans = {
rebuildImageIndex: '重新构建图像索引',
tagSearchNoResultsMessage: '看起来没匹配到任何结果尝试通过重新构建索引来去掉无用的tag',
'TikTok View': '抖音式浏览',
tiktokView: '抖音式观看'
tiktokView: '抖音式浏览'
}

View File

@ -58,7 +58,7 @@ export const zhHant: Partial<IIBI18nMap> = {
copyFilePath: '複製文件路徑',
previewMaskBackgroundOpacity: '預覽遮罩背景透明度',
experimentalLRLayout: '實驗性並列布局',
experimentalLRLayout: '並列布局',
width: '寬度',
alwaysOnTooltipInfo: '若關閉此項,信息面板將收起,直至滑鼠移動至屏幕右側時才打開',
alwaysOn: '常駐',

View File

@ -199,20 +199,10 @@ const goToPrev = (isTriggerByTouch: boolean = false) => {
updateBuffer()
bufferTransform.value = 0
//
nextTick(() => {
//
if (bufferTransform.value !== 0) {
bufferTransform.value = 0
}
if (dragOffset.value !== 0) {
dragOffset.value = 0
}
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch))
})
//
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch))
}, 200) //
}
@ -232,20 +222,10 @@ const goToNext = (isTriggerByTouch: boolean = false) => {
updateBuffer()
bufferTransform.value = 0
//
nextTick(() => {
//
if (bufferTransform.value !== 0) {
bufferTransform.value = 0
}
if (dragOffset.value !== 0) {
dragOffset.value = 0
}
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch))
})
//
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch))
}, 200) //
}
@ -339,15 +319,10 @@ const resetToCenter = () => {
//
const fixMisalignment = () => {
if (isAnimating.value || isDragging.value) return
if (isDragging.value) return
//
if (bufferTransform.value !== 0 || dragOffset.value !== 0) {
console.warn('检测到错位,正在修复...', {
bufferTransform: bufferTransform.value,
dragOffset: dragOffset.value
})
//
bufferTransform.value = 0
dragOffset.value = 0
@ -355,23 +330,10 @@ const fixMisalignment = () => {
// buffer
updateBuffer()
}
}
//
let alignmentCheckInterval: number | null = null
const startAlignmentCheck = () => {
if (!tiktokStore.isMobile) return
alignmentCheckInterval = window.setInterval(() => {
fixMisalignment()
}, 1000) //
}
const stopAlignmentCheck = () => {
if (alignmentCheckInterval) {
clearInterval(alignmentCheckInterval)
alignmentCheckInterval = null
//
if (isAnimating.value) {
isAnimating.value = false
}
}
@ -499,18 +461,12 @@ onMounted(() => {
document.addEventListener('keydown', handleKeydown)
document.addEventListener('fullscreenchange', handleFullscreenChange)
updateBuffer()
//
startAlignmentCheck()
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('fullscreenchange', handleFullscreenChange)
//
stopAlignmentCheck()
//
videoRefs.value.forEach(video => {
if (video) {
@ -543,9 +499,6 @@ watch(() => tiktokStore.visible, (visible) => {
}
})
//
stopAlignmentCheck()
// 退
if (document.fullscreenElement) {
exitFullscreen()
@ -555,14 +508,6 @@ watch(() => tiktokStore.visible, (visible) => {
nextTick(() => {
controlVideoPlayback()
})
//
startAlignmentCheck()
//
nextTick(() => {
fixMisalignment()
})
}
})
@ -682,25 +627,17 @@ watch(() => isMuted.value, (muted) => {
<div
v-if="tiktokStore.hasPrev"
class="nav-indicator nav-prev"
@touchstart.prevent="goToPrev(false)"
@click="goToPrev(false)"
>
<UpOutlined />
</div>
<!-- 修复错位按钮仅移动设备显示 -->
<!-- <div
v-if="tiktokStore.isMobile"
class="nav-indicator nav-fix"
@click="fixMisalignment"
title="修复错位"
>
<span style="font-size: 12px;">修复</span>
</div> -->
<!-- 下一个指示器 -->
<div
v-if="tiktokStore.hasNext"
class="nav-indicator nav-next"
@touchstart.prevent="goToNext(false)"
@click="goToNext(false)"
>
<DownOutlined />
@ -902,6 +839,7 @@ watch(() => isMuted.value, (muted) => {
justify-content: center;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
z-index: 999;
&:hover {
background: rgba(255, 255, 255, 0.5);

View File

@ -142,6 +142,7 @@ const onTiktokViewClick = () => {
@dragstart="onFileDragStart"
@dragend="onFileDragEnd"
@file-item-click="onFileItemClick"
@tiktok-view="(file, idx) => openTiktokViewWithFiles(images, idx)"
:full-screen-preview-image-url="
images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''
"

View File

@ -17,6 +17,7 @@ import MultiSelectKeep from '@/components/MultiSelectKeep.vue'
import { useGlobalStore } from '@/store/useGlobalStore'
import HistoryRecord from '@/components/HistoryRecord.vue'
import { fuzzySearchHistory, FuzzySearchHistoryRecord } from '@/store/searchHistory'
import { openTiktokViewWithFiles } from '@/util/tiktokHelper'
const props = defineProps<{ tabIdx: number; paneIdx: number, searchScope?: string }>()
const isRegex = ref(false)
@ -249,6 +250,7 @@ const { onClearAllSelected, onSelectAll, onReverseSelect } = useKeepMultiSelect(
:full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''"
:cell-width="cellWidth" :selected="multiSelectedIdxs.includes(idx)"
@context-menu-click="onContextMenuClickU" @dragstart="onFileDragStart" @dragend="onFileDragEnd"
@tiktok-view="(file, idx) => openTiktokViewWithFiles(images, idx)"
:enable-change-indicator="changeIndchecked"
:seed-change-checked="seedChangeChecked"
:get-gen-diff="getGenDiff"

View File

@ -248,6 +248,14 @@ const tagAlphabet = computed(() => {
return res
})
//
const onTiktokViewClick = () => {
//
//
closeImageFullscreenPreview()
emit('contextMenuClick', { key: 'tiktokView' } as any, props.file, props.idx)
}
</script>
<template>
@ -314,6 +322,8 @@ const tagAlphabet = computed(() => {
<a-menu-item key="previewInNewWindow">{{ $t('previewInNewWindow') }}</a-menu-item>
<a-menu-item key="copyPreviewUrl">{{ $t('copySourceFilePreviewLink') }}</a-menu-item>
<a-menu-item key="copyFilePath">{{ $t('copyFilePath') }}</a-menu-item>
<a-menu-divider />
<a-menu-item key="tiktokView" @click="onTiktokViewClick">{{ $t('tiktokView') }}</a-menu-item>
</a-menu>
</template>
</a-dropdown>
@ -325,6 +335,13 @@ const tagAlphabet = computed(() => {
<a-button @click="copyPositivePrompt" v-if="imageGenInfo">{{
$t('copyPositivePrompt')
}}</a-button>
<a-button
@click="onTiktokViewClick"
@touchstart.prevent="onTiktokViewClick"
type="default"
>
{{ $t('tiktokView') }}
</a-button>
</div>
</div>
<div class="gen-info" v-if="showFullContent">

View File

@ -238,6 +238,7 @@ watch(
v-model:show-menu-idx="showMenuIdx" :selected="multiSelectedIdxs.includes(idx)" :cell-width="cellWidth"
@file-item-click="onFileItemClick" @dragstart="onFileDragStart" @dragend="onFileDragEnd"
@preview-visible-change="onPreviewVisibleChange" @context-menu-click="onContextMenuClick"
@tiktok-view="(file, idx) => openTiktokViewWithFiles(sortedFiles, idx)"
:is-selected-mutil-files="multiSelectedIdxs.length > 1"
:enable-change-indicator="changeIndchecked"
:seed-change-checked="seedChangeChecked"

View File

@ -10,6 +10,8 @@ import { GridViewFile, useGlobalStore } from '@/store/useGlobalStore'
import { getRandomImages } from '@/api/db'
import { identity } from '@vueuse/core'
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
import { openTiktokViewWithFiles } from '@/util/tiktokHelper'
import MultiSelectKeep from '@/components/MultiSelectKeep.vue'
import { LeftCircleOutlined, RightCircleOutlined } from '@/icon'
import { copy2clipboardI18n } from '@/util'
@ -40,6 +42,17 @@ const fetch = async () => {
onScroll()
}
}
// TikTok View
const onTiktokViewClick = () => {
if (files.value.length === 0) {
message.warn('没有图片可以浏览')
return
}
//
openTiktokViewWithFiles(files.value, previewIdx.value || 0)
}
onMounted(fetch)
const { stackViewEl, multiSelectedIdxs, stack, scroller } = useHookShareState({
images: files as any
@ -67,7 +80,24 @@ const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
<MultiSelectKeep :show="!!multiSelectedIdxs.length || g.keepMultiSelect" @clear-all-selected="onClearAllSelected"
@select-all="onSelectAll" @reverse-select="onReverseSelect" />
<div class="refresh-button">
<a-button @click="fetch" type="primary" :loading="loading" shape="round">{{ $t('shuffle') }}</a-button>
<a-button
@click="fetch"
@touchstart.prevent="fetch"
type="primary"
:loading="loading"
shape="round"
>
{{ $t('shuffle') }}
</a-button>
<a-button
@click="onTiktokViewClick"
@touchstart.prevent="onTiktokViewClick"
type="default"
:disabled="!files?.length"
shape="round"
>
{{ $t('tiktokView') }}
</a-button>
</div>
<AModal v-model:visible="showGenInfo" width="70vw" mask-closable @ok="showGenInfo = false">
<template #cancelText />
@ -90,7 +120,7 @@ const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
<file-item :idx="idx" :file="file" :cell-width="cellWidth" :full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''
" @context-menu-click="onContextMenuClickU" @preview-visible-change="onPreviewVisibleChange"
:is-selected-mutil-files="multiSelectedIdxs.length > 1" :selected="multiSelectedIdxs.includes(idx)"
@file-item-click="onFileItemClick" />
@file-item-click="onFileItemClick" @tiktok-view="(file, idx) => openTiktokViewWithFiles(files, idx)" />
</template>
</RecycleScroller>
<div v-if="previewing" class="preview-switch">
@ -124,6 +154,10 @@ const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
background: white;
border-radius: 9999px;
box-shadow: 0 0 20px var(--zp-secondary);
padding: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.file-list {