Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 119 additions & 13 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -1543,18 +1545,19 @@ func (a *App) SetDeviceTimezone(timezone string, deviceType string) {
}

type PackageInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
UpstreamAuthor string `json:"upstreamAuthor"`
Categories []string `json:"categories"`
URL string `json:"url"`
License string `json:"license"`
Devices []string `json:"devices"`
Depends []string `json:"depends"`
Conflicts []string `json:"conflicts"`
OSMin *string `json:"osMin"`
OSMax *string `json:"osMax"`
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
UpstreamAuthor string `json:"upstreamAuthor"`
Categories []string `json:"categories"`
URL string `json:"url"`
License string `json:"license"`
Devices []string `json:"devices"`
Depends []string `json:"depends"`
Conflicts []string `json:"conflicts"`
OSMin *string `json:"osMin"`
OSMax *string `json:"osMax"`
OSConstraints []vellum.OSConstraint `json:"osConstraints"`
}

var hiddenPackages = map[string]bool{
Expand Down Expand Up @@ -1606,6 +1609,7 @@ func (a *App) GetPackages(deviceType, firmware, arch string) []PackageInfo {
Conflicts: pkg.Conflicts,
OSMin: pkg.OSMin,
OSMax: pkg.OSMax,
OSConstraints: pkg.OSConstraints,
})
}

Expand Down Expand Up @@ -2583,6 +2587,103 @@ func (a *App) GetAppVersion() string {
return version
}

type UpdateCheckResult struct {
UpdateAvailable bool `json:"updateAvailable"`
LatestVersion string `json:"latestVersion"`
CurrentVersion string `json:"currentVersion"`
ReleaseURL string `json:"releaseURL"`
Error string `json:"error,omitempty"`
}

func (a *App) CheckForAppUpdate() UpdateCheckResult {
result := UpdateCheckResult{
CurrentVersion: version,
}

if version == "dev" {
return result
}

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.github.com/repos/rmitchellscott/remanager/releases/latest")
if err != nil {
debug.Printf("[DEBUG] CheckForAppUpdate: request failed: %v\n", err)
result.Error = "network_error"
return result
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
debug.Printf("[DEBUG] CheckForAppUpdate: HTTP %d\n", resp.StatusCode)
result.Error = "api_error"
return result
}

var release struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
}

body, err := io.ReadAll(resp.Body)
if err != nil {
result.Error = "read_error"
return result
}

if err := json.Unmarshal(body, &release); err != nil {
result.Error = "parse_error"
return result
}

result.LatestVersion = release.TagName
result.ReleaseURL = release.HTMLURL
result.UpdateAvailable = isNewerVersion(version, release.TagName)

debug.Printf("[DEBUG] CheckForAppUpdate: current=%s, latest=%s, updateAvailable=%v\n",
version, release.TagName, result.UpdateAvailable)

return result
}

func isNewerVersion(current, latest string) bool {
current = strings.TrimPrefix(current, "v")
latest = strings.TrimPrefix(latest, "v")

currentParts := strings.Split(current, ".")
latestParts := strings.Split(latest, ".")

for len(currentParts) < 3 {
currentParts = append(currentParts, "0")
}
for len(latestParts) < 3 {
latestParts = append(latestParts, "0")
}

for i := 0; i < 3; i++ {
currNum, err1 := strconv.Atoi(currentParts[i])
latestNum, err2 := strconv.Atoi(latestParts[i])

if err1 != nil || err2 != nil {
if latestParts[i] > currentParts[i] {
return true
}
if latestParts[i] < currentParts[i] {
return false
}
continue
}

if latestNum > currNum {
return true
}
if latestNum < currNum {
return false
}
}

return false
}

type SettingsInfo struct {
TabVisibility map[string]bool `json:"tabVisibility"`
ProxyMode bool `json:"proxyMode"`
Expand All @@ -2591,6 +2692,7 @@ type SettingsInfo struct {
Theme string `json:"theme"`
TerminalTheme string `json:"terminalTheme"`
EditorTheme string `json:"editorTheme"`
CheckForUpdates bool `json:"checkForUpdates"`
}

func (a *App) GetSettings() SettingsInfo {
Expand All @@ -2604,6 +2706,7 @@ func (a *App) GetSettings() SettingsInfo {
Theme: "system",
TerminalTheme: "match",
EditorTheme: "match",
CheckForUpdates: true,
}
}
settings, err := a.settingsStore.Load()
Expand All @@ -2617,6 +2720,7 @@ func (a *App) GetSettings() SettingsInfo {
Theme: "system",
TerminalTheme: "match",
EditorTheme: "match",
CheckForUpdates: true,
}
}
debug.Printf("[DEBUG] GetSettings: loaded PreventSleep=%v\n", settings.PreventSleep)
Expand All @@ -2628,10 +2732,11 @@ func (a *App) GetSettings() SettingsInfo {
Theme: settings.Theme,
TerminalTheme: settings.TerminalTheme,
EditorTheme: settings.EditorTheme,
CheckForUpdates: settings.CheckForUpdates,
}
}

