feat: entries images -> media

This commit is contained in:
DIYgod 2024-07-24 20:58:51 +08:00
parent b8dc9802a3
commit 39e9cfc7ba
No known key found for this signature in database
18 changed files with 276 additions and 268 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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()),

View File

@ -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={{

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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>

View File

@ -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,

View File

@ -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 };

View File

@ -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.", {

View File

@ -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) {

View File

@ -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"
/>
) : (

View File

@ -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={{

View File

@ -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>

View File

@ -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()
}}
/>

View File

@ -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={{