From e0dbd033971962dfbef1f413e1d5d26ce224f058 Mon Sep 17 00:00:00 2001 From: TaurusXin Date: Tue, 23 Jul 2024 16:32:49 +0800 Subject: [PATCH] finished: check update --- app.go | 22 ++++ frontend/package.json | 2 + frontend/package.json.md5 | 2 +- frontend/pnpm-lock.yaml | 29 +++++ frontend/src/components/ui/dialog.tsx | 120 ++++++++++++++++++ frontend/src/components/ui/progress.tsx | 26 ++++ frontend/src/main.tsx | 5 +- frontend/src/pages/Download.tsx | 59 +++++++++ frontend/src/pages/Panel.tsx | 108 +++++++++++++++- frontend/wailsjs/go/main/App.d.ts | 4 + frontend/wailsjs/go/main/App.js | 7 + frontend/wailsjs/go/service/SQLiteHelper.d.ts | 4 + frontend/wailsjs/go/service/SQLiteHelper.js | 8 ++ main.go | 8 +- service/download.go | 83 ++++++++++++ service/service.go | 61 ++++++--- 16 files changed, 511 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/progress.tsx create mode 100644 frontend/src/pages/Download.tsx create mode 100644 frontend/wailsjs/go/main/App.d.ts create mode 100644 frontend/wailsjs/go/main/App.js create mode 100644 service/download.go diff --git a/app.go b/app.go index a29c6af..140e539 100644 --- a/app.go +++ b/app.go @@ -1,7 +1,12 @@ package main import ( + "MacFastLookup/service" "context" + "os" + "path/filepath" + + "github.com/wailsapp/wails/v2/pkg/runtime" ) // App struct @@ -19,3 +24,20 @@ func NewApp() *App { func (a *App) startup(ctx context.Context) { a.ctx = ctx } + +func (a *App) StartDownload() { + dbFileDir := ".macfastlookup" + userHomeDir, _ := os.UserHomeDir() + dbFileName := "mac_vendors.sqlite3" + dbFilePath := filepath.Join(userHomeDir, dbFileDir, dbFileName) + + // create the folder if not exist + path := filepath.Join(userHomeDir, dbFileDir) + if _, err := os.Stat(path); os.IsNotExist(err) { + os.Mkdir(path, 0755) + } + + service.DownloadFile("https://tools.taurusxin.com/macfastlookup/mac_vendors.sqlite3", dbFilePath, true, func(progress, total int64) { + runtime.EventsEmit(a.ctx, "download-progress", float32(progress / total)) + }) +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index a4cd61b..e24c405 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,8 @@ "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "class-variance-authority": "^0.7.0", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 0f62ece..0ac0d32 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -6c7c0ac0f4deb7b7027a4a528f0c2aca \ No newline at end of file +50363c2ee2f3f6316fa41844894a6d9c \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3906e91..85d7dd9 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -417,6 +423,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.0': + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -1685,6 +1704,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c23630e --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx new file mode 100644 index 0000000..105fb65 --- /dev/null +++ b/frontend/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3626ff3..c3e087d 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {createRoot} from 'react-dom/client' import './style.css' import App from './App' @@ -8,7 +7,5 @@ const container = document.getElementById('root') const root = createRoot(container!) root.render( - - - + ) diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx new file mode 100644 index 0000000..84333d1 --- /dev/null +++ b/frontend/src/pages/Download.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react' +import { EventsOn } from '../../wailsjs/runtime/runtime' +import { StartDownload } from '../../wailsjs/go/main/App' +import { Close } from '../../wailsjs/go/service/SQLiteHelper' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Progress } from '@/components/ui/progress' + +const Download = (props: { onFinish: () => void }) => { + const [isOpen, setIsOpen] = useState(true) + + const [downloadProgress, setDownloadProgress] = useState(0.0) + const [isDisabled, setIsDisabled] = useState(true) + + useEffect(() => { + Close().then(res => { + StartDownload().then(res => { + console.log(res) + }) + EventsOn('download-progress', (progress: number) => { + setDownloadProgress(progress * 100) + if (progress === 1) { + props.onFinish() + setIsDisabled(false) + } + }) + }) + }, []) + + return ( + + + + Downloading + + + + + + + + ) +} + +export default Download diff --git a/frontend/src/pages/Panel.tsx b/frontend/src/pages/Panel.tsx index cd68895..724c550 100644 --- a/frontend/src/pages/Panel.tsx +++ b/frontend/src/pages/Panel.tsx @@ -3,13 +3,33 @@ import React, { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Checkbox } from '@/components/ui/checkbox' - import { toast } from 'sonner' -import { QueryMacAddressByPrefix, QueryVersion } from '../../wailsjs/go/service/SQLiteHelper' +import { + CheckBeforeInit, + InitSQLiteHelper, + QueryMacAddressByPrefix, + QueryVersion, +} from '../../wailsjs/go/service/SQLiteHelper' import { GetLatestVersion } from '../../wailsjs/go/service/UpdateService' +import { ReloadDatabase } from '../../wailsjs/go/service/SQLiteHelper' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' + +import Download from './Download' const Panel: React.FC = () => { + const [macInputDisabled, setMacInputDisabled] = useState(false) + const [macAddress, setMacAddress] = useState('') const [prefix, setPrefix] = useState('') const [vendorName, setVendorName] = useState('') @@ -20,14 +40,46 @@ const Panel: React.FC = () => { const [checkDisabled, setCheckDisabled] = useState(false) + const [newVersion, setNewVersion] = useState('') + const [isOpen, setIsOpen] = useState(false) + const [isDownloading, setIsDownloading] = useState(false) + useEffect(() => { - QueryVersion().then(res => { - setDatabaseDate(res) + CheckBeforeInit().then(res => { + if (res) { + InitSQLiteHelper().then(res => { + if (res) { + QueryVersion().then(res => { + setDatabaseDate(res) + }) + } else { + toast('Init dababase failed.', { + description: '⛓️‍💥 Please check your internet connection.', + action: { + label: 'Close', + onClick: () => {}, + }, + }) + } + }) + } else { + setCheckDisabled(true) + setMacInputDisabled(true) + // download + } }) }, []) const handleTextChange = (e: React.ChangeEvent) => { setMacAddress(e.target.value) + if (e.target.value === '') { + setPrefix('') + setVendorName('') + setIsPrivate(false) + setBlockType('') + setLastUpdate('') + return + } let address = e.target.value .trim() .toUpperCase() @@ -60,7 +112,7 @@ const Panel: React.FC = () => { if (res.Version === null) { setCheckDisabled(false) toast('Check update failed.', { - description: 'Please check your internet connection.', + description: '⛓️‍💥 Please check your internet connection.', action: { label: 'Close', onClick: () => {}, @@ -77,6 +129,9 @@ const Panel: React.FC = () => { }, }) } else { + // alert + setNewVersion(res) + setIsOpen(true) } } }) @@ -90,6 +145,7 @@ const Panel: React.FC = () => { value={macAddress} onChange={handleTextChange} placeholder="Enter MAC Address" + disabled={macInputDisabled} />
@@ -113,7 +169,7 @@ const Panel: React.FC = () => {
- +
+ + + + Update Available + + Version {newVersion} is available. Do you want to download it now? + + + + { + setIsOpen(false) + }} + > + No + + { + setIsOpen(false) + setIsDownloading(true) + }} + > + Yes + + + + + {isDownloading && ( + { + ReloadDatabase().then(res => { + if (res) { + QueryVersion().then(res => { + setDatabaseDate(res) + }) + } + }) + }} + /> + )}
) } diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..1ef3380 --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function StartDownload():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100644 index 0000000..3820e2b --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function StartDownload() { + return window['go']['main']['App']['StartDownload'](); +} diff --git a/frontend/wailsjs/go/service/SQLiteHelper.d.ts b/frontend/wailsjs/go/service/SQLiteHelper.d.ts index 46fa044..7d6dec6 100644 --- a/frontend/wailsjs/go/service/SQLiteHelper.d.ts +++ b/frontend/wailsjs/go/service/SQLiteHelper.d.ts @@ -2,8 +2,12 @@ // This file is automatically generated. DO NOT EDIT import {service} from '../models'; +export function CheckBeforeInit():Promise; + export function Close():Promise; +export function InitSQLiteHelper():Promise; + export function QueryMacAddressByPrefix(arg1:string):Promise; export function QueryVersion():Promise; diff --git a/frontend/wailsjs/go/service/SQLiteHelper.js b/frontend/wailsjs/go/service/SQLiteHelper.js index d0e8bdd..ee9987c 100644 --- a/frontend/wailsjs/go/service/SQLiteHelper.js +++ b/frontend/wailsjs/go/service/SQLiteHelper.js @@ -2,10 +2,18 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function CheckBeforeInit() { + return window['go']['service']['SQLiteHelper']['CheckBeforeInit'](); +} + export function Close() { return window['go']['service']['SQLiteHelper']['Close'](); } +export function InitSQLiteHelper() { + return window['go']['service']['SQLiteHelper']['InitSQLiteHelper'](); +} + export function QueryMacAddressByPrefix(arg1) { return window['go']['service']['SQLiteHelper']['QueryMacAddressByPrefix'](arg1); } diff --git a/main.go b/main.go index d25e52a..05a7137 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,6 @@ package main import ( "MacFastLookup/service" "embed" - "os" - "path/filepath" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" @@ -19,11 +17,7 @@ func main() { app := NewApp() // Create service - dbFileDir := "MacFastLookup" - userHomeDir, _ := os.UserHomeDir() - dbFileName := "mac_vendors.sqlite3" - dbFilePath := filepath.Join(userHomeDir, dbFileDir, dbFileName) - dbService := service.NewSQLiteHelper(dbFilePath) + dbService := service.NewSQLiteHelper() // create update service updateService := service.NewUpdateService("https://tools.taurusxin.com/macfastlookup/latest_version.txt") diff --git a/service/download.go b/service/download.go new file mode 100644 index 0000000..2c9acf0 --- /dev/null +++ b/service/download.go @@ -0,0 +1,83 @@ +package service + +import ( + "fmt" + "io" + "net/http" + "os" + "strconv" +) + +// ProgressWriter is a custom writer to track progress and execute a callback +type ProgressWriter struct { + io.Writer + Total int64 + Progress int64 + Callback func(progress int64, total int64) +} + +func (pw *ProgressWriter) Write(p []byte) (int, error) { + n, err := pw.Writer.Write(p) + if err != nil { + return n, err + } + pw.Progress += int64(n) + pw.Callback(pw.Progress, pw.Total) + return n, nil +} + +func DownloadFile(url string, filePath string, overwrite bool, callback func(progress int64, total int64)) error { + // Check if the file exists + if _, err := os.Stat(filePath); err == nil { + if overwrite { + // Overwrite the file if it exists + if err := os.Remove(filePath); err != nil { + return err + } + } else { + return fmt.Errorf("file %s already exists", filePath) + } + } + + // Create the file + out, err := os.Create(filePath) + if err != nil { + return err + } + defer out.Close() + + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + + // Check server response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + // Get the size + size, err := strconv.Atoi(resp.Header.Get("Content-Length")) + if err != nil { + return err + } + + // Create progress writer + pw := &ProgressWriter{ + Writer: out, + Total: int64(size), + Callback: callback, + } + + // Write the body to file with progress + _, err = io.Copy(pw, resp.Body) + if err != nil { + return err + } + + fmt.Println("\nDownload complete") + return nil +} diff --git a/service/service.go b/service/service.go index 4301a75..efc79e6 100644 --- a/service/service.go +++ b/service/service.go @@ -3,7 +3,8 @@ package service import ( "database/sql" "fmt" - "log" + "os" + "path/filepath" _ "github.com/mattn/go-sqlite3" ) @@ -18,21 +19,50 @@ type MacAddress struct { } type SQLiteHelper struct { - connectionString string - db *sql.DB + db *sql.DB } -func NewSQLiteHelper(dbFilePath string) *SQLiteHelper { - connectionString := fmt.Sprintf("file:%s?cache=shared&mode=rwc", dbFilePath) - db, err := sql.Open("sqlite3", connectionString) - if err != nil { - log.Fatal(err) +// create the instance of SQLiteHelper but not initialize the database connection +func NewSQLiteHelper() *SQLiteHelper { + return &SQLiteHelper{ + db: nil, + } +} + +// check if the directory and file for the database exist +func (helper *SQLiteHelper) CheckBeforeInit() bool { + userHomeDir, _ := os.UserHomeDir() + dbFileDir := filepath.Join(userHomeDir, ".macfastlookup") + dbFileName := "mac_vendors.sqlite3" + dbFilePath := filepath.Join(dbFileDir, dbFileName) + + if _, err := os.Stat(dbFileDir); os.IsNotExist(err) { + os.Mkdir(dbFileDir, 0755) } - return &SQLiteHelper{ - connectionString: connectionString, - db: db, + if _, err := os.Stat(dbFilePath); os.IsNotExist(err) { + return false } + + return true +} + +// initialize the database connection +func (helper *SQLiteHelper) InitSQLiteHelper() bool { + dbFileDir := ".macfastlookup" + userHomeDir, _ := os.UserHomeDir() + dbFileName := "mac_vendors.sqlite3" + dbFilePath := filepath.Join(userHomeDir, dbFileDir, dbFileName) + + connectionString := fmt.Sprintf("file:%s?cache=shared&mode=rwc", dbFilePath) + db, err := sql.Open("sqlite3", connectionString) + + if err != nil { + return false + } + + helper.db = db + return true } func (helper *SQLiteHelper) Close() { @@ -40,14 +70,7 @@ func (helper *SQLiteHelper) Close() { } func (helper *SQLiteHelper) ReloadDatabase() bool { - helper.db.Close() - db, err := sql.Open("sqlite3", helper.connectionString) - if err != nil { - log.Fatal(err) - return false - } - helper.db = db - return true + return helper.InitSQLiteHelper() } func (helper *SQLiteHelper) QueryMacAddressByPrefix(prefix string) (*MacAddress, error) {