feat: entries images -> media
This commit is contained in:
parent
b8dc9802a3
commit
39e9cfc7ba
|
|
@ -113,7 +113,6 @@
|
|||
"tailwindcss-animate": "1.0.7",
|
||||
"tldts": "6.1.32",
|
||||
"unified": "11.0.5",
|
||||
"unist-util-visit": "5.0.0",
|
||||
"usehooks-ts": "3.1.0",
|
||||
"vfile": "6.0.2",
|
||||
"zod": "3.23.8",
|
||||
|
|
|
|||
|
|
@ -268,9 +268,6 @@ importers:
|
|||
unified:
|
||||
specifier: 11.0.5
|
||||
version: 11.0.5
|
||||
unist-util-visit:
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0
|
||||
usehooks-ts:
|
||||
specifier: 3.1.0
|
||||
version: 3.1.0(react@18.3.1)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { app, dialog, Menu, nativeTheme, ShareMenu } from "electron"
|
|||
import { downloadFile } from "./lib/download"
|
||||
import type { RendererHandlers } from "./renderer-handlers"
|
||||
import { quitAndInstall } from "./updater"
|
||||
import { createSettingWindow, createWindow, getMainWindow } from "./window"
|
||||
import { createSettingWindow, getMainWindow } from "./window"
|
||||
|
||||
const t = tipc.create()
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ export const router = {
|
|||
}),
|
||||
|
||||
saveToEagle: t.procedure
|
||||
.input<{ url: string, images: string[] }>()
|
||||
.input<{ url: string, mediaUrls: string[] }>()
|
||||
.action(async ({ input }) => {
|
||||
try {
|
||||
const res = await fetch("http://localhost:41595/api/item/addFromURLs", {
|
||||
|
|
@ -100,8 +100,8 @@ export const router = {
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: input.images?.map((image) => ({
|
||||
url: image,
|
||||
items: input.mediaUrls?.map((media) => ({
|
||||
url: media,
|
||||
website: input.url,
|
||||
headers: {
|
||||
referer: input.url,
|
||||
|
|
@ -126,36 +126,6 @@ export const router = {
|
|||
handlers.invalidateQuery.send(input)
|
||||
}),
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
previewImage: t.procedure
|
||||
.input<{
|
||||
realUrl: string
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
}>()
|
||||
.action(async ({ input }) => {
|
||||
if (
|
||||
process.env["VITE_IMGPROXY_URL"] &&
|
||||
input.url.startsWith(process.env["VITE_IMGPROXY_URL"])
|
||||
) {
|
||||
const meta = await fetch(
|
||||
`${process.env["VITE_IMGPROXY_URL"]}/unsafe/meta/${encodeURIComponent(
|
||||
input.realUrl,
|
||||
)}`,
|
||||
).then((res) => res.json())
|
||||
input.width = meta.thumbor.source.width
|
||||
input.height = meta.thumbor.source.height
|
||||
}
|
||||
createWindow({
|
||||
extraPath: `/preview?url=${encodeURIComponent(input.realUrl)}`,
|
||||
width: input.width,
|
||||
height: input.height,
|
||||
})
|
||||
}),
|
||||
|
||||
getLoginItemSettings: t.procedure
|
||||
.input<void>()
|
||||
.action(async () => await app.getLoginItemSettings()),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { SiteIcon } from "@renderer/components/site-icon"
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import type { CombinedEntryModel, FeedModel } from "@renderer/models"
|
||||
|
||||
|
|
@ -19,8 +19,9 @@ export function FeedIcon({
|
|||
const image = entry?.authorAvatar || feed.image
|
||||
if (image) {
|
||||
return (
|
||||
<Image
|
||||
<Media
|
||||
src={image}
|
||||
type="photo"
|
||||
loading="lazy"
|
||||
className={cn("mr-2 shrink-0 rounded-sm", className)}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { getProxyUrl } from "@renderer/lib/img-proxy"
|
||||
import { showNativeMenu } from "@renderer/lib/native-menu"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import type { FC, ImgHTMLAttributes } from "react"
|
||||
import { memo, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { usePreviewImages } from "./image/hooks"
|
||||
|
||||
const failedList = new Set<string | undefined>()
|
||||
export type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
|
||||
proxy?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
disableContextMenu?: boolean
|
||||
popper?: boolean
|
||||
}
|
||||
const ImageImpl: FC<ImageProps> = ({
|
||||
className,
|
||||
proxy,
|
||||
disableContextMenu,
|
||||
popper = false,
|
||||
...props
|
||||
}) => {
|
||||
const { src, style, ...rest } = props
|
||||
const [hidden, setHidden] = useState(!src)
|
||||
const [imgSrc, setImgSrc] = useState(
|
||||
proxy && src && !failedList.has(src) ?
|
||||
getProxyUrl({
|
||||
url: src,
|
||||
width: proxy.width,
|
||||
height: proxy.height,
|
||||
}) :
|
||||
src,
|
||||
)
|
||||
|
||||
const errorHandle: React.ReactEventHandler<HTMLImageElement> =
|
||||
useEventCallback((e) => {
|
||||
if (imgSrc !== props.src) {
|
||||
setImgSrc(props.src)
|
||||
failedList.add(props.src)
|
||||
} else {
|
||||
setHidden(true)
|
||||
props.onError?.(e)
|
||||
}
|
||||
})
|
||||
const previewImages = usePreviewImages()
|
||||
const handleClick = useEventCallback(
|
||||
(e: React.MouseEvent<HTMLImageElement>) => {
|
||||
if (popper && src) {
|
||||
e.stopPropagation()
|
||||
previewImages([src], 0)
|
||||
}
|
||||
props.onClick?.(e)
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden rounded", className)} style={style}>
|
||||
<img
|
||||
{...rest}
|
||||
onError={errorHandle}
|
||||
className={cn(
|
||||
hidden && "hidden",
|
||||
!(props.width || props.height) && "size-full",
|
||||
"bg-stone-100 object-cover",
|
||||
popper && "cursor-zoom-in",
|
||||
)}
|
||||
src={imgSrc}
|
||||
onClick={handleClick}
|
||||
{...(!disableContextMenu ?
|
||||
{
|
||||
onContextMenu: (e) => {
|
||||
e.stopPropagation()
|
||||
props.onContextMenu?.(e)
|
||||
showNativeMenu(
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
label: "Open Image in New Window",
|
||||
click: () => {
|
||||
if (props.src && imgSrc && tipcClient) {
|
||||
window.open(props.src, "_blank")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
label: "Copy Image Address",
|
||||
click: () => {
|
||||
if (props.src) {
|
||||
navigator.clipboard.writeText(props.src)
|
||||
toast("Address copied to clipboard.", {
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
e,
|
||||
)
|
||||
},
|
||||
} :
|
||||
{})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Image = memo(ImageImpl)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import type { MediaModel } from "@renderer/hono"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { getProxyUrl } from "@renderer/lib/img-proxy"
|
||||
import { showNativeMenu } from "@renderer/lib/native-menu"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import type { FC, ImgHTMLAttributes } from "react"
|
||||
import { memo, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { usePreviewMedia } from "./media/hooks"
|
||||
|
||||
const failedList = new Set<string | undefined>()
|
||||
export type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
|
||||
proxy?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
disableContextMenu?: boolean
|
||||
popper?: boolean
|
||||
type?: MediaModel["type"]
|
||||
}
|
||||
const MediaImpl: FC<ImageProps> = ({
|
||||
className,
|
||||
proxy,
|
||||
disableContextMenu,
|
||||
popper = false,
|
||||
...props
|
||||
}) => {
|
||||
const { src, style, type, ...rest } = props
|
||||
const [hidden, setHidden] = useState(!src)
|
||||
const [imgSrc, setImgSrc] = useState(
|
||||
proxy && src && !failedList.has(src) ?
|
||||
getProxyUrl({
|
||||
url: src,
|
||||
width: proxy.width,
|
||||
height: proxy.height,
|
||||
}) :
|
||||
src,
|
||||
)
|
||||
|
||||
const errorHandle: React.ReactEventHandler<HTMLImageElement> =
|
||||
useEventCallback((e) => {
|
||||
if (imgSrc !== props.src) {
|
||||
setImgSrc(props.src)
|
||||
failedList.add(props.src)
|
||||
} else {
|
||||
setHidden(true)
|
||||
props.onError?.(e)
|
||||
}
|
||||
})
|
||||
const previewMedia = usePreviewMedia()
|
||||
const handleClick = useEventCallback(
|
||||
(e: React.MouseEvent<HTMLImageElement>) => {
|
||||
if (popper && src) {
|
||||
e.stopPropagation()
|
||||
previewMedia([{
|
||||
url: src,
|
||||
type: "photo",
|
||||
}], 0)
|
||||
}
|
||||
props.onClick?.(e)
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden rounded", className)} style={style}>
|
||||
{(!type || type === "photo") && (
|
||||
<img
|
||||
{...rest}
|
||||
onError={errorHandle}
|
||||
className={cn(
|
||||
hidden && "hidden",
|
||||
!(props.width || props.height) && "size-full",
|
||||
"bg-stone-100 object-cover",
|
||||
popper && "cursor-zoom-in",
|
||||
)}
|
||||
src={imgSrc}
|
||||
onClick={handleClick}
|
||||
{...(!disableContextMenu ?
|
||||
{
|
||||
onContextMenu: (e) => {
|
||||
e.stopPropagation()
|
||||
props.onContextMenu?.(e)
|
||||
showNativeMenu(
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
label: "Open Image in New Window",
|
||||
click: () => {
|
||||
if (props.src && imgSrc && tipcClient) {
|
||||
window.open(props.src, "_blank")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
label: "Copy Image Address",
|
||||
click: () => {
|
||||
if (props.src) {
|
||||
navigator.clipboard.writeText(props.src)
|
||||
toast("Address copied to clipboard.", {
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
e,
|
||||
)
|
||||
},
|
||||
} :
|
||||
{})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Media = memo(MediaImpl)
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
import type { MediaModel } from "@renderer/hono"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { useModalStack } from "../modal/stacked/hooks"
|
||||
import { NoopChildren } from "../modal/stacked/utils"
|
||||
import { PreviewImageContent } from "./preview-image"
|
||||
import { PreviewMediaContent } from "./preview-media"
|
||||
|
||||
export const usePreviewImages = () => {
|
||||
export const usePreviewMedia = () => {
|
||||
const { present } = useModalStack()
|
||||
return useCallback(
|
||||
(images: string[], initialIndex = 0) => {
|
||||
(media: MediaModel[], initialIndex = 0) => {
|
||||
present({
|
||||
content: () => (
|
||||
<div className="relative size-full">
|
||||
<PreviewImageContent initialIndex={initialIndex} images={images} />
|
||||
<PreviewMediaContent initialIndex={initialIndex} media={media} />
|
||||
</div>
|
||||
),
|
||||
title: "Image",
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { m } from "@renderer/components/common/Motion"
|
||||
import { COPY_MAP } from "@renderer/constants"
|
||||
import type { MediaModel } from "@renderer/hono"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { stopPropagation } from "@renderer/lib/dom"
|
||||
import { showNativeMenu } from "@renderer/lib/native-menu"
|
||||
|
|
@ -57,11 +58,11 @@ const Wrapper: Component<{
|
|||
</div>
|
||||
)
|
||||
}
|
||||
export const PreviewImageContent: FC<{
|
||||
images: string[]
|
||||
export const PreviewMediaContent: FC<{
|
||||
media: MediaModel[]
|
||||
initialIndex?: number
|
||||
}> = ({ images, initialIndex = 0 }) => {
|
||||
const [currentSrc, setCurrentSrc] = useState(images[initialIndex])
|
||||
}> = ({ media, initialIndex = 0 }) => {
|
||||
const [currentMedia, setCurrentMedia] = useState(media[initialIndex])
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(image: string, e: React.MouseEvent<HTMLImageElement>) => {
|
||||
|
|
@ -96,9 +97,9 @@ export const PreviewImageContent: FC<{
|
|||
},
|
||||
[],
|
||||
)
|
||||
if (images.length === 0) return null
|
||||
if (images.length === 1) {
|
||||
const src = images[0]
|
||||
if (media.length === 0) return null
|
||||
if (media.length === 1) {
|
||||
const src = media[0].url
|
||||
return (
|
||||
<Wrapper src={src}>
|
||||
<img
|
||||
|
|
@ -111,7 +112,7 @@ export const PreviewImageContent: FC<{
|
|||
)
|
||||
}
|
||||
return (
|
||||
<Wrapper src={currentSrc}>
|
||||
<Wrapper src={currentMedia.url}>
|
||||
<Swiper
|
||||
loop
|
||||
initialSlide={initialIndex}
|
||||
|
|
@ -126,18 +127,18 @@ export const PreviewImageContent: FC<{
|
|||
}}
|
||||
virtual
|
||||
onSlideChange={({ realIndex }) => {
|
||||
setCurrentSrc(images[realIndex])
|
||||
setCurrentMedia(media[realIndex])
|
||||
}}
|
||||
modules={[Scrollbar, Mousewheel, Virtual, Keyboard]}
|
||||
className="size-full"
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<SwiperSlide key={image} virtualIndex={index}>
|
||||
{media.map((med, index) => (
|
||||
<SwiperSlide key={med.url} virtualIndex={index}>
|
||||
<img
|
||||
onContextMenu={(e) => handleContextMenu(image, e)}
|
||||
onContextMenu={(e) => handleContextMenu(med.url, e)}
|
||||
className="size-full object-contain"
|
||||
alt="cover"
|
||||
src={image}
|
||||
src={med.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
|
|
@ -2,30 +2,31 @@ import "swiper/css"
|
|||
import "swiper/css/navigation"
|
||||
import "swiper/css/scrollbar"
|
||||
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import type { MediaModel } from "@renderer/hono"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { useHover } from "@use-gesture/react"
|
||||
import { uniq } from "lodash-es"
|
||||
import { uniqBy } from "lodash-es"
|
||||
import { useRef, useState } from "react"
|
||||
import { Mousewheel, Navigation, Scrollbar } from "swiper/modules"
|
||||
import { Swiper, SwiperSlide } from "swiper/react"
|
||||
|
||||
import styles from "./index.module.css"
|
||||
|
||||
export function SwipeImages({
|
||||
images,
|
||||
export function SwipeMedia({
|
||||
media,
|
||||
uniqueKey,
|
||||
className,
|
||||
imgClassName,
|
||||
onPreview,
|
||||
}: {
|
||||
images?: string[] | null
|
||||
media?: MediaModel[] | null
|
||||
uniqueKey?: string
|
||||
className?: string
|
||||
imgClassName?: string
|
||||
onPreview?: (images: string[], index?: number) => void
|
||||
onPreview?: (media: MediaModel[], index?: number) => void
|
||||
}) {
|
||||
const uniqImages = uniq(images)
|
||||
const uniqMedia = uniqBy(media, "url")
|
||||
|
||||
const hoverRef = useRef<HTMLDivElement>(null)
|
||||
const [enableSwipe, setEnableSwipe] = useState(false)
|
||||
|
|
@ -40,7 +41,7 @@ export function SwipeImages({
|
|||
},
|
||||
)
|
||||
|
||||
if (!images) return null
|
||||
if (!media) return null
|
||||
return (
|
||||
<div
|
||||
ref={hoverRef}
|
||||
|
|
@ -50,7 +51,7 @@ export function SwipeImages({
|
|||
className,
|
||||
)}
|
||||
>
|
||||
{enableSwipe && (uniqImages?.length || 0) > 1 ? (
|
||||
{enableSwipe && (uniqMedia?.length || 0) > 1 ? (
|
||||
<>
|
||||
<Swiper
|
||||
loop
|
||||
|
|
@ -67,12 +68,13 @@ export function SwipeImages({
|
|||
modules={[Navigation, Scrollbar, Mousewheel]}
|
||||
className="size-full"
|
||||
>
|
||||
{uniqImages?.slice(0, 5).map((image, i) => (
|
||||
<SwiperSlide key={image}>
|
||||
<Image
|
||||
{uniqMedia?.slice(0, 5).map((med, i) => (
|
||||
<SwiperSlide key={med.url}>
|
||||
<Media
|
||||
className={cn(imgClassName, "size-full rounded-none")}
|
||||
alt="cover"
|
||||
src={image}
|
||||
src={med.url}
|
||||
type={med.type}
|
||||
loading="lazy"
|
||||
proxy={{
|
||||
width: 600,
|
||||
|
|
@ -80,7 +82,7 @@ export function SwipeImages({
|
|||
}}
|
||||
disableContextMenu
|
||||
onClick={(e) => {
|
||||
onPreview?.(images, i)
|
||||
onPreview?.(uniqMedia, i)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
|
|
@ -102,16 +104,17 @@ export function SwipeImages({
|
|||
<i className="i-mgc-right-cute-fi" />
|
||||
</div>
|
||||
</>
|
||||
) : uniqImages?.length >= 1 ?
|
||||
) : uniqMedia?.length >= 1 ?
|
||||
(
|
||||
<Image
|
||||
<Media
|
||||
onClick={(e) => {
|
||||
onPreview?.(uniqImages)
|
||||
onPreview?.(uniqMedia)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className="size-full rounded-none object-cover sm:transition-transform sm:duration-300 sm:ease-in-out sm:group-hover:scale-105"
|
||||
alt="cover"
|
||||
src={uniqImages[0]}
|
||||
src={uniqMedia[0].url}
|
||||
type={uniqMedia[0].type}
|
||||
loading="lazy"
|
||||
proxy={{
|
||||
width: 600,
|
||||
|
|
@ -450,6 +450,17 @@ declare const collectionsRelations: drizzle_orm.Relations<"collections", {
|
|||
feeds: drizzle_orm.One<"feeds", true>;
|
||||
}>;
|
||||
|
||||
type MediaModel = {
|
||||
url: string;
|
||||
type: "photo" | "video";
|
||||
preview_image_url?: string;
|
||||
};
|
||||
type EnclosuresModel = {
|
||||
url: string;
|
||||
length?: number;
|
||||
type?: string;
|
||||
title?: string;
|
||||
};
|
||||
declare const entries: drizzle_orm_pg_core.PgTableWithColumns<{
|
||||
name: "entries";
|
||||
schema: undefined;
|
||||
|
|
@ -646,32 +657,32 @@ declare const entries: drizzle_orm_pg_core.PgTableWithColumns<{
|
|||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
images: drizzle_orm_pg_core.PgColumn<{
|
||||
name: "images";
|
||||
media: drizzle_orm_pg_core.PgColumn<{
|
||||
name: "media";
|
||||
tableName: "entries";
|
||||
dataType: "array";
|
||||
columnType: "PgArray";
|
||||
data: string[];
|
||||
driverParam: string | string[];
|
||||
data: unknown[];
|
||||
driverParam: string | unknown[];
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
enumValues: undefined;
|
||||
baseColumn: drizzle_orm.Column<{
|
||||
name: "images";
|
||||
name: "media";
|
||||
tableName: "entries";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
dataType: "json";
|
||||
columnType: "PgJsonb";
|
||||
data: unknown;
|
||||
driverParam: unknown;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, object, object>;
|
||||
|
|
@ -755,14 +766,18 @@ declare const entriesOpenAPISchema: z.ZodObject<z.objectUtil.extendShape<Omit<{
|
|||
authorAvatar: z.ZodNullable<z.ZodString>;
|
||||
changedAt: z.ZodString;
|
||||
publishedAt: z.ZodString;
|
||||
images: z.ZodNullable<z.ZodArray<z.ZodString, "many">>;
|
||||
media: z.ZodNullable<z.ZodArray<z.ZodType<string | number | boolean | {
|
||||
[key: string]: string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
||||
} | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null, z.ZodTypeDef, string | number | boolean | {
|
||||
[key: string]: string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
||||
} | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null>, "many">>;
|
||||
categories: z.ZodNullable<z.ZodArray<z.ZodString, "many">>;
|
||||
enclosures: z.ZodNullable<z.ZodArray<z.ZodType<string | number | boolean | {
|
||||
[key: string]: string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
||||
} | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null, z.ZodTypeDef, string | number | boolean | {
|
||||
[key: string]: string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
||||
} | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | (string | number | boolean | any | any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null>, "many">>;
|
||||
}, "enclosures">, {
|
||||
}, "media" | "enclosures">, {
|
||||
enclosures: z.ZodNullable<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
url: z.ZodString;
|
||||
length: z.ZodOptional<z.ZodNumber>;
|
||||
|
|
@ -779,6 +794,19 @@ declare const entriesOpenAPISchema: z.ZodObject<z.objectUtil.extendShape<Omit<{
|
|||
type?: string | undefined;
|
||||
title?: string | undefined;
|
||||
}>, "many">>>;
|
||||
media: z.ZodNullable<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
url: z.ZodString;
|
||||
type: z.ZodEnum<["photo", "video"]>;
|
||||
preview_image_url: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}, {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}>, "many">>>;
|
||||
}>, "strip", z.ZodTypeAny, {
|
||||
description: string | null;
|
||||
title: string | null;
|
||||
|
|
@ -792,8 +820,12 @@ declare const entriesOpenAPISchema: z.ZodObject<z.objectUtil.extendShape<Omit<{
|
|||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
media?: {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
@ -813,8 +845,12 @@ declare const entriesOpenAPISchema: z.ZodObject<z.objectUtil.extendShape<Omit<{
|
|||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
media?: {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
@ -829,12 +865,8 @@ declare const entriesRelations: drizzle_orm.Relations<"entries", {
|
|||
entryReadHistories: drizzle_orm.One<"entryReadHistories", true>;
|
||||
}>;
|
||||
type EntriesModel = InferInsertModel<typeof entries> & {
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number;
|
||||
type?: string;
|
||||
title?: string;
|
||||
}[] | null;
|
||||
enclosures?: EnclosuresModel[] | null;
|
||||
media?: MediaModel[] | null;
|
||||
};
|
||||
declare const entryReadHistories: drizzle_orm_pg_core.PgTableWithColumns<{
|
||||
name: "entryReadHistories";
|
||||
|
|
@ -2997,8 +3029,12 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
media?: {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
@ -3137,8 +3173,12 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
media?: {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
@ -3199,8 +3239,12 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
media?: {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
@ -3274,8 +3318,12 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
media?: {
|
||||
type: "photo" | "video";
|
||||
url: string;
|
||||
preview_image_url?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
@ -3374,4 +3422,4 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
}, "/">;
|
||||
type AppType = typeof _routes;
|
||||
|
||||
export { type ActionsModel, type AppType, type EntriesModel, type EntryReadHistoriesModel, type FeedModel, type SettingsModel, accounts, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, collections, collectionsOpenAPISchema, collectionsRelations, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsInputSchema, feedsOpenAPISchema, feedsRelations, invitations, languageSchema, sessions, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, users, usersOpenApiSchema, usersRelations, verificationTokens, wallets, walletsOpenAPISchema, walletsRelations };
|
||||
export { type ActionsModel, type AppType, type EnclosuresModel, type EntriesModel, type EntryReadHistoriesModel, type FeedModel, type MediaModel, type SettingsModel, accounts, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, collections, collectionsOpenAPISchema, collectionsRelations, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsInputSchema, feedsOpenAPISchema, feedsRelations, invitations, languageSchema, sessions, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, users, usersOpenApiSchema, usersRelations, verificationTokens, wallets, walletsOpenAPISchema, walletsRelations };
|
||||
|
|
|
|||
|
|
@ -149,21 +149,21 @@ export const useEntryActions = ({
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "Save Images to Eagle",
|
||||
name: "Save Media to Eagle",
|
||||
icon: "/eagle.svg",
|
||||
disabled:
|
||||
(checkEagle.isLoading ? true : !checkEagle.data) ||
|
||||
!populatedEntry.entries.images?.length,
|
||||
!populatedEntry.entries.media?.length,
|
||||
onClick: async () => {
|
||||
if (
|
||||
!populatedEntry.entries.url ||
|
||||
!populatedEntry.entries.images?.length
|
||||
!populatedEntry.entries.media?.length
|
||||
) {
|
||||
return
|
||||
}
|
||||
const response = await tipcClient?.saveToEagle({
|
||||
url: populatedEntry.entries.url,
|
||||
images: populatedEntry.entries.images,
|
||||
mediaUrls: populatedEntry.entries.media.map((m) => m.url),
|
||||
})
|
||||
if (response?.status === "success") {
|
||||
toast("Saved to Eagle.", {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Checkbox } from "@renderer/components/ui/checkbox"
|
||||
import { ShikiHighLighter } from "@renderer/components/ui/code-highlighter"
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { LinkWithTooltip } from "@renderer/components/ui/link"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||
import { createElement } from "react"
|
||||
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
|
||||
|
|
@ -11,7 +11,6 @@ import rehypeParse from "rehype-parse"
|
|||
import rehypeSanitize, { defaultSchema } from "rehype-sanitize"
|
||||
import rehypeStringify from "rehype-stringify"
|
||||
import { unified } from "unified"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { VFile } from "vfile"
|
||||
|
||||
export const parseHtml = async (
|
||||
|
|
@ -42,27 +41,7 @@ export const parseHtml = async (
|
|||
|
||||
const hastTree = pipeline.runSync(tree, file)
|
||||
|
||||
const metadata: {
|
||||
desctription: string
|
||||
images: string[]
|
||||
} = {
|
||||
desctription: file.data.meta?.description || "",
|
||||
images: [],
|
||||
}
|
||||
if (hastTree) {
|
||||
visit(hastTree, (node) => {
|
||||
if (node.type === "element") {
|
||||
if (node.tagName === "img" && typeof node.properties.src === "string") {
|
||||
metadata.images.push(node.properties.src)
|
||||
} else if (node.tagName === "a") {
|
||||
node.properties.target = "_blank"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
metadata,
|
||||
content: toJsxRuntime(hastTree, {
|
||||
Fragment,
|
||||
ignoreInvalidStyle: true,
|
||||
|
|
@ -79,7 +58,7 @@ export const parseHtml = async (
|
|||
style: { maxWidth: "100%", display: "inline" },
|
||||
})
|
||||
}
|
||||
return createElement(Image, { ...props, popper: true })
|
||||
return createElement(Media, { ...props, popper: true })
|
||||
},
|
||||
p: ({ node, ...props }) => {
|
||||
if (node?.children) {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import {
|
|||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@renderer/components/ui/form"
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { Input } from "@renderer/components/ui/input"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import { useModalStack } from "@renderer/components/ui/modal/stacked/hooks"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import type { FeedViewType } from "@renderer/lib/enum"
|
||||
|
|
@ -177,9 +177,9 @@ export function DiscoverForm({ type }: { type: string }) {
|
|||
className="flex min-w-0 flex-1 flex-col items-center gap-1"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{assertEntry.images?.[0] ? (
|
||||
<Image
|
||||
src={assertEntry.images?.[0]}
|
||||
{assertEntry.media?.[0] ? (
|
||||
<Media
|
||||
src={assertEntry.media?.[0].url}
|
||||
className="aspect-square w-full"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { FeedIcon } from "@renderer/components/feed-icon"
|
||||
import { RelativeTime } from "@renderer/components/ui/datetime"
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import { FEED_COLLECTION_LIST } from "@renderer/constants"
|
||||
import { useAsRead } from "@renderer/hooks/biz/useAsRead"
|
||||
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
|
||||
|
|
@ -111,9 +111,10 @@ export function ListItem({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{withDetails && entry.entries.images?.[0] && (
|
||||
<Image
|
||||
src={entry.entries.images[0]}
|
||||
{withDetails && entry.entries.media?.[0] && (
|
||||
<Media
|
||||
src={entry.entries.media[0].url}
|
||||
type={entry.entries.media[0].type}
|
||||
className="ml-2 size-20 shrink-0"
|
||||
loading="lazy"
|
||||
proxy={{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { SwipeImages } from "@renderer/components/ui/image/swipe-images"
|
||||
import { SwipeMedia } from "@renderer/components/ui/media/swipe-media"
|
||||
import { ReactVirtuosoItemPlaceholder } from "@renderer/components/ui/placeholder"
|
||||
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { GridItem } from "@renderer/modules/entry-column/grid-item-template"
|
||||
import { useEntry } from "@renderer/store/entry/hooks"
|
||||
|
||||
import { usePreviewImages } from "../../components/ui/image/hooks"
|
||||
import { usePreviewMedia } from "../../components/ui/media/hooks"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function PictureItem({
|
||||
|
|
@ -19,7 +19,7 @@ export function PictureItem({
|
|||
({ entryId }) => entryId === entry?.entries.id,
|
||||
)
|
||||
|
||||
const previewImage = usePreviewImages()
|
||||
const previewMedia = usePreviewMedia()
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
return (
|
||||
<GridItem
|
||||
|
|
@ -28,16 +28,16 @@ export function PictureItem({
|
|||
translation={translation}
|
||||
>
|
||||
<div className="relative flex gap-2 overflow-x-auto">
|
||||
<SwipeImages
|
||||
images={entry.entries.images}
|
||||
<SwipeMedia
|
||||
media={entry.entries.media}
|
||||
className={cn(
|
||||
"aspect-square w-full shrink-0 rounded-md",
|
||||
isActive && "rounded-b-none",
|
||||
)}
|
||||
imgClassName="object-cover"
|
||||
uniqueKey={entryId}
|
||||
onPreview={(images, i) => {
|
||||
previewImage(images, i)
|
||||
onPreview={(media, i) => {
|
||||
previewMedia(media, i)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { FeedIcon } from "@renderer/components/feed-icon"
|
||||
import { RelativeTime } from "@renderer/components/ui/datetime"
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { usePreviewImages } from "@renderer/components/ui/image/hooks"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import { usePreviewMedia } from "@renderer/components/ui/media/hooks"
|
||||
import { useAsRead } from "@renderer/hooks/biz/useAsRead"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { useEntry } from "@renderer/store/entry/hooks"
|
||||
|
|
@ -19,7 +19,7 @@ export const SocialMediaItem: EntryListItemFC = ({
|
|||
}) => {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
|
||||
const previewImage = usePreviewImages()
|
||||
const previewMedia = usePreviewMedia()
|
||||
const asRead = useAsRead(entry)
|
||||
const feed = useFeedById(entry?.feedId)
|
||||
|
||||
|
|
@ -68,10 +68,11 @@ export const SocialMediaItem: EntryListItemFC = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex gap-2 overflow-x-auto">
|
||||
{entry.entries.images?.map((image, i, images) => (
|
||||
<Image
|
||||
key={image}
|
||||
src={image}
|
||||
{entry.entries.media?.map((media, i, mediaList) => (
|
||||
<Media
|
||||
key={media.url}
|
||||
src={media.url}
|
||||
type={media.type}
|
||||
className="size-28 shrink-0"
|
||||
loading="lazy"
|
||||
proxy={{
|
||||
|
|
@ -79,7 +80,7 @@ export const SocialMediaItem: EntryListItemFC = ({
|
|||
height: 224,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
previewImage(images, i)
|
||||
previewMedia(mediaList, i)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { m } from "@renderer/components/common/Motion"
|
||||
import { Image } from "@renderer/components/ui/image"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import { useModalStack } from "@renderer/components/ui/modal"
|
||||
import { NoopChildren } from "@renderer/components/ui/modal/stacked/utils"
|
||||
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
|
||||
|
|
@ -66,9 +66,9 @@ export function VideoItem({ entryId, entryPreview, translation }: UniversalItemP
|
|||
className={cn("pointer-events-none aspect-video w-full shrink-0 rounded-md bg-black object-cover", isActive && "rounded-b-none")}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
key={entry.entries.images?.[0]}
|
||||
src={entry.entries.images?.[0]}
|
||||
<Media
|
||||
key={entry.entries.media?.[0].url}
|
||||
src={entry.entries.media?.[0].url}
|
||||
className={cn("aspect-video w-full shrink-0 rounded-md object-cover", isActive && "rounded-b-none")}
|
||||
loading="lazy"
|
||||
proxy={{
|
||||
|
|
|
|||
Loading…
Reference in New Issue