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<{