diff --git a/icons/mgc/github_2_cute_fi.svg b/icons/mgc/github_2_cute_fi.svg new file mode 100644 index 000000000..d2367045b --- /dev/null +++ b/icons/mgc/github_2_cute_fi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/mgc/youtube_cute_fi.svg b/icons/mgc/youtube_cute_fi.svg new file mode 100644 index 000000000..7785092ae --- /dev/null +++ b/icons/mgc/youtube_cute_fi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/components/site-icon.tsx b/src/renderer/src/components/site-icon.tsx index 202476583..6f534e41d 100644 --- a/src/renderer/src/components/site-icon.tsx +++ b/src/renderer/src/components/site-icon.tsx @@ -1,6 +1,8 @@ import { cn } from "@renderer/lib/utils" import { parse } from "tldts" +import { PlatformIcon } from "./ui/platform-icon" + export function SiteIcon({ url, className, @@ -19,27 +21,36 @@ export function SiteIcon({ try { host = new URL(url).host const pureDomain = parse(host).domainWithoutSuffix - fallback = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain?.slice(0, 2).toUpperCase()}` + fallback = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain + ?.slice(0, 2) + .toUpperCase()}` src = `https://unavatar.follow.is/${host}?fallback=${fallback}` } catch { const pureDomain = parse(url).domainWithoutSuffix - src = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain?.slice(0, 2).toUpperCase()}` + src = `https://avatar.vercel.sh/${pureDomain}.svg?text=${pureDomain + ?.slice(0, 2) + .toUpperCase()}` } return ( - { - if (fallback && e.currentTarget.src !== fallback) { - e.currentTarget.src = fallback - } else { - // Empty svg - e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3C/svg%3E" - } - }} + + className={cn("mr-2 size-5 shrink-0 rounded-sm", className)} + > + { + if (fallback && e.currentTarget.src !== fallback) { + e.currentTarget.src = fallback + } else { + // Empty svg + e.currentTarget.src = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3C/svg%3E" + } + }} + /> + ) } diff --git a/src/renderer/src/components/ui/platform-icon/index.tsx b/src/renderer/src/components/ui/platform-icon/index.tsx new file mode 100644 index 000000000..5aa1150bf --- /dev/null +++ b/src/renderer/src/components/ui/platform-icon/index.tsx @@ -0,0 +1,39 @@ +import { Slot } from "@radix-ui/react-slot" +import { cn } from "@renderer/lib/utils" +import type { FC } from "react" + +import { getSupportedPlatformIconName } from "./utils" + +export const PlatformIcon: FC<{ + url: string + children: React.JSX.Element + className?: string + style?: React.CSSProperties +}> = ({ className, url, children, style, ...rest }) => { + const iconName = getSupportedPlatformIconName(url) + + if (!iconName) { + return ( + + {children} + + ) + } + + return ( + + ) +} + +const IconMap = { + github: tw`i-mgc-github-2-cute-fi text-black dark:text-white`, + twitter: tw`i-mgc-twitter-cute-fi text-[#55acee]`, + x: tw`i-mgc-social-x-cute-li`, + youtube: tw`i-mgc-youtube-cute-fi text-[#ff0000]`, +} diff --git a/src/renderer/src/components/ui/platform-icon/utils.tsx b/src/renderer/src/components/ui/platform-icon/utils.tsx new file mode 100644 index 000000000..c5a7ccefc --- /dev/null +++ b/src/renderer/src/components/ui/platform-icon/utils.tsx @@ -0,0 +1,38 @@ +import { + isGithubUrl, + isTwitterUrl, + isXUrl, + isYoutubeUrl, +} from "@renderer/lib/link-parser" + +export const getSupportedPlatformIconName = (url: string) => { + const safeUrl = parseSafeUrl(url) + + if (!safeUrl) { + return false + } + switch (true) { + case isGithubUrl(safeUrl): { + return "github" + } + case isTwitterUrl(safeUrl): { + return "twitter" + } + case isXUrl(safeUrl): { + return "x" + } + case isYoutubeUrl(safeUrl): { + return "youtube" + } + } + + return false +} + +const parseSafeUrl = (url: string) => { + try { + return new URL(url) + } catch { + return null + } +} diff --git a/src/renderer/src/lib/link-parser.ts b/src/renderer/src/lib/link-parser.ts new file mode 100644 index 000000000..5770be98b --- /dev/null +++ b/src/renderer/src/lib/link-parser.ts @@ -0,0 +1,153 @@ +export const getTweetId = (url: URL) => url.pathname.split("/").pop()! + +const GITHUB_HOST = "github.com" + +export const isGithubRepoUrl = (url: URL) => + url.hostname === GITHUB_HOST && + url.pathname.startsWith("/") && + url.pathname.split("/").length === 3 + +export const isGithubPrUrl = (url: URL) => + url.hostname === GITHUB_HOST && url.pathname.includes("/pull/") + +export const isYoutubeUrl = (url: URL) => + url.hostname === "www.youtube.com" && url.pathname.startsWith("/watch") + +export const isGistUrl = (url: URL) => url.hostname === "gist.github.com" + +export const isGithubCommitUrl = (url: URL) => { + const [_, , , type] = url.pathname.split("/") + return url.hostname === GITHUB_HOST && type === "commit" +} + +export const isGithubProfileUrl = (url: URL) => + url.hostname === GITHUB_HOST && url.pathname.split("/").length === 2 + +export const isGithubFilePreviewUrl = (url: URL) => { + // https://github.com/Innei/sprightly/blob/14234594f44956e6f56f1f92952ce82db37ef4df/src/socket/handler.ts + const [_, , , type] = url.pathname.split("/") + return url.hostname === GITHUB_HOST && type === "blob" +} + +export const isTweetUrl = (url: URL) => + isTwitterUrl(url) && url.pathname.startsWith("/") + +export const isTwitterProfileUrl = (url: URL) => + isTwitterUrl(url) && url.pathname.split("/").length === 2 + +export const isGithubUrl = (url: URL) => url.hostname === GITHUB_HOST + +export const isTwitterUrl = (url: URL) => url.hostname === "twitter.com" +export const isXUrl = (url: URL) => url.hostname === "x.com" +export const isXOrTwitterUrl = (url: URL) => isXUrl(url) || isTwitterUrl(url) + +export const isTelegramUrl = (url: URL) => url.hostname === "t.me" + +export const isCodesandboxUrl = (url: URL) => + // https://codesandbox.io/s/framer-motion-layoutroot-prop-forked-p39g96 + url.hostname === "codesandbox.io" && url.pathname.split("/").length === 3 + +export const isBilibiliUrl = (url: URL) => + url.hostname.includes("bilibili.com") + +export const isBilibiliVideoUrl = (url: URL) => + isBilibiliUrl(url) && url.pathname.startsWith("/video/BV") + +export const isZhihuUrl = (url: URL) => url.hostname === "www.zhihu.com" + +export const isZhihuProfileUrl = (url: URL) => + isZhihuUrl(url) && url.pathname.startsWith("/people/") + +export const isWikipediaUrl = (url: URL) => + url.hostname.includes("wikipedia.org") + +export const isTMDBUrl = (url: URL) => url.hostname.includes("themoviedb.org") + +export const isNpmUrl = (url: URL) => url.hostname.includes("npmjs.com") + +export const isMozillaUrl = (url: URL) => url.hostname.includes("mozilla.org") + +export const parseSelfArticleUrl = (url: URL) => { + const [_, type, ...rest] = url.pathname.split("/") + switch (type) { + case "posts": { + return { + type, + slug: rest.join("/"), + } + } + case "notes": { + return { + type, + nid: +rest[0], + } + } + } +} + +export const parseGithubRepoUrl = (url: URL) => { + const [_, owner, repo] = url.pathname.split("/") + return { + owner, + repo, + } +} + +export const parseGithubGistUrl = (url: URL) => { + const [_, owner, id] = url.pathname.split("/") + return { + owner, + id, + } +} + +export const parseGithubTypedUrl = (url: URL) => { + // https://github.com/Innei/sprightly/blob/14234594f44956e6f56f1f92952ce82db37ef4df/src/socket/handler.ts + // https://github.com/mx-space/core/commit/e1b4d881cf18e1cb66294d2620eada35937d9a12 + const split = url.pathname.split("/") + const [_, owner, repo, type, id] = split + + const afterTypeString = split.slice(4).join("/") + return { + owner, + repo, + type, + id, + afterTypeString, + } +} + +export const parseZhihuProfileUrl = (url: URL) => { + const [_, type, id] = url.pathname.split("/") + return { + type, + id, + } +} + +export const parseGithubPrUrl = (url: URL) => { + const [_, owner, repo, type, pr] = url.pathname.split("/") + return { + owner, + repo, + type, + pr, + } +} + +export const parseTMDBUrl = (url: URL) => { + const [_, type, id] = url.pathname.split("/") + return { + type, + id, + } +} + +export const parseBilibiliVideoUrl = (url: URL) => { + // https://www.bilibili.com/video/BV1tj42197hU + const [_, type, id] = url.pathname.split("/") + return { + type, + id, + } +} diff --git a/src/renderer/src/modules/entry-column/grid-item-template.tsx b/src/renderer/src/modules/entry-column/grid-item-template.tsx index d75ca2773..5beb441cb 100644 --- a/src/renderer/src/modules/entry-column/grid-item-template.tsx +++ b/src/renderer/src/modules/entry-column/grid-item-template.tsx @@ -30,7 +30,7 @@ export function GridItem({ {children}
{virtuosoOptions.totalCount === 0 ? ( entries.isLoading ? @@ -182,7 +182,7 @@ export function EntryColumn() { ) : view && views[view].gridMode ? ( diff --git a/src/renderer/src/modules/profile/user-profile-modal.tsx b/src/renderer/src/modules/profile/user-profile-modal.tsx index 827e7d2fd..da9018381 100644 --- a/src/renderer/src/modules/profile/user-profile-modal.tsx +++ b/src/renderer/src/modules/profile/user-profile-modal.tsx @@ -156,6 +156,7 @@ export const UserProfileModalContent: FC<{