Skip to content

Commit

Permalink
feat: impl settings.videoCard.__internal.useLargePreview
Browse files Browse the repository at this point in the history
  • Loading branch information
magicdawn committed Dec 30, 2024
1 parent e3d9a26 commit f3d3360
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 172 deletions.
36 changes: 16 additions & 20 deletions src/components/VideoCard/card.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { HOST_APP, OPERATION_FAIL_MSG, appWarn } from '$common'
import type { AppRecItem, DmJson, PvideoJson } from '$define'
import { HOST_APP, OPERATION_FAIL_MSG, appLog, appWarn } from '$common'
import type { AppRecItem, PvideoJson } from '$define'
import { getVideoPlayUrl } from '$modules/bilibili/video/play-url'
import { getVideoCid } from '$modules/bilibili/video/video-detail'
import { settings } from '$modules/settings'
import { gmrequest, isWebApiSuccess, request } from '$request'
import { getCsrfToken } from '$utility/cookie'
Expand Down Expand Up @@ -45,27 +47,14 @@ export function isVideoshotJsonCacheable(json: PvideoJson) {
}
}

// dm
export async function dm(aid: string) {
// 暂时没有支持弹幕预览, 不调用该接口
return []

const res = await request.get('/x/v2/dm/ajax', { params: { aid } })
const json = res.data as DmJson

// TODO: process errors

return json.data
}

const cache = new QuickLRU<string, VideoData>({ maxSize: 1_0000 })

export type VideoData = {
videoshotJson: PvideoJson
// dmJson?: DmJson
videoshotJson?: PvideoJson
playUrl?: string
}

export async function fetchVideoData(bvid: string): Promise<VideoData> {
export async function fetchVideoData(bvid: string, cid?: number): Promise<VideoData> {
// cache:lookup
if (cache.has(bvid)) {
const cached = cache.get(bvid)
Expand All @@ -84,10 +73,17 @@ export async function fetchVideoData(bvid: string): Promise<VideoData> {
}
} while (retryTimes < 3)

let playUrl: string | undefined
if (settings.videoCard.__internal.useLargePreview) {
cid ??= await getVideoCid(bvid)
playUrl = await getVideoPlayUrl(bvid, cid)
appLog('playUrl: bvid=%s cid=%s %s', bvid, cid, playUrl)
}

// cache:save
const cacheable = isVideoshotJsonCacheable(videoshotJson)
if (cacheable) {
cache.set(bvid, { videoshotJson })
cache.set(bvid, { videoshotJson, playUrl })
}

const videoshotData = videoshotJson.data
Expand All @@ -102,7 +98,7 @@ export async function fetchVideoData(bvid: string): Promise<VideoData> {
})()
}

return { videoshotJson }
return { videoshotJson, playUrl }
}

/**
Expand Down
301 changes: 156 additions & 145 deletions src/components/VideoCard/child-components/LargePreviewImage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { APP_CLS_CARD, appLog } from '$common'
import { colorPrimaryValue } from '$components/css-vars'
import { css } from '@emotion/react'
import { useEventListener } from 'ahooks'
import { orderBy, throttle } from 'es-toolkit'
Expand All @@ -20,155 +21,165 @@ const reverseDirection = (direction: Direction) => {
}
}

export function LargePreviewImage({ children }: { children?: ReactNode }) {
const ref = useRef<ComponentRef<'div'>>(null)

const [visible, setVisible] = useState(false)
const [position, setPosition] = useState<
| {
direction: Direction
imgWidth: number
imgHeight: number
imgPosX: number
imgPosY: number
export const LargePreview = forwardRef<ComponentRef<'div'>, { children?: ReactNode }>(
({ children }, forwardedRef) => {
const ref = useRef<ComponentRef<'div'>>(null)

const [visible, setVisible] = useState(false)
const [position, setPosition] = useState<
| {
direction: Direction
elWidth: number
elHeight: number
elPosX: number
elPosY: number
}
| undefined
>(undefined)

const calculatePostion = useMemoizedFn(() => {
const card = ref.current?.closest('.' + APP_CLS_CARD)
if (!card) return

const viewportWidth = document.documentElement.clientWidth
const viewportHeight = document.documentElement.clientHeight
const cardRect = card.getBoundingClientRect()

const possibleBoundingBox: Record<Direction, Bbox> = {
top: { x: 0, y: 0, width: viewportWidth, height: cardRect.top },
bottom: {
x: 0,
y: cardRect.bottom,
width: viewportWidth,
height: viewportHeight - cardRect.bottom,
},
left: { x: 0, y: 0, width: cardRect.left, height: viewportHeight },
right: {
x: cardRect.right,
y: 0,
width: viewportWidth - cardRect.right,
height: viewportHeight,
},
}
| undefined
>(undefined)

const calculatePostion = useMemoizedFn(() => {
const card = ref.current?.closest('.' + APP_CLS_CARD)
if (!card) return

const viewportWidth = document.documentElement.clientWidth
const viewportHeight = document.documentElement.clientHeight
const cardRect = card.getBoundingClientRect()

const possibleBoundingBox: Record<Direction, Bbox> = {
top: { x: 0, y: 0, width: viewportWidth, height: cardRect.top },
bottom: {
x: 0,
y: cardRect.bottom,
width: viewportWidth,
height: viewportHeight - cardRect.bottom,
},
left: { x: 0, y: 0, width: cardRect.left, height: viewportHeight },
right: {
x: cardRect.right,
y: 0,
width: viewportWidth - cardRect.right,
height: viewportHeight,
},
}

const aspectRatio = 16 / 9
const getScaleInBox = (bbox: Bbox) => {
const w = aspectRatio
const h = 1
const scale = Math.min(bbox.width / w, bbox.height / h)
return scale
}

const picked = orderBy(
Object.entries(possibleBoundingBox).map(([direction, bbox]) => ({
direction: direction as Direction,
bbox,
scale: getScaleInBox(bbox),
})),
['scale'],
['desc'],
)[0]

const { direction, bbox, scale } = picked
appLog('picked', picked)

const imgHeight = Math.floor(scale) - 40 /* padding */
const imgWidth = Math.floor(imgHeight * aspectRatio)

