fix(short-video): prevent scroll jank on mobile views and persist sound toggle state
parent
4f999abca5
commit
6c2c5e3120
|
|
@ -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;">
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: '抖音式浏览'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
copyFilePath: '複製文件路徑',
|
||||
|
||||
previewMaskBackgroundOpacity: '預覽遮罩背景透明度',
|
||||
experimentalLRLayout: '實驗性並列布局',
|
||||
experimentalLRLayout: '並列布局',
|
||||
width: '寬度',
|
||||
alwaysOnTooltipInfo: '若關閉此項,信息面板將收起,直至滑鼠移動至屏幕右側時才打開',
|
||||
alwaysOn: '常駐',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]) : ''
|
||||
"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue