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
{/* Reset Confirmation Dialog */}
-
-
-
- Reset to Default?
-
+
+
+
+ Reset to Default?
+
This will reset the rename format to "Title - Artist". Your custom format will be lost.
-
-
-
- Cancel
- Reset
-
-
-
+
+
+
+ setShowResetConfirm(false)}>Cancel
+ Reset
+
+
+
{/* 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
-
-
-
+
+
+
+ setShowResetConfirm(false)}>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) => (
-
-
- onPageChange(item.id)}
- >
-
-
-
-
- {item.label}
-
-
- ))}
+ {/* Home */}
+
+
+ onPageChange("main")}
+ >
+
+
+
+
+ Home
+
+
+
+ {/* Settings */}
+
+
+ onPageChange("settings")}
+ >
+
+
+
+
+ Settings
+
+
+
+ {/* Audio Analysis */}
+
+
+ onPageChange("audio-analysis")}
+ >
+
+
+
+
+ Audio Quality Analyzer
+
+
+
+ {/* Audio Converter - using lucide icon (no animated version) */}
+
+
+ onPageChange("audio-converter")}
+ >
+
+
+
+
+ Audio Converter
+
+
+
+ {/* File Manager - using lucide icon (no animated version) */}
+
+
+ onPageChange("file-manager")}
+ >
+
+
+
+
+ File Manager
+
+
+
+ {/* Debug */}
+
+
+ onPageChange("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 };