Răsfoiți Sursa

feat(app): update settings in general settings

adamelmore 3 săptămâni în urmă
părinte
comite
2f5a238b51

+ 107 - 17
packages/app/src/components/settings-general.tsx

@@ -1,8 +1,12 @@
 import { Component, createMemo, type JSX } from "solid-js"
+import { createStore } from "solid-js/store"
+import { Button } from "@opencode-ai/ui/button"
 import { Select } from "@opencode-ai/ui/select"
 import { Switch } from "@opencode-ai/ui/switch"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
+import { showToast } from "@opencode-ai/ui/toast"
 import { useLanguage } from "@/context/language"
+import { usePlatform } from "@/context/platform"
 import { useSettings, monoFontFamily } from "@/context/settings"
 import { playSound, SOUND_OPTIONS } from "@/utils/sound"
 import { Link } from "./link"
@@ -29,8 +33,67 @@ const playDemoSound = (src: string) => {
 export const SettingsGeneral: Component = () => {
   const theme = useTheme()
   const language = useLanguage()
+  const platform = usePlatform()
   const settings = useSettings()
 
+  const [store, setStore] = createStore({
+    checking: false,
+  })
+
+  const check = () => {
+    if (!platform.checkUpdate) return
+    setStore("checking", true)
+
+    void platform
+      .checkUpdate()
+      .then((result) => {
+        if (!result.updateAvailable) {
+          showToast({
+            variant: "success",
+            icon: "circle-check",
+            title: language.t("settings.updates.toast.latest.title"),
+            description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
+          })
+          return
+        }
+
+        const actions =
+          platform.update && platform.restart
+            ? [
+                {
+                  label: language.t("toast.update.action.installRestart"),
+                  onClick: async () => {
+                    await platform.update!()
+                    await platform.restart!()
+                  },
+                },
+                {
+                  label: language.t("toast.update.action.notYet"),
+                  onClick: "dismiss" as const,
+                },
+              ]
+            : [
+                {
+                  label: language.t("toast.update.action.notYet"),
+                  onClick: "dismiss" as const,
+                },
+              ]
+
+        showToast({
+          persistent: true,
+          icon: "download",
+          title: language.t("toast.update.title"),
+          description: language.t("toast.update.description", { version: result.version ?? "" }),
+          actions,
+        })
+      })
+      .catch((err: unknown) => {
+        const message = err instanceof Error ? err.message : String(err)
+        showToast({ title: language.t("common.requestFailed"), description: message })
+      })
+      .finally(() => setStore("checking", false))
+  }
+
   const themeOptions = createMemo(() =>
     Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
   )
@@ -208,23 +271,6 @@ export const SettingsGeneral: Component = () => {
           </div>
         </div>
 
-        {/* Updates Section */}
-        <div class="flex flex-col gap-1">
-          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
-
-          <div class="bg-surface-raised-base px-4 rounded-lg">
-            <SettingsRow
-              title={language.t("settings.general.row.releaseNotes.title")}
-              description={language.t("settings.general.row.releaseNotes.description")}
-            >
-              <Switch
-                checked={settings.general.releaseNotes()}
-                onChange={(checked) => settings.general.setReleaseNotes(checked)}
-              />
-            </SettingsRow>
-          </div>
-        </div>
-
         {/* Sound effects Section */}
         <div class="flex flex-col gap-1">
           <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
@@ -303,6 +349,50 @@ export const SettingsGeneral: Component = () => {
             </SettingsRow>
           </div>
         </div>
+
+        {/* Updates Section */}
+        <div class="flex flex-col gap-1">
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
+
+          <div class="bg-surface-raised-base px-4 rounded-lg">
+            <SettingsRow
+              title={language.t("settings.updates.row.startup.title")}
+              description={language.t("settings.updates.row.startup.description")}
+            >
+              <Switch
+                checked={settings.updates.startup()}
+                disabled={!platform.checkUpdate}
+                onChange={(checked) => settings.updates.setStartup(checked)}
+              />
+            </SettingsRow>
+
+            <SettingsRow
+              title={language.t("settings.general.row.releaseNotes.title")}
+              description={language.t("settings.general.row.releaseNotes.description")}
+            >
+              <Switch
+                checked={settings.general.releaseNotes()}
+                onChange={(checked) => settings.general.setReleaseNotes(checked)}
+              />
+            </SettingsRow>
+
+            <SettingsRow
+              title={language.t("settings.updates.row.check.title")}
+              description={language.t("settings.updates.row.check.description")}
+            >
+              <Button
+                size="small"
+                variant="secondary"
+                disabled={store.checking || !platform.checkUpdate}
+                onClick={check}
+              >
+                {store.checking
+                  ? language.t("settings.updates.action.checking")
+                  : language.t("settings.updates.action.checkNow")}
+              </Button>
+            </SettingsRow>
+          </div>
+        </div>
       </div>
     </div>
   )

+ 12 - 0
packages/app/src/context/settings.tsx

@@ -20,6 +20,9 @@ export interface Settings {
     autoSave: boolean
     releaseNotes: boolean
   }
+  updates: {
+    startup: boolean
+  }
   appearance: {
     fontSize: number
     font: string
@@ -37,6 +40,9 @@ const defaultSettings: Settings = {
     autoSave: true,
     releaseNotes: true,
   },
+  updates: {
+    startup: true,
+  },
   appearance: {
     fontSize: 14,
     font: "ibm-plex-mono",
@@ -104,6 +110,12 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
           setStore("general", "releaseNotes", value)
         },
       },
+      updates: {
+        startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup),
+        setStartup(value: boolean) {
+          setStore("updates", "startup", value)
+        },
+      },
       appearance: {
         fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
         setFontSize(value: number) {

+ 9 - 0
packages/app/src/i18n/en.ts

@@ -540,6 +540,15 @@ export const dict = {
 
   "settings.general.row.releaseNotes.title": "Release notes",
   "settings.general.row.releaseNotes.description": "Show What's New popups after updates",
+
+  "settings.updates.row.startup.title": "Check for updates on startup",
+  "settings.updates.row.startup.description": "Automatically check for updates when OpenCode launches",
+  "settings.updates.row.check.title": "Check for updates",
+  "settings.updates.row.check.description": "Manually check for updates and install if available",
+  "settings.updates.action.checkNow": "Check now",
+  "settings.updates.action.checking": "Checking...",
+  "settings.updates.toast.latest.title": "You're up to date",
+  "settings.updates.toast.latest.description": "You're running the latest version of OpenCode.",
   "font.option.ibmPlexMono": "IBM Plex Mono",
   "font.option.cascadiaCode": "Cascadia Code",
   "font.option.firaCode": "Fira Code",

+ 20 - 3
packages/app/src/pages/layout.tsx

@@ -332,6 +332,7 @@ export default function Layout(props: ParentProps) {
     if (!platform.checkUpdate || !platform.update || !platform.restart) return
 
     let toastId: number | undefined
+    let interval: ReturnType<typeof setInterval> | undefined
 
     async function pollUpdate() {
       const { updateAvailable, version } = await platform.checkUpdate!()
@@ -358,9 +359,25 @@ export default function Layout(props: ParentProps) {
       }
     }
 
-    pollUpdate()
-    const interval = setInterval(pollUpdate, 10 * 60 * 1000)
-    onCleanup(() => clearInterval(interval))
+    createEffect(() => {
+      if (!settings.ready()) return
+
+      if (!settings.updates.startup()) {
+        if (interval === undefined) return
+        clearInterval(interval)
+        interval = undefined
+        return
+      }
+
+      if (interval !== undefined) return
+      void pollUpdate()
+      interval = setInterval(pollUpdate, 10 * 60 * 1000)
+    })
+
+    onCleanup(() => {
+      if (interval === undefined) return
+      clearInterval(interval)
+    })
   })
 
   onMount(() => {