func (a *App) SaveSettings(tabVisibility map[string]bool, proxyMode bool, suppressSystemFileWarnings bool, preventSleep bool, theme string, terminalTheme string, editorTheme string) error {
func (a *App) SaveSettings(tabVisibility map[string]bool, proxyMode bool, suppressSystemFileWarnings bool, preventSleep bool, theme string, terminalTheme string, editorTheme string, checkForUpdates bool) error {
debug.Printf("[DEBUG] SaveSettings: preventSleep=%v, isConnected=%v\n", preventSleep, a.IsConnected())
if a.settingsStore == nil {
return fmt.Errorf("settings store not initialized")
Expand All @@ -2644,6 +2749,7 @@ func (a *App) SaveSettings(tabVisibility map[string]bool, proxyMode bool, suppre
Theme: theme,
TerminalTheme: terminalTheme,
EditorTheme: editorTheme,
CheckForUpdates: checkForUpdates,
}

if preventSleep && a.IsConnected() {
Expand Down
50 changes: 45 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface PackageInfo {
conflicts: string[]
osMin: string | null
osMax: string | null
osConstraints: { version: string; operator: '>=' | '<' | '>' | '<=' | '=' }[] | null
}

interface MaintenanceCommandInfo {
Expand Down Expand Up @@ -196,8 +197,9 @@ declare global {
RespondToDialog(confirmed: boolean): Promise<void>
CancelInstallation(): Promise<void>
GetAppVersion(): Promise<string>
GetSettings(): Promise<{ tabVisibility: Record<string, boolean>; proxyMode: boolean; suppressSystemFileWarnings: boolean; preventSleep: boolean; theme: string; terminalTheme: string; editorTheme: string }>
SaveSettings(tabVisibility: Record<string, boolean>, proxyMode: boolean, suppressSystemFileWarnings: boolean, preventSleep: boolean, theme: string, terminalTheme: string, editorTheme: string): Promise<void>
CheckForAppUpdate(): Promise<{ updateAvailable: boolean; latestVersion: string; currentVersion: string; releaseURL: string; error?: string }>
GetSettings(): Promise<{ tabVisibility: Record<string, boolean>; proxyMode: boolean; suppressSystemFileWarnings: boolean; preventSleep: boolean; theme: string; terminalTheme: string; editorTheme: string; checkForUpdates: boolean }>
SaveSettings(tabVisibility: Record<string, boolean>, proxyMode: boolean, suppressSystemFileWarnings: boolean, preventSleep: boolean, theme: string, terminalTheme: string, editorTheme: string, checkForUpdates: boolean): Promise<void>
GetSystemColorScheme(): Promise<string>
UninstallVellum(removeAllPackages: boolean): Promise<void>
CleanupBrokenVellum(): Promise<void>
Expand Down Expand Up @@ -317,6 +319,7 @@ export default function App() {
const [theme, setTheme] = useState('system')
const [terminalTheme, setTerminalTheme] = useState('match')
const [editorTheme, setEditorTheme] = useState('match')
const [checkForUpdates, setCheckForUpdates] = useState(true)
const [systemPrefersDark, setSystemPrefersDark] = useState(() => window.matchMedia('(prefers-color-scheme: dark)').matches)
const [dnsErrorShown, setDnsErrorShown] = useState(false)
const [showDnsErrorModal, setShowDnsErrorModal] = useState(false)
Expand Down Expand Up @@ -540,6 +543,25 @@ export default function App() {
localStorage.setItem('theme', loadedTheme)
setTerminalTheme(settings?.terminalTheme || 'match')
setEditorTheme(settings?.editorTheme || 'match')
const shouldCheckUpdates = settings?.checkForUpdates ?? true
setCheckForUpdates(shouldCheckUpdates)

if (shouldCheckUpdates) {
window.go.main.App.CheckForAppUpdate().then((result) => {
if (result.updateAvailable && result.latestVersion) {
toast.info(`Update available: ${result.latestVersion}`, {
description: `You're running ${result.currentVersion}`,
action: {
label: 'View Release',
onClick: () => window.runtime.BrowserOpenURL(result.releaseURL)
},
duration: 6000,
})
}
}).catch((err) => {
debugLog('[DEBUG] Update check failed:', err)
})
}
} catch (err) {
debugLog('Could not load initial data:', err)
setSelectedKey('__other__')
Expand Down Expand Up @@ -1410,7 +1432,8 @@ export default function App() {
setShowFilesystemRestoreError(false)
}

const handleSaveSettings = async (newTabVisibility: Record<string, boolean>, newProxyMode: boolean, newSuppressSystemFileWarnings: boolean, newPreventSleep: boolean, newTheme: string, newTerminalTheme: string, newEditorTheme: string) => {
const handleSaveSettings = async (newTabVisibility: Record<string, boolean>, newProxyMode: boolean, newSuppressSystemFileWarnings: boolean, newPreventSleep: boolean, newTheme: string, newTerminalTheme: string, newEditorTheme: string, newCheckForUpdates: boolean) => {
const wasCheckForUpdatesOff = !checkForUpdates
setTabVisibility(newTabVisibility)
setProxyMode(newProxyMode)
setSuppressSystemFileWarnings(newSuppressSystemFileWarnings)
Expand All @@ -1420,12 +1443,28 @@ export default function App() {
await applyThemeWithPortal(newTheme)
setTerminalTheme(newTerminalTheme)
setEditorTheme(newEditorTheme)
await window.go.main.App.SaveSettings(newTabVisibility, newProxyMode, newSuppressSystemFileWarnings, newPreventSleep, newTheme, newTerminalTheme, newEditorTheme)
setCheckForUpdates(newCheckForUpdates)
await window.go.main.App.SaveSettings(newTabVisibility, newProxyMode, newSuppressSystemFileWarnings, newPreventSleep, newTheme, newTerminalTheme, newEditorTheme, newCheckForUpdates)

if (wasCheckForUpdatesOff && newCheckForUpdates) {
window.go.main.App.CheckForAppUpdate().then((result) => {
if (result.updateAvailable && result.latestVersion) {
toast.info(`Update available: ${result.latestVersion}`, {
description: `You're running ${result.currentVersion}`,
action: {
label: 'View Release',
onClick: () => window.runtime.BrowserOpenURL(result.releaseURL)
},
duration: 6000,
})
}
}).catch(() => {})
}
}

const handleEnableProxyModeFromModal = async () => {
setProxyMode(true)
await window.go.main.App.SaveSettings(tabVisibility, true, suppressSystemFileWarnings, preventSleep, theme, terminalTheme, editorTheme)
await window.go.main.App.SaveSettings(tabVisibility, true, suppressSystemFileWarnings, preventSleep, theme, terminalTheme, editorTheme, checkForUpdates)
setShowDnsErrorModal(false)
}

Expand Down Expand Up @@ -3282,6 +3321,7 @@ export default function App() {
theme={theme}
terminalTheme={terminalTheme}
editorTheme={editorTheme}
checkForUpdates={checkForUpdates}
onSaveSettings={handleSaveSettings}
onUninstallVellum={handleUninstallVellum}
uninstalling={vellumUninstalling}
Expand Down
34 changes: 32 additions & 2 deletions frontend/src/components/PackageDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { Button } from '@/components/ui/button'
import { SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet'
import { ExternalLink, Plus, Trash2, Check, AlertTriangle } from 'lucide-react'

interface OSConstraint {
version: string
operator: '>=' | '<' | '>' | '<=' | '='
}

interface PackageInfo {
name: string
version: string
Expand All @@ -16,6 +21,7 @@ interface PackageInfo {
conflicts: string[]
osMin: string | null
osMax: string | null
osConstraints: OSConstraint[] | null
}

interface PackageDetailPanelProps {
Expand Down Expand Up @@ -55,12 +61,36 @@ export function PackageDetailPanel({
isOsCompatible = true,
}: PackageDetailPanelProps) {
const formatOsRange = () => {
if (pkg.osConstraints && pkg.osConstraints.length > 0) {
const minC = pkg.osConstraints.find(c => c.operator === '>=')
const maxC = pkg.osConstraints.find(c => c.operator === '<')
const exactC = pkg.osConstraints.find(c => c.operator === '=')

if (exactC) return exactC.version

if (minC && maxC) {
const maxInclusive = (parseFloat(maxC.version) - 0.01).toFixed(2)
return minC.version === maxInclusive ? minC.version : `${minC.version} – ${maxInclusive}`
}

if (minC) return `${minC.version}+`
if (maxC) {
const maxInclusive = (parseFloat(maxC.version) - 0.01).toFixed(2)
return `≤ ${maxInclusive}`
}
}

if (!pkg.osMin && !pkg.osMax) return null
if (pkg.osMin && pkg.osMax) {
return pkg.osMin === pkg.osMax ? pkg.osMin : `${pkg.osMin} – ${pkg.osMax}`
const maxInclusive = (parseFloat(pkg.osMax) - 0.01).toFixed(2)
return pkg.osMin === maxInclusive ? pkg.osMin : `${pkg.osMin} – ${maxInclusive}`
}
if (pkg.osMin) return `${pkg.osMin}+`
return `≤ ${pkg.osMax}`
if (pkg.osMax) {
const maxInclusive = (parseFloat(pkg.osMax) - 0.01).toFixed(2)
return `≤ ${maxInclusive}`
}
return null
}

const osRange = formatOsRange()
Expand Down
Loading