From 0ba9443ef4e767189c3f14b2230fd53335e6e9a5 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Sat, 20 Dec 2025 07:13:55 +0700 Subject: [PATCH] v6.9 --- app.go | 15 -- backend/ffmpeg.go | 92 --------- frontend/components.json | 4 +- frontend/package.json | 5 +- frontend/package.json.md5 | 2 +- frontend/pnpm-lock.yaml | 189 ++++++------------ .../src/components/AudioConverterPage.tsx | 85 +------- frontend/src/components/FileManagerPage.tsx | 39 ++-- frontend/src/components/SettingsPage.tsx | 42 ++-- frontend/src/components/Sidebar.tsx | 144 ++++++++++--- frontend/src/components/ui/activity.tsx | 104 ++++++++++ frontend/src/components/ui/alert-dialog.tsx | 157 --------------- frontend/src/components/ui/blocks.tsx | 92 +++++++++ frontend/src/components/ui/coffee.tsx | 118 +++++++++++ frontend/src/components/ui/github.tsx | 149 ++++++++++++++ frontend/src/components/ui/home.tsx | 103 ++++++++++ frontend/src/components/ui/radio-group.tsx | 45 ----- frontend/src/components/ui/scroll-area.tsx | 46 ----- frontend/src/components/ui/settings.tsx | 92 +++++++++ frontend/src/components/ui/tabs.tsx | 66 ------ frontend/src/components/ui/terminal.tsx | 103 ++++++++++ 21 files changed, 976 insertions(+), 716 deletions(-) create mode 100644 frontend/src/components/ui/activity.tsx delete mode 100644 frontend/src/components/ui/alert-dialog.tsx create mode 100644 frontend/src/components/ui/blocks.tsx create mode 100644 frontend/src/components/ui/coffee.tsx create mode 100644 frontend/src/components/ui/github.tsx create mode 100644 frontend/src/components/ui/home.tsx delete mode 100644 frontend/src/components/ui/radio-group.tsx delete mode 100644 frontend/src/components/ui/scroll-area.tsx create mode 100644 frontend/src/components/ui/settings.tsx delete mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/components/ui/terminal.tsx diff --git a/app.go b/app.go index 189e659..4c9e00e 100644 --- a/app.go +++ b/app.go @@ -646,21 +646,6 @@ func (a *App) DownloadFFmpeg() DownloadFFmpegResponse { } } -// InstallFFmpegFromFile installs ffmpeg from a local file path -func (a *App) InstallFFmpegFromFile(filePath string) DownloadFFmpegResponse { - err := backend.InstallFFmpegFromFile(filePath) - if err != nil { - return DownloadFFmpegResponse{ - Success: false, - Error: err.Error(), - } - } - return DownloadFFmpegResponse{ - Success: true, - Message: "FFmpeg installed successfully from file", - } -} - // ConvertAudioRequest represents a request to convert audio files type ConvertAudioRequest struct { InputFiles []string `json:"input_files"` diff --git a/backend/ffmpeg.go b/backend/ffmpeg.go index 7ac24e3..0597046 100644 --- a/backend/ffmpeg.go +++ b/backend/ffmpeg.go @@ -13,7 +13,6 @@ import ( "runtime" "strings" "sync" - "time" "github.com/ulikunitz/xz" ) @@ -614,94 +613,3 @@ func GetAudioFileInfo(filePath string) (*AudioFileInfo, error) { Size: info.Size(), }, nil } - -// InstallFFmpegFromFile installs ffmpeg from a local file path -func InstallFFmpegFromFile(filePath string) error { - // Check if file exists - info, err := os.Stat(filePath) - if err != nil { - return fmt.Errorf("file does not exist: %w", err) - } - - // Check if it's a regular file (not a directory) - if info.IsDir() { - return fmt.Errorf("path is a directory, not a file") - } - - // Verify it's likely an ffmpeg executable by checking the filename - fileName := strings.ToLower(filepath.Base(filePath)) - expectedName := "ffmpeg" - if runtime.GOOS == "windows" { - expectedName = "ffmpeg.exe" - } - - if fileName != expectedName && !strings.Contains(fileName, "ffmpeg") { - return fmt.Errorf("file does not appear to be an ffmpeg executable (expected name containing 'ffmpeg')") - } - - // Get destination path - ffmpegPath, err := GetFFmpegPath() - if err != nil { - return fmt.Errorf("failed to get ffmpeg path: %w", err) - } - - ffmpegDir := filepath.Dir(ffmpegPath) - - // Create directory if it doesn't exist - if err := os.MkdirAll(ffmpegDir, 0755); err != nil { - return fmt.Errorf("failed to create ffmpeg directory: %w", err) - } - - // Copy file to destination - sourceFile, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open source file: %w", err) - } - - destFile, err := os.OpenFile(ffmpegPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - sourceFile.Close() - return fmt.Errorf("failed to create destination file: %w", err) - } - - _, err = io.Copy(destFile, sourceFile) - sourceFile.Close() - if err != nil { - destFile.Close() - return fmt.Errorf("failed to copy file: %w", err) - } - - // Ensure all data is written to disk - if err := destFile.Sync(); err != nil { - destFile.Close() - return fmt.Errorf("failed to sync file: %w", err) - } - destFile.Close() - - // On Windows, file may still be locked by antivirus or system - // Wait a bit and retry verification - maxRetries := 3 - retryDelay := 500 * time.Millisecond - - var verifyErr error - for i := 0; i < maxRetries; i++ { - if i > 0 { - time.Sleep(retryDelay) - } - - cmd := exec.Command(ffmpegPath, "-version") - // Hide console window on Windows - setHideWindow(cmd) - verifyErr = cmd.Run() - if verifyErr == nil { - break - } - } - - if verifyErr != nil { - return fmt.Errorf("file copied but ffmpeg verification failed after %d attempts: %w", maxRetries, verifyErr) - } - - fmt.Printf("[FFmpeg] Successfully installed from: %s\n", filePath) - return nil -} diff --git a/frontend/components.json b/frontend/components.json index 2b0833f..fd90f3b 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -18,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} + "registries": { + "@lucide-animated": "https://lucide-animated.com/r/{name}.json" + } } diff --git a/frontend/package.json b/frontend/package.json index daec558..eb4b455 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,18 +12,14 @@ "generate-icon": "node scripts/generate-icon.js" }, "dependencies": { - "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", @@ -31,6 +27,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "motion": "^12.12.1", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index ae2459d..ecc7f45 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -07ce84ccf0f1355c8d93ec1d8bd235ea \ No newline at end of file +1433fe06229ac4658f3b2b15700c7866 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b7c2d1f..20a18bd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@radix-ui/react-alert-dialog': - specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-checkbox': specifier: ^1.3.3 version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -26,12 +23,6 @@ importers: '@radix-ui/react-progress': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-radio-group': - specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-scroll-area': - specifier: ^1.2.10 - version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-select': specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -41,9 +32,6 @@ importers: '@radix-ui/react-switch': specifier: ^1.2.6 version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-tabs': - specifier: ^1.1.13 - version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-toggle': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -65,6 +53,9 @@ importers: lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) + motion: + specifier: ^12.12.1 + version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -599,19 +590,6 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - '@radix-ui/react-alert-dialog@1.1.15': - resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} - 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-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -861,19 +839,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.3.8': - resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} - 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-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -887,19 +852,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-scroll-area@1.2.10': - resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} - 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-select@2.2.6': resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} peerDependencies: @@ -944,19 +896,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tabs@1.1.13': - resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} - 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-toggle-group@1.1.11': resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} peerDependencies: @@ -1586,6 +1525,20 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + framer-motion@12.23.26: + resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1783,6 +1736,26 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + + motion@12.23.26: + resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2469,20 +2442,6 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2718,24 +2677,6 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2753,23 +2694,6 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/number': 1.1.1 @@ -2828,22 +2752,6 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -3467,6 +3375,15 @@ snapshots: flatted@3.3.3: {} + framer-motion@12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + motion-dom: 12.23.23 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + fsevents@2.3.3: optional: true @@ -3613,6 +3530,20 @@ snapshots: dependencies: brace-expansion: 2.0.2 + motion-dom@12.23.23: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + motion@12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + framer-motion: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + ms@2.1.3: {} nanoid@3.3.11: {} diff --git a/frontend/src/components/AudioConverterPage.tsx b/frontend/src/components/AudioConverterPage.tsx index 32b122a..81a876e 100644 --- a/frontend/src/components/AudioConverterPage.tsx +++ b/frontend/src/components/AudioConverterPage.tsx @@ -19,7 +19,6 @@ import { Spinner } from "@/components/ui/spinner"; import { IsFFmpegInstalled, DownloadFFmpeg, - InstallFFmpegFromFile, ConvertAudio, SelectAudioFiles, } from "../../wailsjs/go/main/App"; @@ -120,7 +119,6 @@ export function AudioConverterPage() { }); const [converting, setConverting] = useState(false); const [isDragging, setIsDragging] = useState(false); - const [isDraggingFFmpeg, setIsDraggingFFmpeg] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); // Helper function to save state to sessionStorage @@ -218,61 +216,6 @@ export function AudioConverterPage() { } }; - const handleFFmpegFileDrop = useCallback( - async (_x: number, _y: number, paths: string[]) => { - setIsDraggingFFmpeg(false); - - if (paths.length === 0) return; - - // Only process the first file - const filePath = paths[0]; - const fileName = filePath.split(/[/\\]/).pop()?.toLowerCase() || ""; - - // Check if it's likely an ffmpeg executable - if (!fileName.includes("ffmpeg")) { - toast.error("Invalid File", { - description: "Please drop an FFmpeg executable file", - }); - return; - } - - setInstallingFfmpeg(true); - try { - const result = await InstallFFmpegFromFile(filePath); - if (result.success) { - toast.success("FFmpeg Installed", { - description: "FFmpeg has been installed successfully from file", - }); - setFfmpegInstalled(true); - } else { - toast.error("Installation Failed", { - description: result.error || "Failed to install FFmpeg", - }); - } - } catch (err) { - toast.error("Installation Failed", { - description: err instanceof Error ? err.message : "Unknown error", - }); - } finally { - setInstallingFfmpeg(false); - } - }, - [] - ); - - useEffect(() => { - if (ffmpegInstalled === false) { - // Set up drag and drop for FFmpeg installation - OnFileDrop((x, y, paths) => { - handleFFmpegFileDrop(x, y, paths); - }, true); - - return () => { - OnFileDropOff(); - }; - } - }, [ffmpegInstalled, handleFFmpegFileDrop]); - const handleSelectFiles = async () => { try { const selectedFiles = await SelectAudioFiles(); @@ -480,35 +423,13 @@ export function AudioConverterPage() {
{ - e.preventDefault(); - setIsDraggingFFmpeg(true); - }} - onDragLeave={(e) => { - e.preventDefault(); - setIsDraggingFFmpeg(false); - }} - onDrop={(e) => { - e.preventDefault(); - setIsDraggingFFmpeg(false); - }} - style={{ "--wails-drop-target": "drop" } as React.CSSProperties} + } border-muted-foreground/30`} >
- +
-

- FFmpeg is required to convert audio files. -

- {isDraggingFFmpeg - ? "Drop your FFmpeg executable here" - : "Drag and drop your FFmpeg executable here, or click the button below to download automatically."} + FFmpeg is required to convert audio files

+ + + + {/* Preview Dialog */} @@ -712,7 +702,6 @@ export function FileManagerPage() { ) : ( <> - Rename {previewData.filter((p) => !p.error).length} File(s) )} @@ -792,7 +781,7 @@ export function FileManagerPage() { {/* FFprobe Install Dialog */} - + FFprobe Required diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 9e4e2cd..b75a677 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -13,15 +13,13 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { FolderOpen, Save, RotateCcw, Info } from "lucide-react"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Switch } from "@/components/ui/switch"; import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, applyFont, FONT_OPTIONS, FOLDER_PRESETS, FILENAME_PRESETS, TEMPLATE_VARIABLES, type Settings as SettingsType, type FontFamily, type FolderPreset, type FilenamePreset } from "@/lib/settings"; import { themes, applyTheme } from "@/lib/themes"; @@ -421,20 +419,20 @@ export function SettingsPage() {
{/* Reset Confirmation Dialog */} - - - - Reset to Default? - + + + + Reset to Default? + This will reset all settings to their default values. Your custom configurations will be lost. - - - - Cancel - Reset - - - + + + + + + + + ); } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 615e270..0b9357d 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,4 +1,11 @@ -import { Home, Settings, Bug, Activity, FileMusic, FilePen, LayoutGrid, Coffee, Github } from "lucide-react"; +import { FileMusic, FilePen } from "lucide-react"; +import { HomeIcon } from "@/components/ui/home"; +import { SettingsIcon } from "@/components/ui/settings"; +import { ActivityIcon } from "@/components/ui/activity"; +import { TerminalIcon } from "@/components/ui/terminal"; +import { GithubIcon } from "@/components/ui/github"; +import { BlocksIcon } from "@/components/ui/blocks"; +import { CoffeeIcon } from "@/components/ui/coffee"; import { Tooltip, TooltipContent, @@ -15,35 +22,110 @@ interface SidebarProps { } export function Sidebar({ currentPage, onPageChange }: SidebarProps) { - const navItems = [ - { id: "main" as PageType, icon: Home, label: "Home" }, - { id: "settings" as PageType, icon: Settings, label: "Settings" }, - { id: "audio-analysis" as PageType, icon: Activity, label: "Audio Quality Analyzer" }, - { id: "audio-converter" as PageType, icon: FileMusic, label: "Audio Converter" }, - { id: "file-manager" as PageType, icon: FilePen, label: "File Manager" }, - { id: "debug" as PageType, icon: Bug, label: "Debug Logs" }, - ]; - return (
- {navItems.map((item) => ( - - - - - -

{item.label}

-
-
- ))} + {/* Home */} + + + + + +

Home

+
+
+ + {/* Settings */} + + + + + +

Settings

+
+
+ + {/* Audio Analysis */} + + + + + +

Audio Quality Analyzer

+
+
+ + {/* Audio Converter - using lucide icon (no animated version) */} + + + + + +

Audio Converter

+
+
+ + {/* File Manager - using lucide icon (no animated version) */} + + + + + +

File Manager

+
+
+ + {/* Debug */} + + + + + +

Debug Logs

+
+
{/* Bottom icons */} @@ -56,7 +138,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) { className="h-10 w-10" onClick={() => openExternal("https://github.com/afkarxyz/SpotiFLAC/issues")} > - + @@ -71,7 +153,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) { className="h-10 w-10" onClick={() => openExternal("https://exyezed.cc/")} > - + @@ -86,7 +168,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) { className="h-10 w-10" onClick={() => openExternal("https://ko-fi.com/afkarxyz")} > - + @@ -96,4 +178,4 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
); -} \ No newline at end of file +} diff --git a/frontend/src/components/ui/activity.tsx b/frontend/src/components/ui/activity.tsx new file mode 100644 index 0000000..40f8443 --- /dev/null +++ b/frontend/src/components/ui/activity.tsx @@ -0,0 +1,104 @@ +'use client'; + +import type { Variants } from 'motion/react'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface ActivityIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface ActivityIconProps extends HTMLAttributes { + size?: number; +} + +const PATH_VARIANTS: Variants = { + normal: { + pathLength: 1, + opacity: 1, + pathOffset: 0, + }, + animate: { + pathLength: [0, 1], + opacity: [0, 1], + pathOffset: [1, 0], + transition: { + duration: 0.8, + ease: 'easeInOut', + }, + }, +}; + +const ActivityIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } else { + onMouseEnter?.(e); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + +
+ ); + } +); + +ActivityIcon.displayName = 'ActivityIcon'; + +export { ActivityIcon }; diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx deleted file mode 100644 index 0863e40..0000000 --- a/frontend/src/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -function AlertDialog({ - ...props -}: React.ComponentProps) { - return -} - -function AlertDialogTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - - ) -} - -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogAction({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogCancel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/frontend/src/components/ui/blocks.tsx b/frontend/src/components/ui/blocks.tsx new file mode 100644 index 0000000..95d6ecb --- /dev/null +++ b/frontend/src/components/ui/blocks.tsx @@ -0,0 +1,92 @@ +'use client'; + +import type { Variants } from 'motion/react'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface BlocksIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface BlocksIconProps extends HTMLAttributes { + size?: number; +} + +const VARIANTS: Variants = { + normal: { translateX: 0, translateY: 0 }, + animate: { translateX: -4, translateY: 4 }, +}; + +const BlocksIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } else { + onMouseEnter?.(e); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + +
+ ); + } +); + +BlocksIcon.displayName = 'BlocksIcon'; + +export { BlocksIcon }; diff --git a/frontend/src/components/ui/coffee.tsx b/frontend/src/components/ui/coffee.tsx new file mode 100644 index 0000000..cc1ecc3 --- /dev/null +++ b/frontend/src/components/ui/coffee.tsx @@ -0,0 +1,118 @@ +'use client'; + +import type { Variants } from 'motion/react'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface CoffeeIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface CoffeeIconProps extends HTMLAttributes { + size?: number; +} + +const PATH_VARIANTS: Variants = { + normal: { + y: 0, + opacity: 1, + }, + animate: (custom: number) => ({ + y: -3, + opacity: [0, 1, 0], + transition: { + repeat: Infinity, + duration: 1.5, + ease: 'easeInOut', + delay: 0.2 * custom, + }, + }), +}; + +const CoffeeIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } else { + onMouseEnter?.(e); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + + + +
+ ); + } +); + +CoffeeIcon.displayName = 'CoffeeIcon'; + +export { CoffeeIcon }; diff --git a/frontend/src/components/ui/github.tsx b/frontend/src/components/ui/github.tsx new file mode 100644 index 0000000..6936492 --- /dev/null +++ b/frontend/src/components/ui/github.tsx @@ -0,0 +1,149 @@ +'use client'; + +import type { Variants } from 'motion/react'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface GithubIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface GithubIconProps extends HTMLAttributes { + size?: number; +} + +const BODY_VARIANTS: Variants = { + normal: { + opacity: 1, + pathLength: 1, + scale: 1, + transition: { + duration: 0.3, + }, + }, + animate: { + opacity: [0, 1], + pathLength: [0, 1], + scale: [0.9, 1], + transition: { + duration: 0.4, + }, + }, +}; + +const TAIL_VARIANTS: Variants = { + normal: { + pathLength: 1, + rotate: 0, + transition: { + duration: 0.3, + }, + }, + draw: { + pathLength: [0, 1], + rotate: 0, + transition: { + duration: 0.5, + }, + }, + wag: { + pathLength: 1, + rotate: [0, -15, 15, -10, 10, -5, 5], + transition: { + duration: 2.5, + ease: 'easeInOut', + repeat: Infinity, + }, + }, +}; + +const GithubIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const bodyControls = useAnimation(); + const tailControls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: async () => { + bodyControls.start('animate'); + await tailControls.start('draw'); + tailControls.start('wag'); + }, + stopAnimation: () => { + bodyControls.start('normal'); + tailControls.start('normal'); + }, + }; + }); + + const handleMouseEnter = useCallback( + async (e: React.MouseEvent) => { + if (!isControlledRef.current) { + bodyControls.start('animate'); + await tailControls.start('draw'); + tailControls.start('wag'); + } else { + onMouseEnter?.(e); + } + }, + [bodyControls, onMouseEnter, tailControls] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + bodyControls.start('normal'); + tailControls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [bodyControls, tailControls, onMouseLeave] + ); + + return ( +
+ + + + +
+ ); + } +); + +GithubIcon.displayName = 'GithubIcon'; + +export { GithubIcon }; diff --git a/frontend/src/components/ui/home.tsx b/frontend/src/components/ui/home.tsx new file mode 100644 index 0000000..203ac9b --- /dev/null +++ b/frontend/src/components/ui/home.tsx @@ -0,0 +1,103 @@ +'use client'; + +import type { Transition, Variants } from 'motion/react'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface HomeIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface HomeIconProps extends HTMLAttributes { + size?: number; +} + +const DEFAULT_TRANSITION: Transition = { + duration: 0.6, + opacity: { duration: 0.2 }, +}; + +const PATH_VARIANTS: Variants = { + normal: { + pathLength: 1, + opacity: 1, + }, + animate: { + opacity: [0, 1], + pathLength: [0, 1], + }, +}; + +const HomeIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } else { + onMouseEnter?.(e); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [controls, onMouseLeave] + ); + return ( +
+ + + + +
+ ); + } +); + +HomeIcon.displayName = 'HomeIcon'; + +export { HomeIcon }; diff --git a/frontend/src/components/ui/radio-group.tsx b/frontend/src/components/ui/radio-group.tsx deleted file mode 100644 index 4c76966..0000000 --- a/frontend/src/components/ui/radio-group.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client" - -import * as React from "react" -import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" -import { CircleIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function RadioGroup({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function RadioGroupItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - - - ) -} - -export { RadioGroup, RadioGroupItem } \ No newline at end of file diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx deleted file mode 100644 index cf253cf..0000000 --- a/frontend/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" - -import { cn } from "@/lib/utils" - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName - -const ScrollBar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = "vertical", ...props }, ref) => ( - - - -)) -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName - -export { ScrollArea, ScrollBar } diff --git a/frontend/src/components/ui/settings.tsx b/frontend/src/components/ui/settings.tsx new file mode 100644 index 0000000..fe055ac --- /dev/null +++ b/frontend/src/components/ui/settings.tsx @@ -0,0 +1,92 @@ +'use client'; + +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface SettingsIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface SettingsIconProps extends HTMLAttributes { + size?: number; +} + +const SettingsIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } else { + onMouseEnter?.(e); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + +
+ ); + } +); + +SettingsIcon.displayName = 'SettingsIcon'; + +export { SettingsIcon }; diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx deleted file mode 100644 index 497ba5e..0000000 --- a/frontend/src/components/ui/tabs.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client" - -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" - -import { cn } from "@/lib/utils" - -function Tabs({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function TabsList({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function TabsTrigger({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function TabsContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/frontend/src/components/ui/terminal.tsx b/frontend/src/components/ui/terminal.tsx new file mode 100644 index 0000000..23a0bc2 --- /dev/null +++ b/frontend/src/components/ui/terminal.tsx @@ -0,0 +1,103 @@ +'use client'; + +import type { Variants } from 'motion/react'; +import type { HTMLAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { motion, useAnimation } from 'motion/react'; + +import { cn } from '@/lib/utils'; + +export interface TerminalIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface TerminalIconProps extends HTMLAttributes { + size?: number; +} + +const LINE_VARIANTS: Variants = { + normal: { opacity: 1 }, + animate: { + opacity: [1, 0, 1], + transition: { + duration: 0.8, + repeat: Infinity, + ease: 'linear', + }, + }, +}; + +const TerminalIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } else { + onMouseEnter?.(e); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } else { + onMouseLeave?.(e); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + +
+ ); + } +); + +TerminalIcon.displayName = 'TerminalIcon'; + +export { TerminalIcon };