let imgPosX = 0
let imgPosY = 0
const visualPadding = 20

const fixX = () => {
if (imgPosX < visualPadding) {
imgPosX = visualPadding
return

const aspectRatio = 16 / 9
const getScaleInBox = (bbox: Bbox) => {
const w = aspectRatio
const h = 1
const scale = Math.min(bbox.width / w, bbox.height / h)
return scale
}
if (imgPosX + imgWidth > viewportWidth - visualPadding) {
imgPosX = viewportWidth - visualPadding - imgWidth
return

const picked = orderBy(
Object.entries(possibleBoundingBox).map(([direction, bbox]) => ({
direction: direction as Direction,
bbox,
scale: getScaleInBox(bbox),
})),
['scale'],
['desc'],
)[0]

const { direction, bbox, scale } = picked
appLog('picked', picked)

const elHeight = Math.floor(scale) - 40 /* padding */
const elWidth = Math.floor(elHeight * aspectRatio)

let elPosX = 0
let elPosY = 0
const visualPadding = 20

const fixX = () => {
if (elPosX < visualPadding) {
elPosX = visualPadding
return
}
if (elPosX + elWidth > viewportWidth - visualPadding) {
elPosX = viewportWidth - visualPadding - elWidth
return
}
}
}
const fixY = () => {
if (imgPosY < visualPadding) {
imgPosY = visualPadding
return
const fixY = () => {
if (elPosY < visualPadding) {
elPosY = visualPadding
return
}
if (elPosY + elHeight > viewportHeight - visualPadding) {
elPosY = viewportHeight - visualPadding - elHeight
return
}
}
if (imgPosY + imgHeight > viewportHeight - visualPadding) {
imgPosY = viewportHeight - visualPadding - imgHeight
return

switch (direction) {
case 'top':
elPosX = cardRect.x + cardRect.width / 2 - elWidth / 2
elPosY = cardRect.top - visualPadding - elHeight
fixX()
break
case 'bottom':
elPosX = cardRect.x + cardRect.width / 2 - elWidth / 2
elPosY = cardRect.bottom + visualPadding
fixX()
break
case 'right':
elPosX = cardRect.right + visualPadding
elPosY = cardRect.y + cardRect.height / 2 - elHeight / 2
fixY()
break
case 'left':
elPosX = cardRect.left - visualPadding - elWidth
elPosY = cardRect.y + cardRect.height / 2 - elHeight / 2
fixY()
break
}

setVisible(true)
setPosition({ direction, elWidth, elHeight, elPosX, elPosY })
})

const calculatePostionThrottled = useMemo(
() => throttle(calculatePostion, 100),
[calculatePostion],
)

useMount(() => {
if (forwardedRef) {
typeof forwardedRef === 'function'
? forwardedRef(ref.current)
: (forwardedRef.current = ref.current)
}
}

switch (direction) {
case 'top':
imgPosX = cardRect.x + cardRect.width / 2 - imgWidth / 2
imgPosY = cardRect.top - visualPadding - imgHeight
fixX()
break
case 'bottom':
imgPosX = cardRect.x + cardRect.width / 2 - imgWidth / 2
imgPosY = cardRect.bottom + visualPadding
fixX()
break
case 'right':
imgPosX = cardRect.right + visualPadding
imgPosY = cardRect.y + cardRect.height / 2 - imgHeight / 2
fixY()
break
case 'left':
imgPosX = cardRect.left - visualPadding - imgWidth
imgPosY = cardRect.y + cardRect.height / 2 - imgHeight / 2
fixY()
break
}

setVisible(true)
setPosition({ direction, imgWidth, imgHeight, imgPosX, imgPosY })
})

const calculatePostionThrottled = useMemo(
() => throttle(calculatePostion, 100),
[calculatePostion],
)

useMount(() => {
calculatePostion()
})

useEventListener('resize', calculatePostionThrottled, { target: window })
useEventListener('scroll', calculatePostionThrottled, { target: window })

return (
<div
ref={ref}
css={[
css`
display: ${visible ? 'block' : 'none'};
background-color: rgba(255 255 255 / 0.5);
backdrop-filter: blur(10px);
`,
position &&
calculatePostion()
})

useEventListener('resize', calculatePostionThrottled, { target: window })
useEventListener('scroll', calculatePostionThrottled, { target: window })

return (
<div
ref={ref}
css={[
css`
position: fixed;
z-index: 90000;
width: ${position.imgWidth}px;
height: ${position.imgHeight}px;
top: ${position.imgPosY}px;
left: ${position.imgPosX}px;
display: ${visible ? 'block' : 'none'};
background-color: rgba(255 255 255 / 0.5);
backdrop-filter: blur(10px);
border: 1px solid ${colorPrimaryValue};
border-radius: 20px;
overflow: hidden;
`,
]}
>
{visible && children}
</div>
)
}
position &&
css`
position: fixed;
z-index: 90000;
width: ${position.elWidth}px;
height: ${position.elHeight}px;
top: ${position.elPosY}px;
left: ${position.elPosX}px;
`,
]}
>
{visible && children}
</div>
)
},
)
Loading

0 comments on commit f3d3360

Please sign in to comment.