From 6c366eecb09a9a4859b251b3a301dcaccc935080 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Thu, 27 Nov 2025 22:47:48 -0300 Subject: [PATCH] feat: integrate automatic update checks and enhance settings UI - Added functionality for automatic update checks in the settings page, allowing users to enable or disable this feature. - Integrated app version display and update availability notifications. - Updated settings store to manage auto-check updates state and handle version migration. - Enhanced layout with a new card for update settings, improving user experience and accessibility. --- .github/workflows/release.yml | 185 +++++++++++++++++++++++++ .gitignore | 1 + app/layout.tsx | 2 + app/settings/page.tsx | 91 +++++++++++- components/app-updater-provider.tsx | 40 ++++++ components/modals/update-dialog.tsx | 153 +++++++++++++++++++++ components/ui/checkbox.tsx | 30 ++++ docs/updater-setup.md | 118 ++++++++++++++++ hooks/use-app-updater.ts | 142 +++++++++++++++++++ package.json | 5 +- pnpm-lock.yaml | 10 ++ scripts/bump-version.js | 71 ++++++++++ scripts/generate-changelog.js | 205 ++++++++++++++++++++++++++++ src-tauri/Cargo.lock | 176 +++++++++++++++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 3 +- src-tauri/src/lib.rs | 3 + src-tauri/tauri.conf.json | 12 +- store/settings-store.ts | 23 +++- ~/.tauri/ollahub.key.pub | 1 + 20 files changed, 1266 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 components/app-updater-provider.tsx create mode 100644 components/modals/update-dialog.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 docs/updater-setup.md create mode 100644 hooks/use-app-updater.ts create mode 100644 scripts/bump-version.js create mode 100644 scripts/generate-changelog.js create mode 100644 ~/.tauri/ollahub.key.pub diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f915ae1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,185 @@ +name: Build and Release + +on: + push: + branches: + - main + - master + workflow_dispatch: + +env: + TAURI_VERSION: "2.9.4" + +jobs: + build: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - platform: 'windows-latest' + target: 'x86_64-pc-windows-msvc' + bundle: 'msi' + - platform: 'macos-latest' + target: 'aarch64-apple-darwin' + bundle: 'dmg' + - platform: 'macos-latest' + target: 'x86_64-apple-darwin' + bundle: 'dmg' + - platform: 'ubuntu-latest' + target: 'x86_64-unknown-linux-gnu' + bundle: 'appimage' + + runs-on: ${{ matrix.platform }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: ${{ matrix.target }} + + - name: Install dependencies (Ubuntu only) + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Generate changelog + run: pnpm generate-changelog + + - name: Build Tauri app + run: | + cd src-tauri + cargo build --release --target ${{ matrix.target }} + + - name: Build Tauri bundle + env: + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY }} + run: | + cd src-tauri + if [ -n "$TAURI_PRIVATE_KEY" ]; then + echo "$TAURI_PRIVATE_KEY" > /tmp/private.key + cargo tauri build --target ${{ matrix.target }} --config '{"updater":{"active":true}}' + rm /tmp/private.key + else + cargo tauri build --target ${{ matrix.target }} + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.platform }}-${{ matrix.target }} + path: | + src-tauri/target/${{ matrix.target }}/release/bundle/**/*.${{ matrix.bundle }} + src-tauri/target/${{ matrix.target }}/release/bundle/**/*.exe + src-tauri/target/${{ matrix.target }}/release/bundle/**/*.AppImage + src-tauri/target/${{ matrix.target }}/release/bundle/**/*.deb + retention-days: 7 + + create-release: + needs: build + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Get version + id: get_version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + + - name: Generate changelog for release + run: pnpm generate-changelog + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Extract release notes + id: extract_notes + run: | + if [ -f "data/changelog.json" ]; then + NOTES=$(node -e " + const fs = require('fs'); + try { + const changelog = JSON.parse(fs.readFileSync('data/changelog.json', 'utf-8')); + const latestRelease = changelog.releases[0]; + if (latestRelease) { + let notes = '## ' + (latestRelease.version === 'unreleased' ? 'Próxima Versão' : 'v' + latestRelease.version) + '\\n\\n'; + notes += '**Data:** ' + latestRelease.date + '\\n\\n'; + notes += '### Mudanças\\n\\n'; + latestRelease.commits.forEach(commit => { + const icon = commit.type === 'feat' ? '✨' : '🐛'; + notes += '- ' + icon + ' **' + (commit.type === 'feat' ? 'Nova Funcionalidade' : 'Correção') + ':** ' + commit.message + '\\n'; + }); + console.log(notes); + } else { + console.log('## Release ${{ steps.get_version.outputs.version }}'); + } + } catch (e) { + console.log('## Release ${{ steps.get_version.outputs.version }}'); + } + ") + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "notes=## Release ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.tag }} + name: Release ${{ steps.get_version.outputs.tag }} + body: | + ${{ steps.extract_notes.outputs.notes }} + + ## Downloads + + Baixe o instalador para sua plataforma nos assets abaixo. + draft: false + files: artifacts/**/* + generate_release_notes: false + diff --git a/.gitignore b/.gitignore index 5ef6a52..2146da4 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +~/.tauri/ollahub.key diff --git a/app/layout.tsx b/app/layout.tsx index da5c8c3..8ad06f7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; +import { AppUpdaterProvider } from "@/components/app-updater-provider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -35,6 +36,7 @@ export default function RootLayout({ disableTransitionOnChange > {children} + diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 40472ea..418d46e 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -24,11 +24,13 @@ import { useSettingsStore } from '@/store/settings-store'; import { DomainTagsInput } from '@/components/settings/domain-tags-input'; import { HardwareDashboard } from '@/components/settings/HardwareDashboard'; import { invoke } from '@tauri-apps/api/core'; -import { CheckCircle2, XCircle, Loader2, Download, Trash2, Copy, ExternalLink, Plus, X, BookOpen, GraduationCap, Newspaper, Code, DollarSign, Edit, RotateCcw, Terminal, Power, ArrowLeft } from 'lucide-react'; +import { CheckCircle2, XCircle, Loader2, Download, Trash2, Copy, ExternalLink, Plus, X, BookOpen, GraduationCap, Newspaper, Code, DollarSign, Edit, RotateCcw, Terminal, Power, ArrowLeft, Sparkles, RefreshCw } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { useRouter } from 'next/navigation'; import { SourcesConfig, SourceCategory } from '@/lib/types'; import { TitleBar } from '@/components/titlebar'; +import { useAppUpdater } from '@/hooks/use-app-updater'; +import { getVersion } from '@tauri-apps/api/app'; interface Model { name: string; @@ -52,12 +54,20 @@ export default function SettingsPage() { const [isSavingSources, setIsSavingSources] = useState(false); const [recentLogs, setRecentLogs] = useState([]); const [isLoadingLogs, setIsLoadingLogs] = useState(false); + const [appVersion, setAppVersion] = useState(''); + + const updater = useAppUpdater(); // Load models, storage path, and sources config on mount useEffect(() => { loadModels(); loadStoragePath(); settings.fetchSources(); + + // Load app version + getVersion() + .then(setAppVersion) + .catch(() => setAppVersion('0.1.0')); }, [settings.ollamaUrl]); // Load logs periodically @@ -1095,6 +1105,85 @@ export default function SettingsPage() { + + + + Atualizações + + Configure verificação automática de atualizações + + + +
+
+ +

+ O app verificará novas versões periodicamente +

+
+ +
+ +
+
+ Versão Atual + + {appVersion || 'Carregando...'} + +
+ {updater.updateAvailable && ( +
+ Nova Versão Disponível + + {updater.updateInfo?.version} + +
+ )} +
+ + + + {updater.error && ( +
+

{updater.error}

+
+ )} +
+
diff --git a/components/app-updater-provider.tsx b/components/app-updater-provider.tsx new file mode 100644 index 0000000..95f4c30 --- /dev/null +++ b/components/app-updater-provider.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { UpdateDialog } from '@/components/modals/update-dialog'; +import { useAppUpdater } from '@/hooks/use-app-updater'; +import { useSettingsStore } from '@/store/settings-store'; + +export function AppUpdaterProvider() { + const { updateAvailable, checkUpdate } = useAppUpdater(); + const [isOpen, setIsOpen] = useState(false); + const [hasChecked, setHasChecked] = useState(false); + const autoCheckEnabled = useSettingsStore((state) => state.autoCheckUpdates ?? true); + + useEffect(() => { + if (!autoCheckEnabled || hasChecked) return; + + // Verificar após um delay para não interferir no carregamento inicial + const timer = setTimeout(() => { + checkUpdate().then(() => { + setHasChecked(true); + }); + }, 5000); // 5 segundos após o app carregar + + return () => clearTimeout(timer); + }, [autoCheckEnabled, hasChecked, checkUpdate]); + + useEffect(() => { + if (updateAvailable && !isOpen) { + setIsOpen(true); + } + }, [updateAvailable, isOpen]); + + return ( + + ); +} + diff --git a/components/modals/update-dialog.tsx b/components/modals/update-dialog.tsx new file mode 100644 index 0000000..54936fa --- /dev/null +++ b/components/modals/update-dialog.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Download, Loader2, X, Sparkles } from 'lucide-react'; +import { useAppUpdater } from '@/hooks/use-app-updater'; +import { useSettingsStore } from '@/store/settings-store'; + +interface UpdateDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function UpdateDialog({ open, onOpenChange }: UpdateDialogProps) { + const { + currentVersion, + updateAvailable, + updateInfo, + isChecking, + isDownloading, + downloadProgress, + error, + installUpdate, + } = useAppUpdater(); + + const [dontShowAgain, setDontShowAgain] = useState(false); + const setAutoCheckUpdates = useSettingsStore((state) => state.setAutoCheckUpdates); + + const handleInstall = async () => { + if (dontShowAgain) { + setAutoCheckUpdates(false); + } + await installUpdate(); + }; + + const handleClose = () => { + if (dontShowAgain) { + setAutoCheckUpdates(false); + } + onOpenChange(false); + }; + + if (!updateAvailable && !isChecking) { + return null; + } + + return ( + + + + + + Atualização Disponível + + + Uma nova versão do OllaHub está disponível para download. + + + +
+
+
+ Versão Atual + {currentVersion} +
+
+ Nova Versão + + {updateInfo?.version} + +
+
+ + {updateInfo?.body && ( +
+

+ {updateInfo.body} +

+
+ )} + + {isDownloading && ( +
+
+ Baixando atualização... + {downloadProgress}% +
+ +
+ )} + + {error && ( +
+

{error}

+
+ )} + +
+ setDontShowAgain(checked === true)} + /> + +
+
+ +
+ + +
+
+
+ ); +} + diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/docs/updater-setup.md b/docs/updater-setup.md new file mode 100644 index 0000000..ec99e23 --- /dev/null +++ b/docs/updater-setup.md @@ -0,0 +1,118 @@ +# Configuração do Sistema de Atualização Automática + +Este documento explica como configurar o sistema de atualização automática do OllaHub. + +## Pré-requisitos + +1. Repositório GitHub configurado +2. Rust e Cargo instalados +3. Tauri CLI instalado + +## Passo 1: Gerar Chaves de Assinatura + +Execute o script para gerar as chaves: + +```bash +chmod +x scripts/generate-updater-keys.sh +./scripts/generate-updater-keys.sh +``` + +Ou manualmente: + +```bash +mkdir -p ~/.tauri +cargo tauri signer generate -w ~/.tauri/ollahub.key +``` + +## Passo 2: Configurar Chave Pública + +1. Copie a chave pública gerada (está em `~/.tauri/ollahub.key.pub`) +2. Edite `src-tauri/tauri.conf.json` +3. Substitua `YOUR_PUBLIC_KEY_HERE` pela chave pública +4. Atualize o endpoint com seu repositório GitHub: + +```json +{ + "updater": { + "active": true, + "endpoints": [ + "https://api.github.com/repos/SEU_USUARIO/SEU_REPOSITORIO/releases/latest" + ], + "dialog": true, + "pubkey": "SUA_CHAVE_PUBLICA_AQUI" + } +} +``` + +## Passo 3: Configurar GitHub Secrets + +1. Vá em **Settings** > **Secrets and variables** > **Actions** +2. Clique em **New repository secret** +3. Nome: `TAURI_UPDATER_PRIVATE_KEY` +4. Valor: Cole o conteúdo completo do arquivo `~/.tauri/ollahub.key` + +## Passo 4: Atualizar Workflow (se necessário) + +O workflow `.github/workflows/release.yml` já está configurado para: +- Buildar para Windows, macOS e Linux +- Assinar binários com a chave privada +- Criar release automaticamente quando há push em `main`/`master` + +## Como Funciona + +1. **Build Automático**: Quando você faz push para `main`/`master`, o GitHub Actions: + - Builda o app para todas as plataformas + - Assina os binários + - Cria uma release no GitHub + +2. **Verificação de Atualizações**: O app verifica automaticamente: + - 5 segundos após iniciar (primeira verificação) + - A cada 6 horas (verificação periódica) + - Manualmente via botão nas configurações + +3. **Instalação**: Quando há atualização disponível: + - Usuário recebe notificação + - Pode escolher atualizar agora ou depois + - Download e instalação são automáticos + - App reinicia automaticamente após instalação + +## Bump de Versão + +Para criar uma nova versão: + +```bash +# Patch (0.1.0 -> 0.1.1) +pnpm run bump-version patch --commit + +# Minor (0.1.0 -> 0.2.0) +pnpm run bump-version minor --commit + +# Major (0.1.0 -> 1.0.0) +pnpm run bump-version major --commit +``` + +Depois, faça push: + +```bash +git push && git push --tags +``` + +O GitHub Actions criará automaticamente a release. + +## Troubleshooting + +### Erro: "Failed to check for updates" +- Verifique se o endpoint no `tauri.conf.json` está correto +- Verifique se há releases públicas no GitHub +- Verifique a conexão com a internet + +### Erro: "Signature verification failed" +- Verifique se a chave pública no `tauri.conf.json` está correta +- Verifique se a chave privada no GitHub Secret está correta +- Regere as chaves se necessário + +### Release não é criada automaticamente +- Verifique se o push foi feito para `main` ou `master` +- Verifique os logs do GitHub Actions +- Verifique se as permissões do workflow estão corretas + diff --git a/hooks/use-app-updater.ts b/hooks/use-app-updater.ts new file mode 100644 index 0000000..96838a6 --- /dev/null +++ b/hooks/use-app-updater.ts @@ -0,0 +1,142 @@ +import { useState, useEffect, useCallback } from 'react'; +import { getVersion } from '@tauri-apps/api/app'; +import { useSettingsStore } from '@/store/settings-store'; +import { check as checkForUpdate } from '@tauri-apps/plugin-updater'; + +export interface UpdateInfo { + version: string; + date?: string; + body?: string; +} + +export interface AppUpdaterState { + currentVersion: string; + updateAvailable: boolean; + updateInfo: UpdateInfo | null; + isChecking: boolean; + isDownloading: boolean; + downloadProgress: number; + error: string | null; + checkUpdate: () => Promise; + installUpdate: () => Promise; +} + +export function useAppUpdater(): AppUpdaterState { + const [currentVersion, setCurrentVersion] = useState(''); + const [updateAvailable, setUpdateAvailable] = useState(false); + const [updateInfo, setUpdateInfo] = useState(null); + const [isChecking, setIsChecking] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(0); + const [error, setError] = useState(null); + + const autoCheckEnabled = useSettingsStore((state) => state.autoCheckUpdates ?? true); + + // Obter versão atual + useEffect(() => { + const fetchVersion = async () => { + try { + const version = await getVersion(); + setCurrentVersion(version); + } catch (err) { + console.error('Failed to get app version:', err); + setCurrentVersion('0.1.0'); + } + }; + + fetchVersion(); + }, []); + + // Verificar atualizações + const checkUpdate = useCallback(async () => { + setIsChecking(true); + setError(null); + + try { + const update = await checkForUpdate(); + + if (update?.available) { + setUpdateAvailable(true); + setUpdateInfo({ + version: update.version, + date: update.date, + body: update.body, + }); + } else { + setUpdateAvailable(false); + setUpdateInfo(null); + } + } catch (err) { + console.error('Failed to check for updates:', err); + setError(err instanceof Error ? err.message : 'Erro ao verificar atualizações'); + setUpdateAvailable(false); + } finally { + setIsChecking(false); + } + }, []); + + // Instalar atualização + const installUpdate = useCallback(async () => { + if (!updateAvailable) return; + + setIsDownloading(true); + setError(null); + setDownloadProgress(0); + + try { + const update = await checkForUpdate(); + + if (update?.available) { + // Escutar progresso do download + const unlisten = await update.on('download-progress', (progress: any) => { + setDownloadProgress(progress.progress || 0); + }); + + // Baixar e instalar atualização + await update.downloadAndInstall(); + + unlisten(); + + // Reiniciar aplicativo após instalação + await update.installAndRestart(); + } + } catch (err) { + console.error('Failed to install update:', err); + setError(err instanceof Error ? err.message : 'Erro ao instalar atualização'); + setIsDownloading(false); + } + }, [updateAvailable]); + + // Verificação automática periódica (a cada 6 horas) + useEffect(() => { + if (!autoCheckEnabled) return; + + // Verificar imediatamente ao montar (com delay para não interferir no carregamento) + const initialTimer = setTimeout(() => { + checkUpdate(); + }, 5000); // 5 segundos após o app carregar + + // Verificar periodicamente + const interval = setInterval(() => { + checkUpdate(); + }, 6 * 60 * 60 * 1000); // 6 horas + + return () => { + clearTimeout(initialTimer); + clearInterval(interval); + }; + }, [autoCheckEnabled, checkUpdate]); + + return { + currentVersion, + updateAvailable, + updateInfo, + isChecking, + isDownloading, + downloadProgress, + error, + checkUpdate, + installUpdate, + }; +} + diff --git a/package.json b/package.json index 3c76dcb..2f30dc0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "next dev --webpack", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "generate-changelog": "node scripts/generate-changelog.js", + "bump-version": "node scripts/bump-version.js" }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", @@ -23,6 +25,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tauri-apps/api": "^2.9.0", + "@tauri-apps/plugin-updater": "^2.0.0", "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89ca89a..862e6ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@tauri-apps/api': specifier: ^2.9.0 version: 2.9.0 + '@tauri-apps/plugin-updater': + specifier: ^2.0.0 + version: 2.9.0 '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 @@ -1153,6 +1156,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-updater@2.9.0': + resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -4228,6 +4234,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.9.4 '@tauri-apps/cli-win32-x64-msvc': 2.9.4 + '@tauri-apps/plugin-updater@2.9.0': + dependencies: + '@tauri-apps/api': 2.9.0 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100644 index 0000000..3fadb3c --- /dev/null +++ b/scripts/bump-version.js @@ -0,0 +1,71 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const versionType = process.argv[2] || 'patch'; // patch, minor, major +const shouldCommit = process.argv.includes('--commit'); + +// Validar tipo de versão +if (!['patch', 'minor', 'major'].includes(versionType)) { + console.error('❌ Tipo de versão inválido. Use: patch, minor ou major'); + process.exit(1); +} + +// Ler package.json +const packageJsonPath = path.join(__dirname, '..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + +// Bump version +const [major, minor, patch] = packageJson.version.split('.').map(Number); +let newVersion; + +switch (versionType) { + case 'major': + newVersion = `${major + 1}.0.0`; + break; + case 'minor': + newVersion = `${major}.${minor + 1}.0`; + break; + case 'patch': + default: + newVersion = `${major}.${minor}.${patch + 1}`; + break; +} + +console.log(`📦 Atualizando versão de ${packageJson.version} para ${newVersion}...`); + +// Atualizar package.json +packageJson.version = newVersion; +fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); +console.log('✅ package.json atualizado'); + +// Atualizar tauri.conf.json +const tauriConfPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json'); +const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf-8')); +tauriConf.version = newVersion; +fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n'); +console.log('✅ tauri.conf.json atualizado'); + +// Atualizar Cargo.toml +const cargoTomlPath = path.join(__dirname, '..', 'src-tauri', 'Cargo.toml'); +let cargoToml = fs.readFileSync(cargoTomlPath, 'utf-8'); +cargoToml = cargoToml.replace(/^version = ".*"$/m, `version = "${newVersion}"`); +fs.writeFileSync(cargoTomlPath, cargoToml); +console.log('✅ Cargo.toml atualizado'); + +// Criar commit e tag (opcional) +if (shouldCommit) { + try { + execSync(`git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml`, { stdio: 'inherit' }); + execSync(`git commit -m "chore: bump version to ${newVersion}"`, { stdio: 'inherit' }); + execSync(`git tag v${newVersion}`, { stdio: 'inherit' }); + console.log(`✅ Commit e tag v${newVersion} criados`); + console.log(`\n📝 Próximo passo: git push && git push --tags`); + } catch (error) { + console.error('❌ Erro ao criar commit/tag:', error.message); + process.exit(1); + } +} + +console.log(`\n✅ Versão atualizada para ${newVersion}`); + diff --git a/scripts/generate-changelog.js b/scripts/generate-changelog.js new file mode 100644 index 0000000..f37be56 --- /dev/null +++ b/scripts/generate-changelog.js @@ -0,0 +1,205 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// Ler versão do package.json +const packageJsonPath = path.join(__dirname, '..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); +const currentVersion = packageJson.version; + +// Verificar se estamos em um repositório Git +let isGitRepo = false; +try { + execSync('git rev-parse --git-dir', { stdio: 'ignore', cwd: path.join(__dirname, '..') }); + isGitRepo = true; +} catch (error) { + console.warn('⚠️ Não é um repositório Git. Gerando changelog vazio.'); +} + +// Função para sanitizar mensagens de commit +function sanitizeMessage(message) { + return message + .replace(/\n/g, ' ') + .replace(/\r/g, '') + .replace(/"/g, '\\"') + .trim(); +} + +// Função para extrair tipo e mensagem do commit +function parseCommitMessage(fullMessage) { + const featMatch = fullMessage.match(/^feat(\(.+?\))?:\s*(.+)$/i); + const fixMatch = fullMessage.match(/^fix(\(.+?\))?:\s*(.+)$/i); + + if (featMatch) { + return { type: 'feat', message: featMatch[2] || featMatch[0] }; + } + if (fixMatch) { + return { type: 'fix', message: fixMatch[2] || fixMatch[0] }; + } + return null; +} + +// Função para obter tags de versão +function getVersionTags() { + try { + const tags = execSync('git tag --sort=-version:refname', { + encoding: 'utf-8', + cwd: path.join(__dirname, '..'), + }) + .trim() + .split('\n') + .filter(Boolean) + .filter(tag => /^v?\d+\.\d+\.\d+/.test(tag)); + return tags; + } catch (error) { + return []; + } +} + +// Função para obter commits entre duas tags ou desde o início +function getCommitsBetweenTags(fromTag, toTag) { + try { + let range = ''; + if (fromTag && toTag) { + range = `${fromTag}..${toTag}`; + } else if (toTag) { + range = toTag; + } else { + range = 'HEAD'; + } + + const logFormat = '%H|%an|%ad|%s'; + const dateFormat = '--date=iso-strict'; + + const output = execSync( + `git log ${range} --pretty=format:"${logFormat}" --date=${dateFormat}`, + { + encoding: 'utf-8', + cwd: path.join(__dirname, '..'), + } + ); + + return output + .trim() + .split('\n') + .filter(Boolean) + .map(line => { + const [hash, author, date, message] = line.split('|'); + const parsed = parseCommitMessage(message); + if (!parsed) return null; + + return { + hash: hash.substring(0, 7), + type: parsed.type, + message: sanitizeMessage(parsed.message), + author: author.trim(), + date: date.trim(), + }; + }) + .filter(Boolean); + } catch (error) { + return []; + } +} + +// Função principal para gerar changelog +function generateChangelog() { + const changelogPath = path.join(__dirname, '..', 'data', 'changelog.json'); + const dataDir = path.dirname(changelogPath); + + // Criar diretório data se não existir + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + if (!isGitRepo) { + // Gerar changelog vazio se não for repositório Git + const emptyChangelog = { + version: currentVersion, + generatedAt: new Date().toISOString(), + releases: [], + }; + fs.writeFileSync(changelogPath, JSON.stringify(emptyChangelog, null, 2), 'utf-8'); + console.log('✅ Changelog vazio gerado (não é repositório Git)'); + return; + } + + const releases = []; + const versionTags = getVersionTags(); + + if (versionTags.length > 0) { + // Agrupar por tags de versão + for (let i = 0; i < versionTags.length; i++) { + const currentTag = versionTags[i]; + const nextTag = i < versionTags.length - 1 ? versionTags[i + 1] : null; + + const commits = getCommitsBetweenTags(nextTag, currentTag); + + if (commits.length > 0) { + // Extrair versão da tag (remover 'v' prefix se existir) + const version = currentTag.replace(/^v/, ''); + const releaseDate = commits[0]?.date || new Date().toISOString(); + + releases.push({ + version, + date: releaseDate.split('T')[0], + commits, + }); + } + } + + // Adicionar commits não versionados (após a última tag) + const unreleasedCommits = getCommitsBetweenTags(versionTags[0], 'HEAD'); + if (unreleasedCommits.length > 0) { + releases.unshift({ + version: 'unreleased', + date: unreleasedCommits[0]?.date.split('T')[0] || new Date().toISOString().split('T')[0], + commits: unreleasedCommits, + }); + } + } else { + // Sem tags, agrupar por data (últimos 30 dias ou todos se menos) + const allCommits = getCommitsBetweenTags(null, 'HEAD'); + + if (allCommits.length > 0) { + // Agrupar por data + const commitsByDate = {}; + allCommits.forEach(commit => { + const date = commit.date.split('T')[0]; + if (!commitsByDate[date]) { + commitsByDate[date] = []; + } + commitsByDate[date].push(commit); + }); + + // Criar releases por data (ordenado por data decrescente) + Object.keys(commitsByDate) + .sort((a, b) => b.localeCompare(a)) + .forEach(date => { + releases.push({ + version: 'unreleased', + date, + commits: commitsByDate[date], + }); + }); + } + } + + const changelog = { + version: currentVersion, + generatedAt: new Date().toISOString(), + releases, + }; + + fs.writeFileSync(changelogPath, JSON.stringify(changelog, null, 2), 'utf-8'); + console.log(`✅ Changelog gerado: ${releases.length} release(s) encontrada(s)`); + console.log(` Total de commits: ${releases.reduce((sum, r) => sum + r.commits.length, 0)}`); +} + +// Executar +try { + generateChangelog(); +} catch (error) { + console.error('❌ Erro ao gerar changelog:', error.message); + process.exit(1); +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c83a15f..d2317e2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -129,6 +129,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-notification", "tauri-plugin-shell", + "tauri-plugin-updater", "tokenizers", "tokio", "tokio-cron-scheduler", @@ -136,7 +137,16 @@ dependencies = [ "urlencoding", "uuid", "walkdir", - "zip", + "zip 0.6.6", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", ] [[package]] @@ -1095,6 +1105,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -1822,8 +1843,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1833,9 +1856,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -2280,6 +2305,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 1.0.4", ] [[package]] @@ -2801,6 +2827,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2938,6 +2970,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign-verify" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3447,6 +3485,18 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -3655,6 +3705,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.17", +] + [[package]] name = "pango" version = "0.18.3" @@ -4129,6 +4193,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -4471,6 +4590,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -4478,6 +4599,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -4487,6 +4609,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots 1.0.4", ] [[package]] @@ -4585,6 +4708,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4637,6 +4766,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ + "web-time", "zeroize", ] @@ -5669,6 +5799,38 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http 1.3.1", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.12.24", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.17", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip 4.6.1", +] + [[package]] name = "tauri-runtime" version = "2.9.1" @@ -7668,6 +7830,18 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.12.0", + "memchr", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1701606..5f72eb0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,6 +44,7 @@ futures-util = "0.3" regex = "1.10" walkdir = "2.5" tauri-plugin-autostart = "2" +tauri-plugin-updater = "2" dirs = "5.0" rfd = "0.14" rusqlite = { version = "0.31", features = ["bundled"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 1ba7fc8..8ae57c7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,6 +13,7 @@ "core:window:allow-maximize", "core:window:allow-unmaximize", "core:window:allow-close", - "core:window:allow-is-maximized" + "core:window:allow-is-maximized", + "updater:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6892aa4..83ec2ae 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3613,6 +3613,9 @@ pub fn run() { // Plugin de notificações app.handle().plugin(tauri_plugin_notification::init())?; + // Plugin de atualização automática + app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; + // Modificar comportamento de fechar janela (ocultar ao invés de fechar) if let Some(window) = app.get_webview_window("main") { let window_clone = window.clone(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2bc7910..fb7f18d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ "frontendDist": "../out", "devUrl": "http://localhost:3000", "beforeDevCommand": "pnpm dev", - "beforeBuildCommand": "pnpm build" + "beforeBuildCommand": "pnpm generate-changelog && pnpm build" }, "app": { "windows": [ @@ -35,5 +35,15 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "updater": { + "active": true, + "endpoints": [ + "https://github.com/evandrodevbr/OllaHub/releases/latest/download/latest.json" + ], + "dialog": true, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDgxMjY3NThDMUQyREQ5QUYKUldTdjJTMGRqSFVtZ1k1ZWFHSEcxNExDZzgrNVh6M0Z6VXIyaTU5dDAvZnpQb3p0UVpuRW1Od3YK" + } } } diff --git a/store/settings-store.ts b/store/settings-store.ts index 8bf5255..65d79de 100644 --- a/store/settings-store.ts +++ b/store/settings-store.ts @@ -56,6 +56,9 @@ export interface SettingsState { keywords: string[]; }; + // Updates + autoCheckUpdates: boolean; + // Query Preprocessing queryPreprocessing: { enabled: boolean; @@ -96,6 +99,9 @@ export interface SettingsState { addNotificationKeyword: (keyword: string) => void; removeNotificationKeyword: (keyword: string) => void; + // Updates Actions + setAutoCheckUpdates: (enabled: boolean) => void; + // Query Preprocessing Actions setQueryPreprocessingEnabled: (enabled: boolean) => void; setQueryPreprocessingMinLength: (min: number) => void; @@ -221,6 +227,7 @@ const initialState = { enabled: true, keywords: [], }, + autoCheckUpdates: true, queryPreprocessing: { enabled: true, minLength: 3, @@ -407,6 +414,10 @@ export const useSettingsStore = create()( }, })), + // Updates Actions + setAutoCheckUpdates: (enabled) => + set({ autoCheckUpdates: enabled }), + // Query Preprocessing Actions setQueryPreprocessingEnabled: (enabled) => set((state) => ({ @@ -479,7 +490,17 @@ export const useSettingsStore = create()( }), { name: 'ollahub-settings', - version: 1, + version: 3, + migrate: (persistedState: any, version: number) => { + // Migração da versão 1 para 3 + if (version < 3) { + return { + ...persistedState, + autoCheckUpdates: persistedState.autoCheckUpdates ?? true, + }; + } + return persistedState; + }, } ) ); diff --git a/~/.tauri/ollahub.key.pub b/~/.tauri/ollahub.key.pub new file mode 100644 index 0000000..104b813 --- /dev/null +++ b/~/.tauri/ollahub.key.pub @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDgxMjY3NThDMUQyREQ5QUYKUldTdjJTMGRqSFVtZ1k1ZWFHSEcxNExDZzgrNVh6M0Z6VXIyaTU5dDAvZnpQb3p0UVpuRW1Od3YK \ No newline at end of file