Просмотр исходного кода

fix: avatar not loaded fallback

Aaron Iker 3 месяцев назад
Родитель
Сommit
1455976689

+ 14 - 18
packages/app/src/components/dialog-edit-project.tsx

@@ -3,11 +3,12 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { TextField } from "@opencode-ai/ui/text-field"
 import { Icon } from "@opencode-ai/ui/icon"
+import { Avatar } from "@opencode-ai/ui/avatar"
 import { createMemo, createSignal, For, Show } from "solid-js"
 import { createStore } from "solid-js/store"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { type LocalProject, getAvatarColors } from "@/context/layout"
-import { Avatar } from "@opencode-ai/ui/avatar"
+import { ProjectIcon, isValidImageFile } from "@/components/project-icon"
 
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 
@@ -33,9 +34,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
   const [dragOver, setDragOver] = createSignal(false)
 
   function handleFileSelect(file: File) {
-    if (!file.type.startsWith("image/")) return
+    if (!isValidImageFile(file)) return
     const reader = new FileReader()
-    reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
+    reader.onload = (e) => {
+      setStore("iconUrl", e.target?.result as string)
+    }
     reader.readAsDataURL(file)
   }
 
@@ -98,7 +101,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
             <div class="flex gap-3 items-start">
               <div class="relative">
                 <div
-                  class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
+                  class="size-12 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
                   classList={{
                     "border-text-interactive-base bg-surface-info-base/20": dragOver(),
                     "border-border-base hover:border-border-strong": !dragOver(),
@@ -108,20 +111,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
                   onDragLeave={handleDragLeave}
                   onClick={() => document.getElementById("icon-upload")?.click()}
                 >
-                  <Show
-                    when={store.iconUrl}
-                    fallback={
-                      <div class="size-full flex items-center justify-center">
-                        <Avatar
-                          fallback={store.name || defaultName()}
-                          {...getAvatarColors(store.color)}
-                          class="size-full"
-                        />
-                      </div>
-                    }
-                  >
-                    <img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
-                  </Show>
+                  <ProjectIcon
+                    name={store.name || defaultName()}
+                    projectId={props.project.id}
+                    iconUrl={store.iconUrl}
+                    iconColor={store.color}
+                    class="size-full"
+                  />
                 </div>
                 <Show when={store.iconUrl}>
                   <button

+ 58 - 0
packages/app/src/components/project-icon.tsx

@@ -0,0 +1,58 @@
+import { createMemo, splitProps, type ComponentProps, type JSX } from "solid-js"
+import { Avatar } from "@opencode-ai/ui/avatar"
+import { getAvatarColors } from "@/context/layout"
+
+const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
+const OPENCODE_FAVICON_URL = "https://opencode.ai/favicon.svg"
+
+export interface ProjectIconProps extends Omit<ComponentProps<"div">, "children"> {
+  name: string
+  iconUrl?: string
+  iconColor?: string
+  projectId?: string
+  size?: "small" | "normal" | "large"
+}
+
+export const isValidImageUrl = (url: string | undefined): boolean => {
+  if (!url) return false
+  if (url.startsWith("data:image/x-icon")) return false
+  if (url.startsWith("data:image/vnd.microsoft.icon")) return false
+  return true
+}
+
+export const isValidImageFile = (file: File): boolean => {
+  if (!file.type.startsWith("image/")) return false
+  if (file.type === "image/x-icon" || file.type === "image/vnd.microsoft.icon") return false
+  return true
+}
+
+export const ProjectIcon = (props: ProjectIconProps) => {
+  const [local, rest] = splitProps(props, [
+    "name",
+    "iconUrl",
+    "iconColor",
+    "projectId",
+    "size",
+    "class",
+    "classList",
+    "style",
+  ])
+  const colors = createMemo(() => getAvatarColors(local.iconColor))
+  const validSrc = createMemo(() => {
+    if (local.projectId === OPENCODE_PROJECT_ID) return OPENCODE_FAVICON_URL
+    return isValidImageUrl(local.iconUrl) ? local.iconUrl : undefined
+  })
+
+  return (
+    <Avatar
+      fallback={local.name}
+      src={validSrc()}
+      size={local.size}
+      {...colors()}
+      class={local.class}
+      classList={local.classList}
+      style={local.style as JSX.CSSProperties}
+      {...rest}
+    />
+  )
+}

+ 1 - 1
packages/ui/src/components/avatar.css

@@ -22,7 +22,7 @@
 [data-component="avatar"][data-size="small"] {
   width: 1.25rem;
   height: 1.25rem;
-  font-size: 0.75rem;
+  font-size: 0.65rem;
   line-height: 1;
 }