feat: apply title and description ai translation
This commit is contained in:
parent
78c030613f
commit
810111291c
|
|
@ -8,7 +8,7 @@ import { useEntry } from "@renderer/store/entry"
|
|||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function ArticleItem({ entryId, entryPreview }: UniversalItemProps) {
|
||||
export function ArticleItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
|
||||
const asRead = useAsRead(entry)
|
||||
|
|
@ -33,13 +33,13 @@ export function ArticleItem({ entryId, entryPreview }: UniversalItemProps) {
|
|||
</span>
|
||||
</div>
|
||||
<div className={cn("relative my-0.5 break-words", !!entry.collections && "pr-4", entry.entries.title ? "font-medium" : "text-[13px]")}>
|
||||
{entry.entries.title || entry.entries.description}
|
||||
{translation?.title || translation?.description || entry.entries.title || entry.entries.description}
|
||||
{!!entry.collections && (
|
||||
<i className="i-mingcute-star-fill absolute right-0 top-0.5 text-orange-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("text-[13px]", asRead ? "text-zinc-400" : "text-zinc-500")}>
|
||||
{entry.entries.description}
|
||||
{translation?.description || entry.entries.description}
|
||||
</div>
|
||||
</div>
|
||||
{entry.entries.images?.[0] && (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useEntry } from "@renderer/store/entry"
|
|||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function AudioItem({ entryId, entryPreview }: UniversalItemProps) {
|
||||
export function AudioItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
return (
|
||||
|
|
@ -26,7 +26,7 @@ export function AudioItem({ entryId, entryPreview }: UniversalItemProps) {
|
|||
</span>
|
||||
</div>
|
||||
<div className={cn("relative my-0.5", !!entry.collections && "pr-4")}>
|
||||
{entry.entries.title}
|
||||
{translation?.title || entry.entries.title}
|
||||
{!!entry.collections && (
|
||||
<i className="i-mingcute-star-fill absolute right-0 top-0.5 text-orange-400" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from "@renderer/components/ui/popover"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { views } from "@renderer/lib/constants"
|
||||
import { FeedViewType } from "@renderer/lib/enum"
|
||||
import { buildStorageNS } from "@renderer/lib/ns"
|
||||
import { getEntriesParams } from "@renderer/lib/utils"
|
||||
import { useEntries } from "@renderer/queries/entries"
|
||||
|
|
@ -29,14 +28,7 @@ import { useEventCallback } from "usehooks-ts"
|
|||
import { useShallow } from "zustand/react/shallow"
|
||||
|
||||
import { EmptyIcon } from "../icons/empty"
|
||||
import { ArticleItem } from "./article-item"
|
||||
import { AudioItem } from "./audio-item"
|
||||
import { EntryItemWrapper } from "./item-wrapper"
|
||||
import { NotificationItem } from "./notification-item"
|
||||
import { PictureItem } from "./picture-item"
|
||||
import { SocialMediaItem } from "./social-media-item"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
import { VideoItem } from "./video-item"
|
||||
|
||||
const unreadOnlyAtom = atomWithStorage<boolean>(
|
||||
buildStorageNS("entry-unreadonly"),
|
||||
|
|
@ -50,38 +42,6 @@ export function EntryColumn() {
|
|||
const entries = useEntriesByView()
|
||||
const { entriesIds } = entries
|
||||
|
||||
let Item: FC<UniversalItemProps>
|
||||
|
||||
switch (activeList?.view) {
|
||||
case FeedViewType.Articles: {
|
||||
Item = ArticleItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.SocialMedia: {
|
||||
Item = SocialMediaItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Pictures: {
|
||||
Item = PictureItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Videos: {
|
||||
Item = VideoItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Audios: {
|
||||
Item = AudioItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Notifications: {
|
||||
Item = NotificationItem
|
||||
break
|
||||
}
|
||||
default: {
|
||||
Item = ArticleItem
|
||||
}
|
||||
}
|
||||
|
||||
const handleRangeChange = useEventCallback(
|
||||
debounce(
|
||||
async ({ startIndex }: ListRange) => {
|
||||
|
|
@ -134,12 +94,10 @@ export function EntryColumn() {
|
|||
key={entryId}
|
||||
entryId={entryId}
|
||||
view={activeList?.view}
|
||||
>
|
||||
<Item entryId={entryId} />
|
||||
</EntryItemWrapper>
|
||||
/>
|
||||
)
|
||||
},
|
||||
[Item, activeList?.view],
|
||||
[activeList?.view],
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,29 @@
|
|||
import { useAsRead } from "@renderer/hooks/useAsRead"
|
||||
import { useBizQuery } from "@renderer/hooks/useBizQuery"
|
||||
import { useEntryActions, useRead } from "@renderer/hooks/useEntryActions"
|
||||
import { views } from "@renderer/lib/constants"
|
||||
import { FeedViewType } from "@renderer/lib/enum"
|
||||
import { showNativeMenu } from "@renderer/lib/native-menu"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { Queries } from "@renderer/queries"
|
||||
import { feedActions, useFeedStore } from "@renderer/store"
|
||||
import { useEntry } from "@renderer/store/entry"
|
||||
import type { FC } from "react"
|
||||
|
||||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
import { ArticleItem } from "./article-item"
|
||||
import { AudioItem } from "./audio-item"
|
||||
import { NotificationItem } from "./notification-item"
|
||||
import { PictureItem } from "./picture-item"
|
||||
import { SocialMediaItem } from "./social-media-item"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
import { VideoItem } from "./video-item"
|
||||
|
||||
export function EntryItemWrapper({
|
||||
children,
|
||||
entryId,
|
||||
view,
|
||||
}: {
|
||||
entryId: string
|
||||
children: React.ReactNode
|
||||
view?: number
|
||||
}) {
|
||||
const entry = useEntry(entryId)
|
||||
|
|
@ -23,6 +32,14 @@ export function EntryItemWrapper({
|
|||
entry,
|
||||
})
|
||||
|
||||
const translation = useBizQuery(Queries.ai.translation({
|
||||
id: entry.entries.id,
|
||||
language: entry.settings?.translation,
|
||||
fields: view ? views[view].translation : "",
|
||||
}), {
|
||||
enabled: !!entry.settings?.translation,
|
||||
})
|
||||
|
||||
const activeEntry = useFeedStore((state) => state.activeEntry)
|
||||
|
||||
const asRead = useAsRead(entry)
|
||||
|
|
@ -32,6 +49,38 @@ export function EntryItemWrapper({
|
|||
// NOTE: prevent 0 height element, react virtuoso will not stop render any more
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
|
||||
let Item: FC<UniversalItemProps>
|
||||
|
||||
switch (view) {
|
||||
case FeedViewType.Articles: {
|
||||
Item = ArticleItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.SocialMedia: {
|
||||
Item = SocialMediaItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Pictures: {
|
||||
Item = PictureItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Videos: {
|
||||
Item = VideoItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Audios: {
|
||||
Item = AudioItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Notifications: {
|
||||
Item = NotificationItem
|
||||
break
|
||||
}
|
||||
default: {
|
||||
Item = ArticleItem
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(!views[view || 0].wideMode && "pb-3")}>
|
||||
<div
|
||||
|
|
@ -66,7 +115,7 @@ export function EntryItemWrapper({
|
|||
)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Item entryId={entryId} translation={translation.data} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useEntry } from "@renderer/store/entry"
|
|||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function NotificationItem({ entryId, entryPreview }: UniversalItemProps) {
|
||||
export function NotificationItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
return (
|
||||
|
|
@ -26,7 +26,7 @@ export function NotificationItem({ entryId, entryPreview }: UniversalItemProps)
|
|||
</span>
|
||||
</div>
|
||||
<div className={cn("relative my-0.5", !!entry.collections && "pr-4")}>
|
||||
{entry.entries.title}
|
||||
{translation?.title || entry.entries.title}
|
||||
{!!entry.collections && (
|
||||
<i className="i-mingcute-star-fill absolute right-0 top-0.5 text-orange-400" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useEntry } from "@renderer/store/entry"
|
|||
import { usePreviewImages } from "../ui/image/hooks"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function PictureItem({ entryId, entryPreview }: UniversalItemProps) {
|
||||
export function PictureItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
const previewImage = usePreviewImages()
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
|
|
@ -37,7 +37,7 @@ export function PictureItem({ entryId, entryPreview }: UniversalItemProps) {
|
|||
!!entry.collections && "pr-4",
|
||||
)}
|
||||
>
|
||||
{entry.entries.title}
|
||||
{translation?.title || entry.entries.title}
|
||||
{!!entry.collections && (
|
||||
<i className="i-mingcute-star-fill absolute right-0 top-0.5 text-orange-400" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useEntry } from "@renderer/store/entry"
|
|||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function SocialMediaItem({ entryId, entryPreview }: UniversalItemProps) {
|
||||
export function SocialMediaItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
|
||||
// NOTE: prevent 0 height element, react virtuoso will not stop render any more
|
||||
|
|
@ -30,7 +30,7 @@ export function SocialMediaItem({ entryId, entryPreview }: UniversalItemProps) {
|
|||
</span>
|
||||
</div>
|
||||
<div className={cn("relative mt-0.5", !!entry.collections && "pr-4")}>
|
||||
{entry.entries.description}
|
||||
{translation?.description || entry.entries.description}
|
||||
{!!entry.collections && (
|
||||
<i className="i-mingcute-star-fill absolute right-0 top-0.5 text-orange-400" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,8 @@ export type UniversalItemProps = {
|
|||
entryPreview?: EntryModel | {
|
||||
feeds: FeedModel
|
||||
}
|
||||
translation?: {
|
||||
title?: string
|
||||
description?: string
|
||||
} | null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useMemo, useRef, useState } from "react"
|
|||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
import type { UniversalItemProps } from "./types"
|
||||
|
||||
export function VideoItem({ entryId, entryPreview }: UniversalItemProps) {
|
||||
export function VideoItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
|
||||
const iframeSrc = useMemo(() => urlToIframe(entry.entries.url), [entry.entries.url])
|
||||
|
|
@ -52,7 +52,7 @@ export function VideoItem({ entryId, entryPreview }: UniversalItemProps) {
|
|||
</div>
|
||||
<div className="flex-1 px-2 pb-3 pt-1 text-sm">
|
||||
<div className={cn("relative mb-0.5 mt-1 truncate font-medium", !!entry.collections && "pr-4")}>
|
||||
{entry.entries.title}
|
||||
{translation?.title || entry.entries.title}
|
||||
{!!entry.collections && (
|
||||
<i className="i-mingcute-star-fill absolute right-0 top-0.5 text-orange-400" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,27 @@ import * as hono_utils_http_status from 'hono/utils/http-status';
|
|||
import * as hono from 'hono';
|
||||
|
||||
declare const routes: hono_hono_base.HonoBase<hono.Env, {
|
||||
"/ai/translation": {
|
||||
$get: {
|
||||
input: {
|
||||
query: {
|
||||
id: string;
|
||||
language: "en" | "ja" | "zh-CN" | "zh-TW";
|
||||
fields: string;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
code: 0;
|
||||
data?: {
|
||||
description?: string | undefined;
|
||||
title?: string | undefined;
|
||||
} | undefined;
|
||||
};
|
||||
outputFormat: "json";
|
||||
status: 200;
|
||||
};
|
||||
};
|
||||
} & {
|
||||
"/actions": {
|
||||
$get: {
|
||||
input: {};
|
||||
|
|
@ -373,7 +394,6 @@ declare const routes: hono_hono_base.HonoBase<hono.Env, {
|
|||
feedIdList?: string[] | undefined;
|
||||
publishedAfter?: string | undefined;
|
||||
collected?: boolean | undefined;
|
||||
withContent?: boolean | undefined;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
|
|
@ -393,7 +413,6 @@ declare const routes: hono_hono_base.HonoBase<hono.Env, {
|
|||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
content?: string | null | undefined;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ export const views = [
|
|||
name: "Articles",
|
||||
icon: <i className="i-mingcute-news-fill" />,
|
||||
className: "text-amber-700",
|
||||
translation: "title,description",
|
||||
},
|
||||
{
|
||||
name: "Social Media",
|
||||
icon: <i className="i-mingcute-twitter-fill" />,
|
||||
className: "text-sky-600",
|
||||
wideMode: true,
|
||||
translation: "description",
|
||||
},
|
||||
{
|
||||
name: "Pictures",
|
||||
|
|
@ -25,6 +27,7 @@ export const views = [
|
|||
className: "text-green-600",
|
||||
gridMode: true,
|
||||
wideMode: true,
|
||||
translation: "title",
|
||||
},
|
||||
{
|
||||
name: "Videos",
|
||||
|
|
@ -32,16 +35,19 @@ export const views = [
|
|||
className: "text-red-600",
|
||||
gridMode: true,
|
||||
wideMode: true,
|
||||
translation: "title",
|
||||
},
|
||||
{
|
||||
name: "Audios",
|
||||
icon: <i className="i-mingcute-headphone-fill" />,
|
||||
className: "text-purple-600",
|
||||
translation: "title",
|
||||
},
|
||||
{
|
||||
name: "Notifications",
|
||||
icon: <i className="i-mingcute-notification-fill" />,
|
||||
className: "text-yellow-600",
|
||||
translation: "title",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { defineQuery } from "@renderer/lib/defineQuery"
|
||||
|
||||
export const ai = {
|
||||
translation: ({
|
||||
id,
|
||||
language,
|
||||
fields,
|
||||
}: {
|
||||
id: string
|
||||
language?: string
|
||||
fields: string
|
||||
}) =>
|
||||
defineQuery(["translation", id, language], async () => {
|
||||
if (!language) {
|
||||
return null
|
||||
}
|
||||
const res = await apiClient.ai.translation.$get({
|
||||
query: {
|
||||
id,
|
||||
language: language as "en" | "ja" | "zh-CN" | "zh-TW",
|
||||
fields,
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
}),
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { action } from "./actions"
|
||||
import { ai } from "./ai"
|
||||
import { auth } from "./auth"
|
||||
import { entries } from "./entries"
|
||||
import { feed } from "./feed"
|
||||
|
|
@ -10,4 +11,5 @@ export const Queries = {
|
|||
feed,
|
||||
action,
|
||||
auth,
|
||||
ai,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue