fix(app): no more favicons

This commit is contained in:
Adam
2026-01-19 14:59:41 -06:00
parent cac35bc52d
commit a4d1824412
6 changed files with 51 additions and 24 deletions

View File

@@ -22,7 +22,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
const [store, setStore] = createStore({ const [store, setStore] = createStore({
name: defaultName(), name: defaultName(),
color: props.project.icon?.color || "pink", color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.url || "", iconUrl: props.project.icon?.override || "",
saving: false, saving: false,
}) })
@@ -74,7 +74,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
await globalSDK.client.project.update({ await globalSDK.client.project.update({
projectID: props.project.id, projectID: props.project.id,
name, name,
icon: { color: store.color, url: store.iconUrl }, icon: { color: store.color, override: store.iconUrl },
}) })
setStore("saving", false) setStore("saving", false)
dialog.close() dialog.close()

View File

@@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}) })
}) })
const usedColors = new Set<AvatarColorKey>() const [colors, setColors] = createStore<Record<string, AvatarColorKey>>({})
function pickAvailableColor(): AvatarColorKey { function pickAvailableColor(used: Set<string>): AvatarColorKey {
const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c)) const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c))
if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
return available[Math.floor(Math.random() * available.length)] return available[Math.floor(Math.random() * available.length)]
} }
@@ -222,24 +222,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const metadata = projectID const metadata = projectID
? globalSync.data.project.find((x) => x.id === projectID) ? globalSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree) : globalSync.data.project.find((x) => x.worktree === project.worktree)
return [ return {
{ ...(metadata ?? {}),
...(metadata ?? {}), ...project,
...project, icon: {
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color }, url: metadata?.icon?.url,
override: metadata?.icon?.override,
color: metadata?.icon?.color,
}, },
]
}
function colorize(project: LocalProject) {
if (project.icon?.color) return project
const color = pickAvailableColor()
usedColors.add(color)
project.icon = { ...project.icon, color }
if (project.id) {
globalSdk.client.project.update({ projectID: project.id, icon: { color } })
} }
return project
} }
const roots = createMemo(() => { const roots = createMemo(() => {
@@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}) })
}) })
const enriched = createMemo(() => server.projects.list().flatMap(enrich)) const enriched = createMemo(() => server.projects.list().map(enrich))
const list = createMemo(() => enriched().flatMap(colorize)) const list = createMemo(() => {
const projects = enriched()
return projects.map((project) => {
const color = project.icon?.color ?? colors[project.worktree]
if (!color) return project
const icon = project.icon ? { ...project.icon, color } : { color }
return { ...project, icon }
})
})
createEffect(() => {
const projects = enriched()
if (projects.length === 0) return
const used = new Set<string>()
for (const project of projects) {
const color = project.icon?.color ?? colors[project.worktree]
if (color) used.add(color)
}
for (const project of projects) {
if (project.icon?.color) continue
if (colors[project.worktree]) continue
const color = pickAvailableColor(used)
used.add(color)
setColors(project.worktree, color)
if (!project.id) continue
void globalSdk.client.project.update({ projectID: project.id, icon: { color } })
}
})
onMount(() => { onMount(() => {
Promise.all( Promise.all(

View File

@@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
<div class="size-full rounded overflow-clip"> <div class="size-full rounded overflow-clip">
<Avatar <Avatar
fallback={name()} fallback={name()}
src={props.project.id === opencode ? "https://opencode.ai/favicon-v2.svg" : props.project.icon?.url} src={props.project.id === opencode ? "https://opencode.ai/favicon-v2.svg" : props.project.icon?.override}
{...getAvatarColors(props.project.icon?.color)} {...getAvatarColors(props.project.icon?.color)}
class="size-full rounded" class="size-full rounded"
style={ style={

View File

@@ -25,6 +25,7 @@ export namespace Project {
icon: z icon: z
.object({ .object({
url: z.string().optional(), url: z.string().optional(),
override: z.string().optional(),
color: z.string().optional(), color: z.string().optional(),
}) })
.optional(), .optional(),
@@ -190,6 +191,7 @@ export namespace Project {
if (!existing.sandboxes) existing.sandboxes = [] if (!existing.sandboxes) existing.sandboxes = []
if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing) if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
const result: Info = { const result: Info = {
...existing, ...existing,
worktree, worktree,
@@ -213,6 +215,7 @@ export namespace Project {
export async function discover(input: Info) { export async function discover(input: Info) {
if (input.vcs !== "git") return if (input.vcs !== "git") return
if (input.icon?.override) return
if (input.icon?.url) return if (input.icon?.url) return
const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}") const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}")
const matches = await Array.fromAsync( const matches = await Array.fromAsync(
@@ -293,6 +296,7 @@ export namespace Project {
...draft.icon, ...draft.icon,
} }
if (input.icon.url !== undefined) draft.icon.url = input.icon.url if (input.icon.url !== undefined) draft.icon.url = input.icon.url
if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined
if (input.icon.color !== undefined) draft.icon.color = input.icon.color if (input.icon.color !== undefined) draft.icon.color = input.icon.color
} }
draft.time.updated = Date.now() draft.time.updated = Date.now()

View File

@@ -302,6 +302,7 @@ export class Project extends HeyApiClient {
name?: string name?: string
icon?: { icon?: {
url?: string url?: string
override?: string
color?: string color?: string
} }
}, },

View File

@@ -25,6 +25,7 @@ export type Project = {
name?: string name?: string
icon?: { icon?: {
url?: string url?: string
override?: string
color?: string color?: string
} }
time: { time: {
@@ -2229,6 +2230,7 @@ export type ProjectUpdateData = {
name?: string name?: string
icon?: { icon?: {
url?: string url?: string
override?: string
color?: string color?: string
} }
} }