Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: manual action #1867

Merged
merged 21 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/renderer/src/atoms/ai-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { atom } from "jotai"

import { createAtomHooks } from "~/lib/jotai"

export const [, , useShowAISummary, , getShowAISummary, setShowAISummary] = createAtomHooks(
atom<boolean>(false),
)

export const toggleShowAISummary = () => setShowAISummary(!getShowAISummary())
export const enableShowAISummary = () => setShowAISummary(true)
export const disableShowAISummary = () => setShowAISummary(false)
10 changes: 10 additions & 0 deletions apps/renderer/src/atoms/ai-translation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { atom } from "jotai"

import { createAtomHooks } from "~/lib/jotai"

export const [, , useShowAITranslation, , getShowAITranslation, setShowAITranslation] =
createAtomHooks(atom<boolean>(false))

export const toggleShowAITranslation = () => setShowAITranslation(!getShowAITranslation())
export const enableShowAITranslation = () => setShowAITranslation(true)
export const disableShowAITranslation = () => setShowAITranslation(false)
2 changes: 2 additions & 0 deletions apps/renderer/src/atoms/settings/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const createDefaultSettings = (): GeneralSettings => ({
// App
appLaunchOnStartup: false,
language: "en",
translationLanguage: "zh-CN",

// mobile app
startupScreen: "timeline",
// Data control
Expand Down
13 changes: 5 additions & 8 deletions apps/renderer/src/components/ui/markdown/HTML.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js"
import katexStyle from "katex/dist/katex.min.css?raw"
import { createElement, Fragment, memo, useEffect, useMemo, useRef, useState } from "react"
import { createElement, Fragment, memo, useEffect, useMemo, useState } from "react"

import { useShowAITranslation } from "~/atoms/ai-translation"
import { ENTRY_CONTENT_RENDER_CONTAINER_ID } from "~/constants/dom"
import { parseHtml } from "~/lib/parse-html"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
Expand Down Expand Up @@ -53,15 +54,11 @@ const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(props: HTMLProp

const [refElement, setRefElement] = useState<HTMLElement | null>(null)

const onceRef = useRef(false)
useEffect(() => {
if (onceRef.current || !refElement) {
return
}
const showAITranslation = useShowAITranslation()

useEffect(() => {
translate?.(refElement)
onceRef.current = true
}, [translate, refElement])
}, [refElement, showAITranslation, translate])

const markdownElement = useMemo(
() =>
Expand Down
29 changes: 28 additions & 1 deletion apps/renderer/src/hooks/biz/useEntryActions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isMobile } from "@follow/components/hooks/useMobile.js"
import type { FeedViewType } from "@follow/constants"
import { FeedViewType } from "@follow/constants"
import type { ReactNode } from "react"
import { useCallback, useMemo } from "react"

import { useShowAISummary } from "~/atoms/ai-summary"
import { useShowAITranslation } from "~/atoms/ai-translation"
import {
getReadabilityStatus,
ReadabilityStatus,
Expand Down Expand Up @@ -82,6 +84,9 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
const isInbox = !!inbox

const isShowSourceContent = useShowSourceContent()
const isShowAISummary = useShowAISummary()
const isShowAITranslation = useShowAITranslation()

const getCmd = useGetCommand()
const runCmdFn = useRunCommandFn()
const actionConfigs = useMemo(() => {
Expand Down Expand Up @@ -156,6 +161,26 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
hide: !isShowSourceContent,
active: true,
},
{
id: COMMAND_ID.entry.toggleAISummary,
onClick: runCmdFn(COMMAND_ID.entry.toggleAISummary, []),
hide:
!!entry?.settings?.summary ||
([FeedViewType.SocialMedia, FeedViewType.Videos] as (number | undefined)[]).includes(
entry?.view,
),
active: isShowAISummary,
},
{
id: COMMAND_ID.entry.toggleAITranslation,
onClick: runCmdFn(COMMAND_ID.entry.toggleAITranslation, []),
hide:
!!entry?.settings?.translation ||
([FeedViewType.SocialMedia, FeedViewType.Videos] as (number | undefined)[]).includes(
entry?.view,
),
active: isShowAITranslation,
},
{
id: COMMAND_ID.entry.share,
onClick: runCmdFn(COMMAND_ID.entry.share, [{ entryId }]),
Expand Down Expand Up @@ -194,6 +219,8 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
getCmd,
inList,
isInbox,
isShowAISummary,
isShowAITranslation,
isShowSourceContent,
runCmdFn,
view,
Expand Down
4 changes: 4 additions & 0 deletions apps/renderer/src/hooks/biz/useNavigateEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { FeedViewType } from "@follow/constants"
import { isUndefined } from "es-toolkit/compat"
import { useCallback } from "react"

import { disableShowAISummary } from "~/atoms/ai-summary"
import { disableShowAITranslation } from "~/atoms/ai-translation"
import { setSidebarActiveView } from "~/atoms/sidebar"
import { resetShowSourceContent } from "~/atoms/source-content"
import {
Expand Down Expand Up @@ -69,6 +71,8 @@ export const navigateEntry = (options: NavigateEntryOptions) => {
setSidebarActiveView(view)
}
resetShowSourceContent()
disableShowAISummary()
disableShowAITranslation()

const finalView = nextSearchParams.get("view")

Expand Down
39 changes: 33 additions & 6 deletions apps/renderer/src/lib/immersive-translate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SupportedLanguages } from "@follow/models/types"
import { franc } from "franc-min"

import type { FlatEntryModel } from "~/store/entry"
Expand All @@ -22,15 +23,34 @@ export function immersiveTranslate({
html,
entry,
cache,
targetLanguage,
}: {
html?: HTMLElement
entry: FlatEntryModel
cache?: {
get: (key: string) => string | undefined
set: (key: string, value: string) => void
}
targetLanguage?: SupportedLanguages
}) {
if (!html || !entry.settings?.translation) {
if (!html) {
return
}

const translation = entry.settings?.translation ?? targetLanguage

const immersiveTranslateMark = html.querySelectorAll("[data-immersive-translate-mark=true]")
if (immersiveTranslateMark.length > 0) {
if (translation) {
return
}

for (const mark of immersiveTranslateMark) {
mark.remove()
}
}

if (!translation) {
return
}

Expand All @@ -42,7 +62,7 @@ export function immersiveTranslate({

translate({
entry,
language: entry.settings?.translation,
language: translation,
part: textNode.textContent,
extraFields: ["content"],
}).then((transformed) => {
Expand All @@ -52,8 +72,13 @@ export function immersiveTranslate({

const p = document.createElement("p")
p.append(document.createTextNode(textNode.textContent!))
p.append(document.createElement("br"))
p.append(document.createTextNode(transformed.content))

const fontTag = document.createElement("font")
fontTag.dataset["immersiveTranslateMark"] = "true"
fontTag.append(document.createElement("br"))
fontTag.append(document.createTextNode(transformed.content))

p.append(fontTag)

textNode.replaceWith(p)
})
Expand All @@ -70,14 +95,15 @@ export function immersiveTranslate({

for (const tag of tags) {
const sourceLanguage = franc(tag.textContent ?? "")
if (sourceLanguage === LanguageMap[entry.settings?.translation].code) {
if (sourceLanguage === LanguageMap[translation].code) {
return
}

const children = Array.from(tag.childNodes)
tag.dataset.childCount = children.filter((child) => child.textContent).length.toString()

const fontTag = document.createElement("font")
fontTag.dataset["immersiveTranslateMark"] = "true"

if (children.length > 0) {
fontTag.style.display = "none"
Expand Down Expand Up @@ -121,7 +147,7 @@ export function immersiveTranslate({

translate({
entry,
language: entry.settings?.translation,
language: translation,
part: textContent,
extraFields: ["content"],
}).then((transformed) => {
Expand All @@ -140,6 +166,7 @@ export function immersiveTranslate({
}

const parentFontTag = document.createElement("font")
parentFontTag.dataset["immersiveTranslateMark"] = "true"
parentFontTag.append(document.createElement("br"))
parentFontTag.append(fontTag)

Expand Down
17 changes: 13 additions & 4 deletions apps/renderer/src/lib/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,29 @@ import { apiClient } from "./api-fetch"
export const LanguageMap: Record<
SupportedLanguages,
{
label: string
value: string
code: string
}
> = {
en: {
value: "en",
label: "English",
code: "eng",
},
ja: {
value: "ja",
label: "Japanese",
code: "jpn",
},
"zh-CN": {
value: "zh-CN",
label: "Simplified Chinese",
code: "cmn",
},
"zh-TW": {
value: "zh-TW",
label: "Traditional Chinese(Taiwan)",
code: "cmn",
},
}
Expand All @@ -40,18 +50,17 @@ export async function translate({
if (!language) {
return null
}
let fields =
entry.settings?.translation && view !== undefined ? views[view!].translation.split(",") : []
let fields = language && view !== undefined ? views[view!].translation.split(",") : []
if (extraFields) {
fields = [...fields, ...extraFields]
}
const { franc } = await import("franc-min")

fields = fields.filter((field) => {
if (entry.settings?.translation && entry.entries[field]) {
if (language && entry.entries[field]) {
const sourceLanguage = franc(entry.entries[field])

if (sourceLanguage === LanguageMap[entry.settings?.translation].code) {
if (sourceLanguage === LanguageMap[language].code) {
return false
} else {
return true
Expand Down
18 changes: 18 additions & 0 deletions apps/renderer/src/modules/command/commands/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useMutation } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"

import { toggleShowAISummary } from "~/atoms/ai-summary"
import { toggleShowAITranslation } from "~/atoms/ai-translation"
import { setShowSourceContent, useSourceContentModal } from "~/atoms/source-content"
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
Expand Down Expand Up @@ -290,5 +292,21 @@ export const useRegisterEntryCommands = () => {
unread.mutate({ entryId, feedId: entry.feedId })
},
},
{
id: COMMAND_ID.entry.toggleAISummary,
label: t("entry_actions.toggle_ai_summary"),
icon: <i className="i-mgc-magic-2-cute-re" />,
run: () => {
toggleShowAISummary()
},
},
{
id: COMMAND_ID.entry.toggleAITranslation,
label: t("entry_actions.toggle_ai_translation"),
icon: <i className="i-mgc-translate-2-cute-re" />,
run: () => {
toggleShowAITranslation()
},
},
])
}
2 changes: 2 additions & 0 deletions apps/renderer/src/modules/command/commands/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const COMMAND_ID = {
share: "entry:share",
read: "entry:read",
unread: "entry:unread",
toggleAISummary: "entry:toggle-ai-summary",
toggleAITranslation: "entry:toggle-ai-translation",
},
integration: {
saveToEagle: "integration:save-to-eagle",
Expand Down
12 changes: 12 additions & 0 deletions apps/renderer/src/modules/command/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ export type UnReadCommand = Command<{
fn: ({ entryId }) => void
}>

export type ToggleAISummaryCommand = Command<{
id: typeof COMMAND_ID.entry.toggleAISummary
fn: () => void
}>

export type ToggleAITranslationCommand = Command<{
id: typeof COMMAND_ID.entry.toggleAITranslation
fn: () => void
}>

export type EntryCommand =
| TipCommand
| StarCommand
Expand All @@ -76,6 +86,8 @@ export type EntryCommand =
| ShareCommand
| ReadCommand
| UnReadCommand
| ToggleAISummaryCommand
| ToggleAITranslationCommand

// Integration commands

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useFeedActions } from "~/hooks/biz/useFeedActions"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
import { useContextMenu } from "~/hooks/common/useContextMenu"
import { COMMAND_ID } from "~/modules/command/commands/id"
import type { FlatEntryModel } from "~/store/entry"
import { entryActions } from "~/store/entry"

Expand Down Expand Up @@ -77,12 +78,23 @@ export const EntryItemWrapper: FC<
setIsContextMenuOpen(true)
await showContextMenu(
[
...actionConfigs.map((item) => ({
type: "text" as const,
label: item.name,
click: () => item.onClick(),
shortcut: item.shortcut,
})),
...actionConfigs
.filter(
(item) =>
!(
[
COMMAND_ID.entry.viewSourceContent,
COMMAND_ID.entry.toggleAISummary,
COMMAND_ID.entry.toggleAITranslation,
] as string[]
).includes(item.id),
)
.map((item) => ({
type: "text" as const,
label: item.name,
click: () => item.onClick(),
shortcut: item.shortcut,
})),
{
type: "separator" as const,
},
Expand Down
Loading
Loading