feat(desktop): implement popover for sharing entries with social media options
- Added a new Popover component to display sharing options for entries. - Created SharePanel component to handle different sharing actions, including native sharing and copying links. - Integrated mouse position tracking to position the popover correctly. - Updated entry action commands to utilize the new sharing functionality. - Added new icons for social media platforms and updated localization files for share-related texts. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
18b22b203f
commit
397365c508
|
|
@ -0,0 +1,43 @@
|
|||
import type { PopoverContentProps } from "@radix-ui/react-popover"
|
||||
import { atom } from "jotai"
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
import { createAtomHooks, jotaiStore } from "~/lib/jotai"
|
||||
|
||||
// Atom
|
||||
|
||||
export interface PopoverProps extends Omit<PopoverContentProps, "children"> {
|
||||
/** Custom z-index for popover */
|
||||
zIndex?: number
|
||||
/** Whether the popover should close when clicked outside */
|
||||
modal?: boolean
|
||||
}
|
||||
|
||||
type PopoverState =
|
||||
| { open: false }
|
||||
| {
|
||||
open: true
|
||||
position: { x: number; y: number }
|
||||
content: ReactNode
|
||||
props?: PopoverProps
|
||||
// Just for abort callback
|
||||
abortController: AbortController
|
||||
}
|
||||
|
||||
export const [popoverAtom, usePopoverState, usePopoverValue, useSetPopover] = createAtomHooks(
|
||||
atom<PopoverState>({ open: false }),
|
||||
)
|
||||
|
||||
export const showPopover = (
|
||||
mouseXY: { x: number; y: number },
|
||||
element: ReactNode,
|
||||
props?: PopoverProps,
|
||||
) => {
|
||||
jotaiStore.set(popoverAtom, {
|
||||
open: true,
|
||||
position: mouseXY,
|
||||
content: element,
|
||||
props,
|
||||
abortController: new AbortController(),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { ipcServices } from "~/lib/client"
|
||||
import { getEntry } from "~/store/entry"
|
||||
|
||||
interface SharePanelProps {
|
||||
entryId: string
|
||||
}
|
||||
|
||||
interface ShareOption {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
action: () => Promise<void> | void
|
||||
color?: string
|
||||
bgColor?: string
|
||||
}
|
||||
|
||||
interface SocialShareOption {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
url: string
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
const socialOptions: SocialShareOption[] = [
|
||||
{
|
||||
id: "twitter",
|
||||
label: "X",
|
||||
icon: tw`i-mgc-social-x-cute-re`,
|
||||
url: "https://x.com/intent/tweet?text={text}&url={url}",
|
||||
color: "text-white",
|
||||
bgColor: "bg-black",
|
||||
},
|
||||
{
|
||||
id: "facebook",
|
||||
label: "Facebook",
|
||||
icon: tw`i-mgc-facebook-cute-re`,
|
||||
url: "https://www.facebook.com/sharer/sharer.php?u={url}",
|
||||
color: "text-white",
|
||||
bgColor: "bg-[#1877F2]",
|
||||
},
|
||||
{
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
icon: tw`i-mgc-telegram-cute-re`,
|
||||
url: "https://t.me/share/url?url={url}&text={text}",
|
||||
color: "text-white",
|
||||
bgColor: "bg-[#0088CC]",
|
||||
},
|
||||
{
|
||||
id: "weibo",
|
||||
label: "微博",
|
||||
icon: tw`i-mgc-weibo-cute-re`,
|
||||
url: "https://service.weibo.com/share/share.php?url={url}&title={text}",
|
||||
color: "text-white",
|
||||
bgColor: "bg-[#E6162D]",
|
||||
},
|
||||
]
|
||||
|
||||
export const SharePanel = ({ entryId }: SharePanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const generateShareContent = useCallback(
|
||||
(entry: ReturnType<typeof getEntry>) => {
|
||||
if (!entry) return null
|
||||
|
||||
const { title, description } = entry.entries
|
||||
const shareUrl = globalThis.location.href
|
||||
|
||||
// Limit text to 50 characters with ellipsis
|
||||
const truncateText = (text: string, maxLength = 50) => {
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text
|
||||
}
|
||||
|
||||
const shareTitle = `${title || t("share.default_title")} - Follow`
|
||||
const baseText = description || title || t("share.default_description")
|
||||
const truncatedText = truncateText(baseText)
|
||||
const shareText = `${truncatedText} | ${t("share.discover_more")}`
|
||||
|
||||
return {
|
||||
title: shareTitle,
|
||||
text: shareText,
|
||||
url: shareUrl,
|
||||
}
|
||||
},
|
||||
[t],
|
||||
)
|
||||
|
||||
const handleNativeShare = useCallback(async () => {
|
||||
const entry = getEntry(entryId)
|
||||
const shareContent = generateShareContent(entry)
|
||||
|
||||
if (!shareContent) return
|
||||
|
||||
try {
|
||||
if (IN_ELECTRON) {
|
||||
// Use Electron's share menu
|
||||
await ipcServices?.menu.showShareMenu(shareContent.url)
|
||||
} else if (navigator.share) {
|
||||
// Use Web Share API
|
||||
await navigator.share({
|
||||
title: shareContent.title,
|
||||
text: shareContent.text,
|
||||
url: shareContent.url,
|
||||
})
|
||||
} else {
|
||||
// Fallback to copying link
|
||||
await navigator.clipboard.writeText(shareContent.url)
|
||||
toast.success(t("share.link_copied"))
|
||||
}
|
||||
} catch {
|
||||
// If sharing fails, copy link as fallback
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareContent.url)
|
||||
toast.success(t("share.link_copied"))
|
||||
} catch {
|
||||
toast.error(t("share.copy_failed"))
|
||||
}
|
||||
}
|
||||
}, [entryId, generateShareContent, t])
|
||||
|
||||
const handleCopyLink = useCallback(async () => {
|
||||
const shareUrl = globalThis.location.href
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl)
|
||||
toast.success(t("share.link_copied"))
|
||||
} catch {
|
||||
toast.error(t("share.copy_failed"))
|
||||
}
|
||||
}, [t])
|
||||
|
||||
const handleSocialShare = useCallback(
|
||||
(shareUrlTemplate: string) => {
|
||||
const entry = getEntry(entryId)
|
||||
const shareContent = generateShareContent(entry)
|
||||
|
||||
if (!shareContent) return
|
||||
|
||||
const encodedUrl = encodeURIComponent(shareContent.url)
|
||||
const shareTitle = encodeURIComponent(shareContent.title)
|
||||
const shareText = encodeURIComponent(shareContent.text)
|
||||
|
||||
const finalUrl = shareUrlTemplate
|
||||
.replace("{url}", encodedUrl)
|
||||
.replace("{title}", shareTitle)
|
||||
.replace("{text}", shareText)
|
||||
|
||||
window.open(finalUrl, "_blank", "width=600,height=400")
|
||||
},
|
||||
[entryId, generateShareContent],
|
||||
)
|
||||
|
||||
const actionOptions: ShareOption[] = [
|
||||
...(IN_ELECTRON || (typeof navigator !== "undefined" && "share" in navigator)
|
||||
? [
|
||||
{
|
||||
id: "native-share",
|
||||
label: t("share.system_share"),
|
||||
icon: "i-mgc-share-forward-cute-re",
|
||||
action: handleNativeShare,
|
||||
color: "text-blue-500",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "copy-link",
|
||||
label: t("share.copy_link"),
|
||||
icon: "i-mgc-link-cute-re",
|
||||
action: handleCopyLink,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto max-w-[400px] px-2">
|
||||
<div className="mb-4 flex flex-col text-center">
|
||||
<h3 className="text-text mb-2 mt-1 font-semibold">{t("share.title")}</h3>
|
||||
{(() => {
|
||||
const entry = getEntry(entryId)
|
||||
const title = entry?.entries?.title
|
||||
return title ? (
|
||||
<p className="text-text-secondary mt-1 min-w-0 text-wrap text-left text-sm font-medium">
|
||||
{title}
|
||||
</p>
|
||||
) : null
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h4 className="text-text-secondary text-xs font-medium uppercase tracking-wide">
|
||||
{t("share.social_media")}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{socialOptions.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-2"
|
||||
onClick={() => handleSocialShare(option.url)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-12 items-center justify-center rounded-full transition-all duration-200",
|
||||
option.bgColor,
|
||||
"group-hover:scale-110 group-active:scale-95",
|
||||
"shadow-lg",
|
||||
)}
|
||||
>
|
||||
<i className={cn(option.icon, "size-5", option.color)} />
|
||||
</div>
|
||||
<span className="text-text-secondary text-xs font-medium">{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
<h4 className="text-text-secondary text-xs font-medium uppercase tracking-wide">
|
||||
{t("share.actions")}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{actionOptions.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"cursor-button relative flex select-none items-center rounded-lg",
|
||||
"text-sm outline-none transition-all duration-200",
|
||||
"hover:bg-fill-secondary/80 active:bg-fill-secondary",
|
||||
"group",
|
||||
)}
|
||||
onClick={() => option.action()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-7 items-center justify-center rounded-full",
|
||||
"bg-fill-tertiary/80 group-hover:bg-fill-tertiary",
|
||||
"transition-colors duration-200",
|
||||
)}
|
||||
>
|
||||
<i
|
||||
className={cn(option.icon, "size-3.5", option.color || "text-text-secondary")}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-text text-xs font-medium">{option.label}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ export const useEntryActions = ({
|
|||
new EntryActionMenuItem({
|
||||
id: COMMAND_ID.entry.share,
|
||||
onClick: runCmdFn(COMMAND_ID.entry.share, [{ entryId }]),
|
||||
hide: !entry.url || !("share" in navigator || IN_ELECTRON),
|
||||
hide: !entry.url,
|
||||
shortcut: shortcuts[COMMAND_ID.entry.share],
|
||||
entryId,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getMousePosition } from "@follow/components/hooks/useMouse.js"
|
||||
import { FeedViewType, UserRole } from "@follow/constants"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { cn, resolveUrlWithBase } from "@follow/utils/utils"
|
||||
|
|
@ -8,6 +9,7 @@ import { toast } from "sonner"
|
|||
import { toggleShowAISummaryOnce } from "~/atoms/ai-summary"
|
||||
import { toggleShowAITranslationOnce } from "~/atoms/ai-translation"
|
||||
import { AudioPlayer, getAudioPlayerAtomValue } from "~/atoms/player"
|
||||
import { showPopover } from "~/atoms/popover"
|
||||
import { useIsInMASReview } from "~/atoms/server-configs"
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import {
|
||||
|
|
@ -16,6 +18,7 @@ import {
|
|||
useSourceContentModal,
|
||||
} from "~/atoms/source-content"
|
||||
import { useUserRole } from "~/atoms/user"
|
||||
import { SharePanel } from "~/components/common/SharePanel"
|
||||
import { toggleEntryReadability } from "~/hooks/biz/useEntryActions"
|
||||
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
|
||||
import { getRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
|
|
@ -283,19 +286,15 @@ export const useRegisterEntryCommands = () => {
|
|||
toast.error("Failed to share: url is not available", { duration: 3000 })
|
||||
return
|
||||
}
|
||||
if (!entry.entries.url) return
|
||||
|
||||
if (IN_ELECTRON) {
|
||||
return ipcServices?.menu.showShareMenu(entry.entries.url)
|
||||
} else {
|
||||
const { title, description } = entry.entries
|
||||
navigator.share({
|
||||
title: title || undefined,
|
||||
text: description || undefined,
|
||||
url: entry.entries.url,
|
||||
})
|
||||
}
|
||||
return
|
||||
const xy = getMousePosition()
|
||||
showPopover(
|
||||
{
|
||||
x: xy.x,
|
||||
y: xy.y + 20,
|
||||
},
|
||||
<SharePanel entryId={entry.entries.id} />,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ export const ContainerToc = memo(
|
|||
className={cn(
|
||||
"animate-in fade-in-0 slide-in-from-bottom-12 easing-spring spring-soft flex flex-col items-end",
|
||||
"scrollbar-none max-h-[calc(100vh-100px)] overflow-auto",
|
||||
"@[700px]:-translate-x-12 @[800px]:-translate-x-16 @[900px]:translate-x-0 @[900px]:items-start",
|
||||
"@[700px]:-translate-x-12 @[800px]:-translate-x-4 @[900px]:translate-x-0 @[900px]:items-start",
|
||||
)}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ export { ExternalJumpInProvider as LazyExternalJumpInProvider } from "../externa
|
|||
export { LottieRenderContainer as LazyLottieRenderContainer } from "~/components/ui/lottie-container"
|
||||
export const LazyReloadPrompt = () => null
|
||||
export const LazyPWAPrompt = () => null
|
||||
|
||||
export { PopoverProvider as LazyPopoverProvider } from "../popover-provider"
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
export { ContextMenuProvider as LazyContextMenuProvider } from "../context-menu-provider"
|
||||
export { ExtensionExposeProvider as LazyExtensionExposeProvider } from "../extension-expose-provider"
|
||||
export { LottieRenderContainer as LazyLottieRenderContainer } from "~/components/ui/lottie-container"
|
||||
export { ModalStackProvider as LazyModalStackProvider } from "~/components/ui/modal"
|
||||
|
||||
const noop = () => null
|
||||
export const LazyReloadPrompt = noop
|
||||
export const LazyPWAPrompt = noop
|
||||
export const LazyExternalJumpInProvider = noop
|
||||
|
|
@ -10,6 +10,11 @@ const LazyContextMenuProvider = lazy(() =>
|
|||
default: res.ContextMenuProvider,
|
||||
})),
|
||||
)
|
||||
const LazyPopoverProvider = lazy(() =>
|
||||
import("./../popover-provider").then((res) => ({
|
||||
default: res.PopoverProvider,
|
||||
})),
|
||||
)
|
||||
|
||||
const LazyExtensionExposeProvider = lazy(() =>
|
||||
import("./../extension-expose-provider").then((res) => ({
|
||||
|
|
@ -47,6 +52,7 @@ export {
|
|||
LazyContextMenuProvider,
|
||||
LazyExtensionExposeProvider,
|
||||
LazyLottieRenderContainer,
|
||||
LazyPopoverProvider,
|
||||
LazyPWAPrompt,
|
||||
LazyReloadPrompt,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
// Import from the correct path
|
||||
import { useSetGlobalFocusableScope } from "@follow/components/common/Focusable/hooks.js"
|
||||
import { Spring } from "@follow/components/constants/spring.js"
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@follow/components/ui/popover/index.jsx"
|
||||
import { AnimatePresence, m } from "motion/react"
|
||||
import { memo, useEffect, useRef } from "react"
|
||||
|
||||
import { usePopoverState } from "~/atoms/popover"
|
||||
import { HotkeyScope } from "~/constants"
|
||||
|
||||
export const PopoverProvider: Component = ({ children }) => (
|
||||
<>
|
||||
{children}
|
||||
<Handler />
|
||||
</>
|
||||
)
|
||||
|
||||
const Handler = memo(() => {
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
const [popoverState, setPopoverState] = usePopoverState()
|
||||
const setGlobalFocusableScope = useSetGlobalFocusableScope()
|
||||
|
||||
useEffect(() => {
|
||||
if (!popoverState.open) return
|
||||
const triggerElement = ref.current
|
||||
if (!triggerElement) return
|
||||
|
||||
triggerElement.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
)
|
||||
}, [popoverState])
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onOpenChange={(state) => {
|
||||
if (state) {
|
||||
setGlobalFocusableScope(HotkeyScope.DropdownMenu, "append")
|
||||
} else {
|
||||
setGlobalFocusableScope(HotkeyScope.DropdownMenu, "remove")
|
||||
setPopoverState({ open: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger
|
||||
ref={ref}
|
||||
className="pointer-events-none"
|
||||
style={
|
||||
popoverState.open
|
||||
? { position: "fixed", top: popoverState.position.y, left: popoverState.position.x }
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
<PopoverContent asChild forceMount>
|
||||
<AnimatePresence>
|
||||
{popoverState.open && (
|
||||
<m.div
|
||||
className="bg-material-ultra-thick backdrop-blur-background mr-2 rounded-xl border p-2 shadow-2xl"
|
||||
initial={{ opacity: 0, scale: 0.95, y: -10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
||||
transition={Spring.presets.smooth}
|
||||
>
|
||||
<PopoverArrow className="fill-border" />
|
||||
{popoverState.content}
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
|
||||
Handler.displayName = "PopoverHandler"
|
||||
|
|
@ -24,6 +24,7 @@ import {
|
|||
LazyExtensionExposeProvider,
|
||||
LazyExternalJumpInProvider,
|
||||
LazyLottieRenderContainer,
|
||||
LazyPopoverProvider,
|
||||
LazyPWAPrompt,
|
||||
LazyReloadPrompt,
|
||||
} from "./lazy/index"
|
||||
|
|
@ -56,6 +57,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
|
|||
<Suspense>
|
||||
<LazyExtensionExposeProvider />
|
||||
<LazyContextMenuProvider />
|
||||
<LazyPopoverProvider />
|
||||
<LazyLottieRenderContainer />
|
||||
<LazyExternalJumpInProvider />
|
||||
<LazyReloadPrompt />
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.08 2.045c-1.874.165-3.723.904-5.28 2.109-.437.339-1.307 1.209-1.646 1.646-1.8 2.326-2.505 5.195-1.976 8.046.29 1.566.959 3.04 1.976 4.354.339.437 1.209 1.307 1.646 1.646 2.441 1.889 5.453 2.566 8.44 1.895 2.487-.559 4.752-2.144 6.145-4.301.806-1.247 1.283-2.527 1.521-4.08.098-.641.098-2.079 0-2.72-.285-1.858-.936-3.388-2.06-4.84-.339-.437-1.209-1.307-1.646-1.646-2.067-1.599-4.554-2.336-7.12-2.109m1.752 1.997a8.182 8.182 0 0 1 4.208 1.747c.354.286 1.027.972 1.286 1.311A8.123 8.123 0 0 1 20 12a8.1 8.1 0 0 1-1.789 5.04c-.286.354-.972 1.027-1.311 1.286a8.467 8.467 0 0 1-2.4 1.269c-.479.156-1.203.325-1.398.325H13V14h.553c.696 0 .893-.052 1.144-.303.183-.183.303-.46.303-.697 0-.237-.12-.514-.303-.697-.251-.251-.448-.303-1.144-.303H13l.002-1.09c.002-1.172.015-1.267.215-1.529a1.18 1.18 0 0 1 .291-.248c.17-.099.234-.111.729-.134.615-.028.772-.082 1.002-.344.18-.205.241-.37.241-.655 0-.285-.061-.45-.241-.655-.254-.29-.375-.325-1.099-.324-.541.002-.69.016-.962.093-.958.27-1.756 1.057-2.054 2.025-.094.306-.099.379-.115 1.591L10.993 12h-.549c-.691 0-.89.053-1.138.3a.96.96 0 0 0 0 1.4c.248.248.447.3 1.141.3H11v5.92h-.102c-.426 0-1.649-.371-2.369-.72a7.375 7.375 0 0 1-2.083-1.459 7.632 7.632 0 0 1-1.645-2.267c-1.321-2.735-.987-5.939.873-8.374.259-.339.932-1.025 1.286-1.311a8.254 8.254 0 0 1 4.16-1.745 10.09 10.09 0 0 1 1.712-.002" fill="#10161F" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18.221 4.525c-.587.085-1.448.357-2.636.832-1.051.421-8.787 3.649-9.561 3.99-2.222.981-3.27 1.694-3.691 2.513-.222.431-.267.665-.244 1.244.023.569.119.869.403 1.267.218.305.463.532.84.778.365.238 1.191.633 1.488.711.347.092.806.085 1.112-.017.719-.24 2.292-1.28 5.105-3.375 2.768-2.063 3.636-2.637 4.074-2.696.136-.018.168-.008.207.064.069.13-.057.368-.406.764-.311.353-.588.616-2.232 2.122-1.764 1.616-2.971 2.873-3.201 3.333-.22.44-.143.918.214 1.329.139.159.433.369 2.767 1.976 1.994 1.373 2.881 1.888 3.58 2.079.981.269 1.83.06 2.502-.615.251-.252.365-.406.512-.693.385-.753.571-1.506 1.023-4.151.653-3.824 1.085-6.441 1.161-7.027.223-1.735.12-2.69-.369-3.422-.541-.808-1.502-1.173-2.648-1.006" fill="#10161F" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 845 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18.34 4.467c-.595.075-1.434.325-2.518.751-.735.289-7.532 3.116-9.11 3.789-1.28.546-2.64 1.218-3.152 1.558-.441.293-1.055.886-1.215 1.175-.228.408-.271.571-.293 1.1-.035.868.127 1.3.706 1.884.784.789 1.926 1.262 5.742 2.377.506.148.99.304 1.075.347.085.043.886.584 1.78 1.203 3.665 2.535 4.219 2.839 5.345 2.93.999.081 1.908-.483 2.419-1.501.411-.819.536-1.362 1.177-5.14.945-5.561 1.043-6.204 1.089-7.131.045-.881-.092-1.635-.389-2.156-.5-.876-1.516-1.329-2.656-1.186m.86 2.12c.219.245.254.802.119 1.895-.115.931-.178 1.323-.762 4.778-.74 4.372-.878 5.074-1.12 5.702-.156.403-.366.638-.57.638-.355 0-.846-.231-1.814-.854a154.94 154.94 0 0 1-3.367-2.297l-.135-.1 2.141-2.144c1.177-1.18 2.173-2.208 2.213-2.285.106-.204.1-.649-.013-.86a1.113 1.113 0 0 0-.501-.478c-.206-.086-.612-.079-.811.015-.108.051-.919.835-2.5 2.419-1.287 1.289-2.361 2.344-2.387 2.344-.085 0-2.935-.868-3.553-1.082-1.712-.593-2.296-1.005-2.098-1.483.151-.364 1.206-.98 3.04-1.775.876-.379 8.136-3.404 8.967-3.735 1.645-.657 2.285-.851 2.745-.834.273.01.303.02.406.136" fill="#10161F" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M16.643 3.069C16.291 3.193 16 3.614 16 4c0 .396.291.808.66.934.107.036.31.066.451.066.341 0 .98.133 1.358.283a4.013 4.013 0 0 1 2.248 2.248c.15.378.283 1.017.283 1.358 0 .348.096.609.299.812.64.64 1.701.172 1.701-.75 0-.107-.019-.369-.042-.583a5.986 5.986 0 0 0-5.326-5.326c-.516-.056-.775-.049-.989.027m-6.176 1.972c-.819.129-1.58.451-2.64 1.115-1.573.985-3.025 2.34-4.058 3.789-1.444 2.024-2.048 4.188-1.629 5.84.382 1.503 1.606 2.89 3.381 3.83 3.475 1.839 8.475 1.838 11.959-.001 1.705-.901 2.924-2.241 3.338-3.671a4.603 4.603 0 0 0 .167-1.458c-.096-1.255-.765-2.226-2.067-2.998l-.383-.227.057-.18c.031-.099.065-.423.076-.72.023-.624-.03-.928-.233-1.33-.665-1.318-2.113-1.794-4.041-1.327a12.27 12.27 0 0 1-.417.097c-.009 0-.017-.064-.017-.142 0-.255-.124-.711-.284-1.046a2.79 2.79 0 0 0-1.815-1.501c-.327-.088-1.051-.124-1.394-.07m.921 2.019c.56.167.747.84.455 1.64-.072.198-.15.441-.173.541-.137.59.33 1.171.946 1.175.211.002.303-.026.684-.202 1.055-.488 1.697-.669 2.38-.672.391-.002.488.011.649.085.466.216.504.813.092 1.473-.23.369-.288.597-.224.88.055.247.229.508.414.621.071.044.255.127.409.185.687.26 1.274.618 1.581.963.463.52.519 1.204.163 1.988-.724 1.597-2.985 2.831-5.844 3.19-.552.07-2.288.07-2.84 0-3.2-.402-5.618-1.896-6.025-3.722-.121-.545-.009-1.388.291-2.184.265-.708.812-1.638 1.398-2.381.425-.539 1.61-1.693 2.173-2.115 1.634-1.227 2.723-1.687 3.471-1.465" fill="#10161F" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -136,16 +136,14 @@
|
|||
"entry_actions.save_to_obsidian": "Save to Obsidian",
|
||||
"entry_actions.save_to_outline": "Save to Outline",
|
||||
"entry_actions.save_to_readeck": "Save to Readeck",
|
||||
"entry_actions.save_to_readwise": "Save to Readwise",
|
||||
"entry_actions.save_to_zotero": "Save to Zotero",
|
||||
"entry_actions.save_to_readwise": "Saved to Readwise.",
|
||||
"entry_actions.save_to_zotero": "Saved to Zotero",
|
||||
"entry_actions.saved_to_cubox": "Saved to Cubox",
|
||||
"entry_actions.saved_to_eagle": "Saved to Eagle.",
|
||||
"entry_actions.saved_to_instapaper": "Saved to Instapaper.",
|
||||
"entry_actions.saved_to_obsidian": "Saved to Obsidian",
|
||||
"entry_actions.saved_to_outline": "Saved to Outline.",
|
||||
"entry_actions.saved_to_readeck": "Saved to Readeck.",
|
||||
"entry_actions.saved_to_readwise": "Saved to Readwise.",
|
||||
"entry_actions.saved_to_zotero": "Saved to Zotero",
|
||||
"entry_actions.share": "Share",
|
||||
"entry_actions.star": "Star / UnStar",
|
||||
"entry_actions.starred": "Starred.",
|
||||
|
|
@ -337,6 +335,16 @@
|
|||
"search.placeholder": "Search...",
|
||||
"search.result_count_local_mode": "(Local mode)",
|
||||
"search.tooltip.local_search": "This search covers locally available data. Try a Refetch to include the latest data.",
|
||||
"share.actions": "Actions",
|
||||
"share.copy_failed": "Failed to copy link",
|
||||
"share.copy_link": "Copy Link",
|
||||
"share.default_description": "Check out this entry",
|
||||
"share.default_title": "Entry Share",
|
||||
"share.discover_more": "Discover more on Follow",
|
||||
"share.link_copied": "Link copied to clipboard",
|
||||
"share.social_media": "Social Media",
|
||||
"share.system_share": "System Share",
|
||||
"share.title": "Share Entry",
|
||||
"shortcuts.guide.title": "Shortcuts Guideline",
|
||||
"sidebar.add_more_feeds": "Add more feeds",
|
||||
"sidebar.category_remove_dialog.cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@
|
|||
"entry_actions.saved_to_obsidian": "Obsidian に保存されました。",
|
||||
"entry_actions.saved_to_outline": "Outline に保存されました。",
|
||||
"entry_actions.saved_to_readeck": "Readeck に保存されました。",
|
||||
"entry_actions.saved_to_readwise": "Readwise に保存されました。",
|
||||
"entry_actions.share": "共有",
|
||||
"entry_actions.star": "スター",
|
||||
"entry_actions.starred": "スターに追加されました。",
|
||||
|
|
|
|||
|
|
@ -143,8 +143,6 @@
|
|||
"entry_actions.saved_to_obsidian": "已保存到 Obsidian。",
|
||||
"entry_actions.saved_to_outline": "已保存到 Outline。",
|
||||
"entry_actions.saved_to_readeck": "已保存到 Readeck。",
|
||||
"entry_actions.saved_to_readwise": "已保存到 Readwise。",
|
||||
"entry_actions.saved_to_zotero": "已保存到 Zotero。",
|
||||
"entry_actions.share": "分享",
|
||||
"entry_actions.star": "收藏",
|
||||
"entry_actions.starred": "已收藏",
|
||||
|
|
@ -336,6 +334,16 @@
|
|||
"search.placeholder": "搜索...",
|
||||
"search.result_count_local_mode": "(本地模式)",
|
||||
"search.tooltip.local_search": "当前搜索仅包含本地可用数据,尝试重新搜索得到更多结果。",
|
||||
"share.actions": "操作",
|
||||
"share.copy_failed": "复制链接失败",
|
||||
"share.copy_link": "复制链接",
|
||||
"share.default_description": "查看这个精彩内容",
|
||||
"share.default_title": "内容分享",
|
||||
"share.discover_more": "在 Follow 上发现更多精彩内容",
|
||||
"share.link_copied": "链接已复制到剪贴板",
|
||||
"share.social_media": "社交媒体",
|
||||
"share.system_share": "系统分享",
|
||||
"share.title": "分享内容",
|
||||
"shortcuts.guide.title": "快捷键指南",
|
||||
"sidebar.add_more_feeds": "添加订阅源",
|
||||
"sidebar.category_remove_dialog.cancel": "取消",
|
||||
|
|
|
|||
|
|
@ -141,8 +141,6 @@
|
|||
"entry_actions.saved_to_obsidian": "已儲存到 Obsidian。",
|
||||
"entry_actions.saved_to_outline": "已儲存到 Outline。",
|
||||
"entry_actions.saved_to_readeck": "已儲存到 Readeck。",
|
||||
"entry_actions.saved_to_readwise": "已儲存到 Readwise。",
|
||||
"entry_actions.saved_to_zotero": "已儲存到 Zotero。",
|
||||
"entry_actions.share": "分享",
|
||||
"entry_actions.star": "收藏",
|
||||
"entry_actions.starred": "已收藏",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { atom } from "jotai"
|
||||
|
||||
export const mouseAtom = atom({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { jotaiStore } from "@follow/utils"
|
||||
import { useAtomValue } from "jotai"
|
||||
|
||||
import { mouseAtom } from "../atoms/mouse"
|
||||
|
||||
export const useMousePosition = () => {
|
||||
return useAtomValue(mouseAtom)
|
||||
}
|
||||
|
||||
export const getMousePosition = () => jotaiStore.get(mouseAtom)
|
||||
|
|
@ -4,6 +4,8 @@ import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect"
|
|||
import { useStore } from "jotai"
|
||||
import type { FC } from "react"
|
||||
|
||||
import { mouseAtom } from "../atoms/mouse"
|
||||
|
||||
export const EventProvider: FC = () => {
|
||||
const store = useStore()
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
|
|
@ -36,5 +38,13 @@ export const EventProvider: FC = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
store.set(mouseAtom, { x: e.clientX, y: e.clientY })
|
||||
}
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
return () => window.removeEventListener("mousemove", handleMouseMove)
|
||||
}, [store])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const PopoverTrigger = PopoverPrimitive.Trigger
|
|||
|
||||
const PopoverClose = PopoverPrimitive.Close
|
||||
|
||||
const PopoverArrow = PopoverPrimitive.Arrow
|
||||
|
||||
const PopoverContent = ({
|
||||
ref,
|
||||
className,
|
||||
|
|
@ -37,4 +39,4 @@ const PopoverContent = ({
|
|||
)
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverClose, PopoverContent, PopoverTrigger }
|
||||
export { Popover, PopoverArrow, PopoverClose, PopoverContent, PopoverTrigger }
|
||||
|
|
|
|||
Loading…
Reference in New Issue