diff --git a/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx b/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx index 4ff362ea4..6eaaaf54c 100644 --- a/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx +++ b/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx @@ -14,14 +14,16 @@ import type { PreviewImageProps } from "../image/PreviewImage" import { PreviewImage } from "../image/PreviewImage" export const MediaCarousel = ({ + entryId, media, onPreview, aspectRatio, Accessory, AccessoryProps, }: { + entryId: string media: MediaModel[] - onPreview: () => void + onPreview?: () => void aspectRatio: number } & Pick) => { const [containerWidth, setContainerWidth] = useState(0) @@ -53,7 +55,7 @@ export const MediaCarousel = ({ if (m.type === "photo") { return ( - + = ({ imageUrl, children }) => { - if (!imageUrl) { +type ImageContextMenuProps = PropsWithChildren<{ + imageUrl?: string + entryId?: string +}> + +export const ImageContextMenu = ({ imageUrl, entryId, children }: ImageContextMenuProps) => { + const entry = useEntry(entryId!) + const feedId = entry?.feedId + const view = useSelectedView() + const isEntryStarred = useIsEntryStarred(entryId!) + + if (!imageUrl || !entry) { return children } @@ -51,6 +61,32 @@ export const ImageContextMenu: FC< {children} + {entryId && feedId && view !== undefined && ( + { + if (isEntryStarred) { + collectionSyncService.unstarEntry(entryId) + toast.info("Unstarred") + } else { + collectionSyncService.starEntry({ + feedId, + entryId, + view, + }) + toast.info("Starred") + } + }} + > + + {isEntryStarred ? "Unstar" : "Star"} + + )} + { diff --git a/apps/mobile/src/modules/context-menu/entry.tsx b/apps/mobile/src/modules/context-menu/entry.tsx index 92f9b3aa9..66cd0ae30 100644 --- a/apps/mobile/src/modules/context-menu/entry.tsx +++ b/apps/mobile/src/modules/context-menu/entry.tsx @@ -3,12 +3,12 @@ import { router } from "expo-router" import type { PropsWithChildren } from "react" import { useCallback } from "react" import { Share, Text, View } from "react-native" -import * as ContextMenu from "zeego/context-menu" import { EntryContentWebView, setWebViewEntry, } from "@/src/components/native/webview/EntryContentWebView" +import { ContextMenu } from "@/src/components/ui/context-menu" import { openLink } from "@/src/lib/native" import { toast } from "@/src/lib/toast" import { useSelectedView } from "@/src/modules/screen/atoms" @@ -19,6 +19,7 @@ import { unreadSyncService } from "@/src/store/unread/store" export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: string }>) => { const entry = useEntry(id) + const feedId = entry?.feedId const view = useSelectedView() const isEntryStarred = useIsEntryStarred(id) @@ -62,37 +63,31 @@ export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: s /> - { - if (isEntryStarred) { - collectionSyncService.unstarEntry(id) - toast.info("Unstarred") - } else { - if (!entry.feedId) { - toast.error("Feed not found") - return + {feedId && view !== undefined && ( + { + if (isEntryStarred) { + collectionSyncService.unstarEntry(id) + toast.info("Unstarred") + } else { + collectionSyncService.starEntry({ + feedId, + entryId: id, + view, + }) + toast.info("Starred") } - if (!view) { - toast.error("View not found") - return - } - collectionSyncService.starEntry({ - feedId: entry.feedId, - entryId: id, - view, - }) - toast.info("Starred") - } - }} - > - - {isEntryStarred ? "Unstar" : "Star"} - + > + + {isEntryStarred ? "Unstar" : "Star"} + + )} {entry.url && ( + +export const VideoContextMenu = ({ entryId, children }: VideoContextMenuProps) => { + const entry = useEntry(entryId) + const feedId = entry?.feedId + const view = useSelectedView() + const isEntryStarred = useIsEntryStarred(entryId) + + if (!entry) { + return children + } + + return ( + + {children} + + + {feedId && view !== undefined && ( + { + if (isEntryStarred) { + collectionSyncService.unstarEntry(entryId) + toast.info("Unstarred") + } else { + collectionSyncService.starEntry({ + feedId, + entryId, + view, + }) + toast.info("Starred") + } + }} + > + + {isEntryStarred ? "Unstar" : "Star"} + + )} + + { + if (!entry.url) return + openLink(entry.url) + }} + > + + Open Link + + + { + if (!entry.url) return + Clipboard.setString(entry.url) + toast.success("Link copied to clipboard") + }} + > + + Copy Link + + { + if (!entry.url) return + await Share.share({ + message: [entry.title, entry.url].filter(Boolean).join("\n"), + url: entry.url, + title: entry.title || "Shared Video", + }) + return + }} + > + + Share + + + + ) +} diff --git a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx index 97746b2aa..04dd5bb25 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx @@ -1,5 +1,7 @@ import { FeedViewType } from "@follow/constants" +import { transformVideoUrl } from "@follow/utils" import { uniqBy } from "es-toolkit/compat" +import { Image } from "expo-image" import { LinearGradient } from "expo-linear-gradient" import { useEffect, useMemo } from "react" import { ScrollView, Text, View } from "react-native" @@ -7,20 +9,17 @@ import Animated, { useSharedValue, withTiming } from "react-native-reanimated" import { useSafeAreaInsets } from "react-native-safe-area-context" import { useUISettingKey } from "@/src/atoms/settings/ui" -import { - EntryContentWebView, - setWebViewEntry, -} from "@/src/components/native/webview/EntryContentWebView" +import { EntryContentWebView } from "@/src/components/native/webview/EntryContentWebView" import { MediaCarousel } from "@/src/components/ui/carousel/MediaCarousel" import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" -import { PreviewImage } from "@/src/components/ui/image/PreviewImage" import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable" import type { MediaModel } from "@/src/database/schemas/types" import { openLink } from "@/src/lib/native" import { useEntry } from "@/src/store/entry/hooks" import { useFeed } from "@/src/store/feed/hooks" +import { VideoContextMenu } from "../../context-menu/video" import { useEntryListContextView } from "../EntryListContext" export function EntryGridItem({ id }: { id: string }) { @@ -45,32 +44,27 @@ export function EntryGridItem({ id }: { id: string }) { ) } - const WrapperComponent = view === FeedViewType.Videos ? ItemPressable : View + if (view === FeedViewType.Pictures) { + return ( + + + + ) + } return ( - { - if (!item.url) { - return - } - if (view === FeedViewType.Videos) { - openLink(item.url) - } - }} - > - { - if (item) { - setWebViewEntry(item) - } + + { + if (!item.url) return + const formattedUrl = transformVideoUrl({ url: item.url }) || item.url + openLink(formattedUrl) }} - /> - + > + + + ) } @@ -84,7 +78,7 @@ const MediaItems = ({ media: MediaModel[] view: FeedViewType entryId: string - onPreview: () => void + onPreview?: () => void title: string }) => { const firstMedia = media[0] @@ -108,7 +102,15 @@ const MediaItems = ({ {mediaUrl && ( - + )} @@ -120,6 +122,7 @@ const MediaItems = ({ return ( { +}): string | null => { if (url?.match(/\/\/www.bilibili.com\/video\/BV\w+/)) { const player = isIframe ? "https://player.bilibili.com/player.html" @@ -39,7 +39,7 @@ export const transformVideoUrl = ({ } if (attachments) { - return attachments.find((attachment) => attachment.mime_type === "text/html")?.url + return attachments.find((attachment) => attachment.mime_type === "text/html")?.url ?? null } return null }