mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat: enhance Live Photo functionality and improve user experience
- Added support for long press to play Live Photos on mobile devices. - Implemented a ref for LivePhoto component to control play and stop actions. - Updated localization files to include new strings for Live Photo playback status. - Enhanced CSS styles to prevent text selection and improve layout consistency. - Refactored ExifPanel and ProgressiveImage components for better integration with Live Photo features. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -82,7 +82,7 @@ export const ExifPanel: FC<{
|
||||
|
||||
<ScrollArea
|
||||
rootClassName="flex-1 min-h-0 overflow-auto lg:overflow-hidden"
|
||||
viewportClassName="px-4 pb-4"
|
||||
viewportClassName="px-4 pb-4 [&_*]:select-text"
|
||||
>
|
||||
<div className={`space-y-${isMobile ? '3' : '4'}`}>
|
||||
{/* 基本信息和标签 - 合并到一个 section */}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { m, useAnimationControls } from 'motion/react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, m, useAnimationControls } from 'motion/react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { clsxm } from '~/lib/cn'
|
||||
@@ -19,30 +25,36 @@ interface LivePhotoProps {
|
||||
isCurrentImage: boolean
|
||||
/** 自定义样式类名 */
|
||||
className?: string
|
||||
onPlayingChange?: (isPlaying: boolean) => void
|
||||
}
|
||||
|
||||
export interface LivePhotoHandle {
|
||||
play: () => void
|
||||
stop: () => void
|
||||
getIsVideoLoaded: () => boolean
|
||||
}
|
||||
|
||||
export const LivePhoto = ({
|
||||
ref,
|
||||
videoUrl,
|
||||
imageLoaderManager,
|
||||
loadingIndicatorRef,
|
||||
isCurrentImage,
|
||||
className,
|
||||
}: LivePhotoProps) => {
|
||||
onPlayingChange,
|
||||
}: LivePhotoProps & { ref?: React.RefObject<LivePhotoHandle | null> }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Live Photo 相关状态
|
||||
const [isPlayingLivePhoto, setIsPlayingLivePhoto] = useState(false)
|
||||
const [livePhotoVideoLoaded, setLivePhotoVideoLoaded] = useState(false)
|
||||
const [isConvertingVideo, setIsConvertingVideo] = useState(false)
|
||||
const [conversionMethod, setConversionMethod] = useState<string>('')
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const videoAnimateController = useAnimationControls()
|
||||
|
||||
// Live Photo hover 相关
|
||||
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [isLongPressing, setIsLongPressing] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
onPlayingChange?.(isPlayingLivePhoto)
|
||||
}, [isPlayingLivePhoto, onPlayingChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -53,9 +65,7 @@ export const LivePhoto = ({
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsConvertingVideo(true)
|
||||
|
||||
const processVideo = async () => {
|
||||
try {
|
||||
const videoResult = await imageLoaderManager.processLivePhotoVideo(
|
||||
@@ -67,11 +77,9 @@ export const LivePhoto = ({
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (videoResult.conversionMethod) {
|
||||
setConversionMethod(videoResult.conversionMethod)
|
||||
}
|
||||
|
||||
setLivePhotoVideoLoaded(true)
|
||||
} catch (videoError) {
|
||||
console.error('Failed to process Live Photo video:', videoError)
|
||||
@@ -79,7 +87,6 @@ export const LivePhoto = ({
|
||||
setIsConvertingVideo(false)
|
||||
}
|
||||
}
|
||||
|
||||
processVideo()
|
||||
}, [
|
||||
isCurrentImage,
|
||||
@@ -90,64 +97,28 @@ export const LivePhoto = ({
|
||||
loadingIndicatorRef,
|
||||
])
|
||||
|
||||
// 清理函数
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clean up timers
|
||||
if (hoverTimerRef.current) {
|
||||
clearTimeout(hoverTimerRef.current)
|
||||
hoverTimerRef.current = null
|
||||
}
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current)
|
||||
longPressTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 重置状态(当不是当前图片时)
|
||||
useEffect(() => {
|
||||
if (!isCurrentImage) {
|
||||
setIsPlayingLivePhoto(false)
|
||||
setLivePhotoVideoLoaded(false)
|
||||
setIsConvertingVideo(false)
|
||||
setConversionMethod('')
|
||||
setIsLongPressing(false)
|
||||
|
||||
// Clean up timers
|
||||
if (hoverTimerRef.current) {
|
||||
clearTimeout(hoverTimerRef.current)
|
||||
hoverTimerRef.current = null
|
||||
}
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current)
|
||||
longPressTimerRef.current = null
|
||||
}
|
||||
|
||||
// Reset video animation
|
||||
videoAnimateController.set({ opacity: 0 })
|
||||
}
|
||||
}, [isCurrentImage, videoAnimateController])
|
||||
|
||||
// Live Photo hover 处理
|
||||
const handleBadgeMouseEnter = useCallback(() => {
|
||||
const play = useCallback(async () => {
|
||||
if (!livePhotoVideoLoaded || isPlayingLivePhoto || isConvertingVideo) return
|
||||
|
||||
hoverTimerRef.current = setTimeout(async () => {
|
||||
setIsPlayingLivePhoto(true)
|
||||
|
||||
// 开始淡入动画
|
||||
await videoAnimateController.start({
|
||||
opacity: 1,
|
||||
transition: { duration: 0.15, ease: 'easeOut' },
|
||||
})
|
||||
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.currentTime = 0
|
||||
video.play()
|
||||
}
|
||||
}, 200) // 200ms hover 延迟
|
||||
setIsPlayingLivePhoto(true)
|
||||
await videoAnimateController.start({
|
||||
opacity: 1,
|
||||
transition: { duration: 0.15, ease: 'easeOut' },
|
||||
})
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.currentTime = 0
|
||||
video.play()
|
||||
}
|
||||
}, [
|
||||
livePhotoVideoLoaded,
|
||||
isPlayingLivePhoto,
|
||||
@@ -155,142 +126,69 @@ export const LivePhoto = ({
|
||||
videoAnimateController,
|
||||
])
|
||||
|
||||
const handleBadgeMouseLeave = useCallback(async () => {
|
||||
if (hoverTimerRef.current) {
|
||||
clearTimeout(hoverTimerRef.current)
|
||||
hoverTimerRef.current = null
|
||||
const stop = useCallback(async () => {
|
||||
if (!isPlayingLivePhoto) return
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.pause()
|
||||
video.currentTime = 0
|
||||
}
|
||||
|
||||
if (isPlayingLivePhoto) {
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.pause()
|
||||
video.currentTime = 0
|
||||
}
|
||||
|
||||
// 开始淡出动画
|
||||
await videoAnimateController.start({
|
||||
opacity: 0,
|
||||
transition: { duration: 0.2, ease: 'easeIn' },
|
||||
})
|
||||
|
||||
setIsPlayingLivePhoto(false)
|
||||
}
|
||||
}, [isPlayingLivePhoto, videoAnimateController])
|
||||
|
||||
// 视频播放结束处理
|
||||
const handleVideoEnded = useCallback(async () => {
|
||||
// 播放结束时淡出
|
||||
await videoAnimateController.start({
|
||||
opacity: 0,
|
||||
transition: { duration: 0.2, ease: 'easeIn' },
|
||||
})
|
||||
|
||||
setIsPlayingLivePhoto(false)
|
||||
}, [videoAnimateController])
|
||||
}, [isPlayingLivePhoto, videoAnimateController])
|
||||
|
||||
// Live Photo 长按处理(移动端)
|
||||
const handleTouchStart = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (
|
||||
!livePhotoVideoLoaded ||
|
||||
isPlayingLivePhoto ||
|
||||
isConvertingVideo ||
|
||||
e.touches.length > 1 // 多指触摸不触发长按
|
||||
)
|
||||
return
|
||||
useImperativeHandle(ref, () => ({
|
||||
play,
|
||||
stop,
|
||||
getIsVideoLoaded: () => livePhotoVideoLoaded,
|
||||
}))
|
||||
|
||||
longPressTimerRef.current = setTimeout(async () => {
|
||||
setIsLongPressing(true)
|
||||
setIsPlayingLivePhoto(true)
|
||||
const handleVideoEnded = useCallback(() => {
|
||||
stop()
|
||||
}, [stop])
|
||||
|
||||
// 开始淡入动画
|
||||
await videoAnimateController.start({
|
||||
opacity: 1,
|
||||
transition: { duration: 0.15, ease: 'easeOut' },
|
||||
})
|
||||
// Desktop hover logic
|
||||
const handleBadgeMouseEnter = useCallback(() => {
|
||||
if (isMobileDevice) return
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current)
|
||||
hoverTimerRef.current = setTimeout(play, 200)
|
||||
}, [play])
|
||||
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.currentTime = 0
|
||||
video.play()
|
||||
}
|
||||
}, 500) // 500ms 长按延迟
|
||||
},
|
||||
[
|
||||
livePhotoVideoLoaded,
|
||||
isPlayingLivePhoto,
|
||||
isConvertingVideo,
|
||||
videoAnimateController,
|
||||
],
|
||||
)
|
||||
|
||||
const handleTouchEnd = useCallback(async () => {
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current)
|
||||
longPressTimerRef.current = null
|
||||
}
|
||||
|
||||
if (isLongPressing && isPlayingLivePhoto) {
|
||||
setIsLongPressing(false)
|
||||
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.pause()
|
||||
video.currentTime = 0
|
||||
}
|
||||
|
||||
// 开始淡出动画
|
||||
await videoAnimateController.start({
|
||||
opacity: 0,
|
||||
transition: { duration: 0.2, ease: 'easeIn' },
|
||||
})
|
||||
|
||||
setIsPlayingLivePhoto(false)
|
||||
}
|
||||
}, [isLongPressing, isPlayingLivePhoto, videoAnimateController])
|
||||
|
||||
const handleTouchMove = useCallback(() => {
|
||||
// 触摸移动时取消长按
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current)
|
||||
longPressTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
const handleBadgeMouseLeave = useCallback(() => {
|
||||
if (isMobileDevice) return
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current)
|
||||
stop()
|
||||
}, [stop])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Live Photo 视频 */}
|
||||
<m.video
|
||||
ref={videoRef}
|
||||
className={clsxm(
|
||||
'absolute inset-0 z-10 h-full w-full object-contain',
|
||||
!isPlayingLivePhoto && 'hidden',
|
||||
'pointer-events-none absolute inset-0 z-10 h-full w-full object-contain',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
opacity: isPlayingLivePhoto ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
}}
|
||||
muted
|
||||
playsInline
|
||||
onEnded={handleVideoEnded}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchMove={handleTouchMove}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={videoAnimateController}
|
||||
/>
|
||||
|
||||
{/* Live Photo 标识 */}
|
||||
<div
|
||||
className={clsxm(
|
||||
'absolute z-50 flex items-center space-x-1 rounded-xl bg-black/50 px-1 py-1 text-xs text-white cursor-pointer transition-all duration-200 hover:bg-black/70',
|
||||
'absolute z-20 flex items-center space-x-1 rounded-xl bg-black/50 px-1 py-1 text-xs text-white transition-all duration-200',
|
||||
!isMobileDevice && 'cursor-pointer hover:bg-black/70',
|
||||
import.meta.env.DEV ? 'top-16 right-4' : 'top-12 lg:top-4 left-4',
|
||||
)}
|
||||
onMouseEnter={handleBadgeMouseEnter}
|
||||
onMouseLeave={handleBadgeMouseLeave}
|
||||
title={
|
||||
isMobileDevice
|
||||
? t('photo.live.tooltip.mobile.main')
|
||||
: t('photo.live.tooltip.desktop.main')
|
||||
}
|
||||
>
|
||||
{isConvertingVideo ? (
|
||||
<div className="flex items-center gap-1 px-1">
|
||||
@@ -307,13 +205,29 @@ export const LivePhoto = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作提示 */}
|
||||
<div className="pointer-events-none absolute bottom-4 left-1/2 z-20 -translate-x-1/2 rounded bg-black/50 px-2 py-1 text-xs text-white opacity-0 duration-200 group-hover:opacity-50">
|
||||
<AnimatePresence>
|
||||
{isPlayingLivePhoto && (
|
||||
<m.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="pointer-events-none absolute bottom-4 left-1/2 z-20 -translate-x-1/2"
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded bg-black/50 px-2 py-1 text-xs text-white">
|
||||
<i className="i-mingcute-live-photo-line" />
|
||||
<span>{t('photo.live.playing')}</span>
|
||||
</div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div
|
||||
className={clsxm(
|
||||
'pointer-events-none absolute bottom-4 left-1/2 z-20 -translate-x-1/2 rounded bg-black/50 px-2 py-1 text-xs text-white opacity-0 duration-200 group-hover:opacity-50',
|
||||
isPlayingLivePhoto && 'opacity-0!',
|
||||
)}
|
||||
>
|
||||
{isConvertingVideo
|
||||
? t('photo.live.converting.detail', {
|
||||
method: 'transmux',
|
||||
})
|
||||
? t('photo.live.converting.detail', { method: 'transmux' })
|
||||
: isMobileDevice
|
||||
? t('photo.live.tooltip.mobile.zoom')
|
||||
: t('photo.live.tooltip.desktop.zoom')}
|
||||
@@ -321,3 +235,5 @@ export const LivePhoto = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
LivePhoto.displayName = 'LivePhoto'
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
useShowContextMenu,
|
||||
} from '~/atoms/context-menu'
|
||||
import { clsxm } from '~/lib/cn'
|
||||
import { isMobileDevice } from '~/lib/device-viewport'
|
||||
import { canUseWebGL } from '~/lib/feature'
|
||||
import { ImageLoaderManager } from '~/lib/image-loader-manager'
|
||||
|
||||
import { SlidingNumber } from '../number/SlidingNumber'
|
||||
import type { LivePhotoHandle } from './LivePhoto'
|
||||
import { LivePhoto } from './LivePhoto'
|
||||
import type { LoadingIndicatorRef } from './LoadingIndicator'
|
||||
import { LoadingIndicator } from './LoadingIndicator'
|
||||
@@ -88,6 +90,9 @@ export const ProgressiveImage = ({
|
||||
|
||||
const loadingIndicatorRef = useRef<LoadingIndicatorRef>(null)
|
||||
const imageLoaderManagerRef = useRef<ImageLoaderManager | null>(null)
|
||||
const livePhotoRef = useRef<LivePhotoHandle>(null)
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [isLivePhotoPlaying, setIsLivePhotoPlaying] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (highResLoaded || error || !isCurrentImage) return
|
||||
@@ -167,6 +172,29 @@ export const ProgressiveImage = ({
|
||||
[onZoomChange],
|
||||
)
|
||||
|
||||
const handleLongPressStart = useCallback(() => {
|
||||
if (!isMobileDevice) return
|
||||
const playVideo = () => livePhotoRef.current?.play()
|
||||
if (
|
||||
!isLivePhoto ||
|
||||
!livePhotoRef.current?.getIsVideoLoaded() ||
|
||||
isLivePhotoPlaying
|
||||
) {
|
||||
return
|
||||
}
|
||||
longPressTimerRef.current = setTimeout(playVideo, 200)
|
||||
}, [isLivePhoto, isLivePhotoPlaying])
|
||||
|
||||
const handleLongPressEnd = useCallback(() => {
|
||||
if (!isMobileDevice) return
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current)
|
||||
}
|
||||
if (isLivePhotoPlaying) {
|
||||
livePhotoRef.current?.stop()
|
||||
}
|
||||
}, [isLivePhotoPlaying])
|
||||
|
||||
const handleWebGLLoadingStateChange = useCallback(
|
||||
(
|
||||
isLoading: boolean,
|
||||
@@ -216,7 +244,14 @@ export const ProgressiveImage = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsxm('relative overflow-hidden', className)}>
|
||||
<div
|
||||
className={clsxm('relative overflow-hidden', className)}
|
||||
onMouseDown={handleLongPressStart}
|
||||
onMouseUp={handleLongPressEnd}
|
||||
onMouseLeave={handleLongPressEnd}
|
||||
onTouchStart={handleLongPressStart}
|
||||
onTouchEnd={handleLongPressEnd}
|
||||
>
|
||||
{/* 缩略图 */}
|
||||
{thumbnailSrc && !isHighResImageRendered && (
|
||||
<img
|
||||
@@ -346,10 +381,12 @@ export const ProgressiveImage = ({
|
||||
isCurrentImage &&
|
||||
imageLoaderManagerRef.current && (
|
||||
<LivePhoto
|
||||
ref={livePhotoRef}
|
||||
videoUrl={livePhotoVideoUrl}
|
||||
imageLoaderManager={imageLoaderManagerRef.current}
|
||||
loadingIndicatorRef={loadingIndicatorRef}
|
||||
isCurrentImage={isCurrentImage}
|
||||
onPlayingChange={setIsLivePhotoPlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -156,7 +156,9 @@ const categories = {
|
||||
'GPS Position',
|
||||
'GPS Speed',
|
||||
'GPS Speed Ref',
|
||||
'GPS Map Datum',
|
||||
'GPS Time Stamp',
|
||||
'GPS Date Stamp',
|
||||
'GPS Date/Time',
|
||||
],
|
||||
imageProperties: [
|
||||
'Image Width',
|
||||
@@ -377,7 +379,7 @@ export const RawExifViewer: React.FC<RawExifViewerProps> = ({
|
||||
|
||||
<ScrollArea
|
||||
rootClassName="h-0 grow flex-1 -mb-6 -mx-6"
|
||||
viewportClassName="px-7 pb-6 pt-4"
|
||||
viewportClassName="px-7 pb-6 pt-4 [&_*]:select-text"
|
||||
flex
|
||||
>
|
||||
<div className="min-w-0 space-y-6">
|
||||
|
||||
@@ -8,6 +8,6 @@ export const isMobileDevice = (() => {
|
||||
navigator.userAgent,
|
||||
) ||
|
||||
// 现代检测方式:支持触摸且屏幕较小
|
||||
('ontouchstart' in window && window.screen.width < 1024)
|
||||
'ontouchstart' in window
|
||||
)
|
||||
})()
|
||||
|
||||
@@ -8,6 +8,12 @@ body {
|
||||
@apply grow h-0 shrink-0 overflow-auto relative;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"photo.live.badge": "Live",
|
||||
"photo.live.converting.detail": "Converting video format using {{method}}...",
|
||||
"photo.live.converting.video": "Converting Live Photo video",
|
||||
"photo.live.playing": "Playing Live Photo",
|
||||
"photo.live.tooltip.desktop.main": "Hover to play Live Photo",
|
||||
"photo.live.tooltip.desktop.zoom": "Hover to play Live Photo / Double-click to zoom",
|
||||
"photo.live.tooltip.mobile.main": "Long press to play Live Photo",
|
||||
|
||||
@@ -231,11 +231,12 @@
|
||||
"photo.copying": "画像をコピーしています...",
|
||||
"photo.download": "画像をダウンロード",
|
||||
"photo.error.loading": "画像の読み込みに失敗しました",
|
||||
"photo.live.badge": "ライブ",
|
||||
"photo.live.badge": "Live",
|
||||
"photo.live.converting.detail": "{{method}}を使用してビデオ形式を変換しています...",
|
||||
"photo.live.converting.video": "ライブフォトビデオを変換しています",
|
||||
"photo.live.tooltip.desktop.main": "ホバーしてライブフォトを再生",
|
||||
"photo.live.tooltip.desktop.zoom": "「ライブ」バッジをホバーして再生/ダブルクリックしてズーム",
|
||||
"photo.live.converting.video": "Live Photoのビデオを変換中",
|
||||
"photo.live.playing": "ライブ写真を再生中",
|
||||
"photo.live.tooltip.desktop.main": "ホバーしてLive Photoを再生",
|
||||
"photo.live.tooltip.desktop.zoom": "ホバーしてLive Photoを再生 / ダブルクリックしてズーム",
|
||||
"photo.live.tooltip.mobile.main": "長押ししてライブフォトを再生",
|
||||
"photo.live.tooltip.mobile.zoom": "長押ししてライブフォトを再生/ダブルタップしてズーム",
|
||||
"photo.share.actions": "アクション",
|
||||
|
||||
@@ -230,12 +230,13 @@
|
||||
"photo.copy.success": "이미지를 클립보드에 복사했습니다.",
|
||||
"photo.copying": "이미지 복사 중...",
|
||||
"photo.download": "이미지 다운로드",
|
||||
"photo.error.loading": "이미지 로딩 실패",
|
||||
"photo.error.loading": "이미지를 불러오지 못했습니다",
|
||||
"photo.live.badge": "라이브",
|
||||
"photo.live.converting.detail": "{{method}}을 (를) 사용하여 비디오 형식 변환 중...",
|
||||
"photo.live.converting.detail": "{{method}}를 사용하여 비디오 형식을 변환하는 중...",
|
||||
"photo.live.converting.video": "라이브 포토 비디오 변환 중",
|
||||
"photo.live.tooltip.desktop.main": "마우스를 올려 라이브 포토 재생",
|
||||
"photo.live.tooltip.desktop.zoom": "‘라이브’배지에 마우스를 올려 재생/더블 클릭하여 확대",
|
||||
"photo.live.playing": "라이브 포토 재생 중",
|
||||
"photo.live.tooltip.desktop.main": "호버하여 라이브 포토 재생",
|
||||
"photo.live.tooltip.desktop.zoom": "호버하여 라이브 포토 재생 / 더블 클릭하여 확대",
|
||||
"photo.live.tooltip.mobile.main": "길게 눌러 라이브 포토 재생",
|
||||
"photo.live.tooltip.mobile.zoom": "길게 눌러 라이브 포토 재생/더블 탭하여 확대",
|
||||
"photo.share.actions": "작업",
|
||||
|
||||
@@ -231,12 +231,13 @@
|
||||
"photo.copy.success": "图像已复制到剪贴板",
|
||||
"photo.copying": "正在复制图像...",
|
||||
"photo.download": "下载图像",
|
||||
"photo.error.loading": "图像加载失败",
|
||||
"photo.error.loading": "图片加载失败",
|
||||
"photo.live.badge": "实况",
|
||||
"photo.live.converting.detail": "正在使用 {{method}} 转换视频格式...",
|
||||
"photo.live.converting.video": "正在转换实况照片视频",
|
||||
"photo.live.playing": "正在播放实况照片",
|
||||
"photo.live.tooltip.desktop.main": "悬停以播放实况照片",
|
||||
"photo.live.tooltip.desktop.zoom": "悬停\"实况\"徽章以播放/双击以缩放",
|
||||
"photo.live.tooltip.desktop.zoom": "悬停播放实况照片 / 双击缩放",
|
||||
"photo.live.tooltip.mobile.main": "长按以播放实况照片",
|
||||
"photo.live.tooltip.mobile.zoom": "长按以播放实况照片/双击以缩放",
|
||||
"photo.reaction.success": "点赞成功",
|
||||
|
||||
@@ -230,14 +230,15 @@
|
||||
"photo.copy.success": "圖像已複製到剪貼簿",
|
||||
"photo.copying": "正在複製圖像...",
|
||||
"photo.download": "下載圖像",
|
||||
"photo.error.loading": "圖像載入失敗",
|
||||
"photo.live.badge": "實況",
|
||||
"photo.error.loading": "圖片載入失敗",
|
||||
"photo.live.badge": "原況",
|
||||
"photo.live.converting.detail": "正在使用 {{method}} 轉換影片格式...",
|
||||
"photo.live.converting.video": "正在轉換實況照片影片",
|
||||
"photo.live.tooltip.desktop.main": "懸停以播放實況照片",
|
||||
"photo.live.tooltip.desktop.zoom": "懸停「實況」徽章以播放/雙擊以縮放",
|
||||
"photo.live.tooltip.mobile.main": "長按以播放實況照片",
|
||||
"photo.live.tooltip.mobile.zoom": "長按以播放實況照片/雙擊以縮放",
|
||||
"photo.live.converting.video": "正在轉換原況照片影片",
|
||||
"photo.live.playing": "正在播放原況照片",
|
||||
"photo.live.tooltip.desktop.main": "懸停以播放原況照片",
|
||||
"photo.live.tooltip.desktop.zoom": "懸停播放原況照片 / 輕按兩下縮放",
|
||||
"photo.live.tooltip.mobile.main": "長按以播放原況照片",
|
||||
"photo.live.tooltip.mobile.zoom": "長按以播放原況照片/雙擊以縮放",
|
||||
"photo.share.actions": "操作",
|
||||
"photo.share.copy.failed": "複製失敗",
|
||||
"photo.share.copy.link": "複製連結",
|
||||
|
||||
@@ -230,14 +230,15 @@
|
||||
"photo.copy.success": "圖像已複製到剪貼簿",
|
||||
"photo.copying": "正在複製圖像...",
|
||||
"photo.download": "下載圖像",
|
||||
"photo.error.loading": "圖像載入失敗",
|
||||
"photo.live.badge": "實況",
|
||||
"photo.error.loading": "圖片載入失敗",
|
||||
"photo.live.badge": "原況",
|
||||
"photo.live.converting.detail": "正在使用 {{method}} 轉換影片格式...",
|
||||
"photo.live.converting.video": "正在轉換實況照片影片",
|
||||
"photo.live.tooltip.desktop.main": "懸停以播放實況照片",
|
||||
"photo.live.tooltip.desktop.zoom": "懸停「實況」徽章以播放/雙擊以縮放",
|
||||
"photo.live.tooltip.mobile.main": "長按以播放實況照片",
|
||||
"photo.live.tooltip.mobile.zoom": "長按以播放實況照片/雙擊以縮放",
|
||||
"photo.live.converting.video": "正在轉換原況照片影片",
|
||||
"photo.live.playing": "正在播放原況照片",
|
||||
"photo.live.tooltip.desktop.main": "懸停以播放原況照片",
|
||||
"photo.live.tooltip.desktop.zoom": "懸停播放原況照片 / 輕按兩下縮放",
|
||||
"photo.live.tooltip.mobile.main": "長按以播放原況照片",
|
||||
"photo.live.tooltip.mobile.zoom": "長按以播放原況照片/雙擊以縮放",
|
||||
"photo.share.actions": "操作",
|
||||
"photo.share.copy.failed": "複製失敗",
|
||||
"photo.share.copy.link": "複製連結",
|
||||
|
||||
Reference in New Issue
Block a user