v6.9
This commit is contained in:
@@ -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
|
// ConvertAudioRequest represents a request to convert audio files
|
||||||
type ConvertAudioRequest struct {
|
type ConvertAudioRequest struct {
|
||||||
InputFiles []string `json:"input_files"`
|
InputFiles []string `json:"input_files"`
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ulikunitz/xz"
|
"github.com/ulikunitz/xz"
|
||||||
)
|
)
|
||||||
@@ -614,94 +613,3 @@ func GetAudioFileInfo(filePath string) (*AudioFileInfo, error) {
|
|||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
}, nil
|
}, 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,5 +18,7 @@
|
|||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"registries": {}
|
"registries": {
|
||||||
|
"@lucide-animated": "https://lucide-animated.com/r/{name}.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,18 +12,14 @@
|
|||||||
"generate-icon": "node scripts/generate-icon.js"
|
"generate-icon": "node scripts/generate-icon.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.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-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@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": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
@@ -31,6 +27,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"motion": "^12.12.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
07ce84ccf0f1355c8d93ec1d8bd235ea
|
1433fe06229ac4658f3b2b15700c7866
|
||||||
Generated
+60
-129
@@ -8,9 +8,6 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
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':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.3
|
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)
|
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':
|
'@radix-ui/react-progress':
|
||||||
specifier: ^1.1.8
|
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)
|
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':
|
'@radix-ui/react-select':
|
||||||
specifier: ^2.2.6
|
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)
|
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':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.2.6
|
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)
|
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':
|
'@radix-ui/react-toggle':
|
||||||
specifier: ^1.1.10
|
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)
|
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:
|
lucide-react:
|
||||||
specifier: ^0.562.0
|
specifier: ^0.562.0
|
||||||
version: 0.562.0(react@19.2.3)
|
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:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
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':
|
'@radix-ui/primitive@1.1.3':
|
||||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
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':
|
'@radix-ui/react-arrow@1.1.7':
|
||||||
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -861,19 +839,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-roving-focus@1.1.11':
|
||||||
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -887,19 +852,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-select@2.2.6':
|
||||||
resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
|
resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -944,19 +896,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-toggle-group@1.1.11':
|
||||||
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
|
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1586,6 +1525,20 @@ packages:
|
|||||||
flatted@3.3.3:
|
flatted@3.3.3:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
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:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -1783,6 +1736,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -2469,20 +2442,6 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@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)':
|
'@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:
|
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)
|
'@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': 19.2.7
|
||||||
'@types/react-dom': 19.2.3(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -2753,23 +2694,6 @@ snapshots:
|
|||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
'@types/react-dom': 19.2.3(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.1
|
'@radix-ui/number': 1.1.1
|
||||||
@@ -2828,22 +2752,6 @@ snapshots:
|
|||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
'@types/react-dom': 19.2.3(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3467,6 +3375,15 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.3.3: {}
|
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:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3613,6 +3530,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
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: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { Spinner } from "@/components/ui/spinner";
|
|||||||
import {
|
import {
|
||||||
IsFFmpegInstalled,
|
IsFFmpegInstalled,
|
||||||
DownloadFFmpeg,
|
DownloadFFmpeg,
|
||||||
InstallFFmpegFromFile,
|
|
||||||
ConvertAudio,
|
ConvertAudio,
|
||||||
SelectAudioFiles,
|
SelectAudioFiles,
|
||||||
} from "../../wailsjs/go/main/App";
|
} from "../../wailsjs/go/main/App";
|
||||||
@@ -120,7 +119,6 @@ export function AudioConverterPage() {
|
|||||||
});
|
});
|
||||||
const [converting, setConverting] = useState(false);
|
const [converting, setConverting] = useState(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isDraggingFFmpeg, setIsDraggingFFmpeg] = useState(false);
|
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
// Helper function to save state to sessionStorage
|
// 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 () => {
|
const handleSelectFiles = async () => {
|
||||||
try {
|
try {
|
||||||
const selectedFiles = await SelectAudioFiles();
|
const selectedFiles = await SelectAudioFiles();
|
||||||
@@ -480,35 +423,13 @@ export function AudioConverterPage() {
|
|||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center justify-center border-2 border-dashed rounded-lg transition-all ${
|
className={`flex flex-col items-center justify-center border-2 border-dashed rounded-lg transition-all ${
|
||||||
isFullscreen ? "flex-1 min-h-[400px]" : "h-[400px]"
|
isFullscreen ? "flex-1 min-h-[400px]" : "h-[400px]"
|
||||||
} ${
|
} border-muted-foreground/30`}
|
||||||
isDraggingFFmpeg
|
|
||||||
? "border-primary bg-primary/10"
|
|
||||||
: "border-muted-foreground/30"
|
|
||||||
}`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDraggingFFmpeg(true);
|
|
||||||
}}
|
|
||||||
onDragLeave={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDraggingFFmpeg(false);
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDraggingFFmpeg(false);
|
|
||||||
}}
|
|
||||||
style={{ "--wails-drop-target": "drop" } as React.CSSProperties}
|
|
||||||
>
|
>
|
||||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||||
<Download className="h-8 w-8 text-muted-foreground" />
|
<Download className="h-8 w-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-2 text-center">
|
|
||||||
FFmpeg is required to convert audio files.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4 text-center">
|
<p className="text-sm text-muted-foreground mb-4 text-center">
|
||||||
{isDraggingFFmpeg
|
FFmpeg is required to convert audio files
|
||||||
? "Drop your FFmpeg executable here"
|
|
||||||
: "Drag and drop your FFmpeg executable here, or click the button below to download automatically."}
|
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleInstallFfmpeg}
|
onClick={handleInstallFfmpeg}
|
||||||
|
|||||||
@@ -52,16 +52,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
|
|
||||||
interface FileNode {
|
interface FileNode {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -641,20 +631,20 @@ export function FileManagerPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reset Confirmation Dialog */}
|
{/* Reset Confirmation Dialog */}
|
||||||
<AlertDialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
|
<Dialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-w-md [&>button]:hidden">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Reset to Default?</AlertDialogTitle>
|
<DialogTitle>Reset to Default?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This will reset the rename format to "Title - Artist". Your custom format will be lost.
|
This will reset the rename format to "Title - Artist". Your custom format will be lost.
|
||||||
</AlertDialogDescription>
|
</DialogDescription>
|
||||||
</AlertDialogHeader>
|
</DialogHeader>
|
||||||
<AlertDialogFooter>
|
<DialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<Button variant="outline" onClick={() => setShowResetConfirm(false)}>Cancel</Button>
|
||||||
<AlertDialogAction onClick={resetToDefault}>Reset</AlertDialogAction>
|
<Button onClick={resetToDefault}>Reset</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Preview Dialog */}
|
{/* Preview Dialog */}
|
||||||
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||||
@@ -712,7 +702,6 @@ export function FileManagerPage() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
Rename {previewData.filter((p) => !p.error).length} File(s)
|
Rename {previewData.filter((p) => !p.error).length} File(s)
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -792,7 +781,7 @@ export function FileManagerPage() {
|
|||||||
|
|
||||||
{/* FFprobe Install Dialog */}
|
{/* FFprobe Install Dialog */}
|
||||||
<Dialog open={showFFprobeDialog} onOpenChange={setShowFFprobeDialog}>
|
<Dialog open={showFFprobeDialog} onOpenChange={setShowFFprobeDialog}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md [&>button]:hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>FFprobe Required</DialogTitle>
|
<DialogTitle>FFprobe Required</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -13,15 +13,13 @@ import {
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { FolderOpen, Save, RotateCcw, Info } from "lucide-react";
|
import { FolderOpen, Save, RotateCcw, Info } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
Dialog,
|
||||||
AlertDialogAction,
|
DialogContent,
|
||||||
AlertDialogCancel,
|
DialogDescription,
|
||||||
AlertDialogContent,
|
DialogFooter,
|
||||||
AlertDialogDescription,
|
DialogHeader,
|
||||||
AlertDialogFooter,
|
DialogTitle,
|
||||||
AlertDialogHeader,
|
} from "@/components/ui/dialog";
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
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 { 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";
|
import { themes, applyTheme } from "@/lib/themes";
|
||||||
@@ -421,20 +419,20 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reset Confirmation Dialog */}
|
{/* Reset Confirmation Dialog */}
|
||||||
<AlertDialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
|
<Dialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
|
||||||
<AlertDialogContent>
|
<DialogContent className="max-w-md [&>button]:hidden">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Reset to Default?</AlertDialogTitle>
|
<DialogTitle>Reset to Default?</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
This will reset all settings to their default values. Your custom configurations will be lost.
|
This will reset all settings to their default values. Your custom configurations will be lost.
|
||||||
</AlertDialogDescription>
|
</DialogDescription>
|
||||||
</AlertDialogHeader>
|
</DialogHeader>
|
||||||
<AlertDialogFooter>
|
<DialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<Button variant="outline" onClick={() => setShowResetConfirm(false)}>Cancel</Button>
|
||||||
<AlertDialogAction onClick={handleReset}>Reset</AlertDialogAction>
|
<Button onClick={handleReset}>Reset</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -15,35 +22,110 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ currentPage, onPageChange }: 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 (
|
return (
|
||||||
<div className="fixed left-0 top-0 h-full w-14 bg-card border-r border-border flex flex-col items-center py-14 z-30">
|
<div className="fixed left-0 top-0 h-full w-14 bg-card border-r border-border flex flex-col items-center py-14 z-30">
|
||||||
<div className="flex flex-col gap-2 flex-1">
|
<div className="flex flex-col gap-2 flex-1">
|
||||||
{navItems.map((item) => (
|
{/* Home */}
|
||||||
<Tooltip key={item.id} delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={currentPage === item.id ? "secondary" : "ghost"}
|
variant={currentPage === "main" ? "secondary" : "ghost"}
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-10 w-10"
|
className="h-10 w-10"
|
||||||
onClick={() => onPageChange(item.id)}
|
onClick={() => onPageChange("main")}
|
||||||
>
|
>
|
||||||
<item.icon className="h-5 w-5" />
|
<HomeIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>{item.label}</p>
|
<p>Home</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
|
||||||
|
{/* Settings */}
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentPage === "settings" ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
onClick={() => onPageChange("settings")}
|
||||||
|
>
|
||||||
|
<SettingsIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Settings</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Audio Analysis */}
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentPage === "audio-analysis" ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
onClick={() => onPageChange("audio-analysis")}
|
||||||
|
>
|
||||||
|
<ActivityIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Audio Quality Analyzer</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Audio Converter - using lucide icon (no animated version) */}
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentPage === "audio-converter" ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
onClick={() => onPageChange("audio-converter")}
|
||||||
|
>
|
||||||
|
<FileMusic className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Audio Converter</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* File Manager - using lucide icon (no animated version) */}
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentPage === "file-manager" ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
onClick={() => onPageChange("file-manager")}
|
||||||
|
>
|
||||||
|
<FilePen className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>File Manager</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Debug */}
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={currentPage === "debug" ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
onClick={() => onPageChange("debug")}
|
||||||
|
>
|
||||||
|
<TerminalIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Debug Logs</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom icons */}
|
{/* Bottom icons */}
|
||||||
@@ -56,7 +138,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
|
|||||||
className="h-10 w-10"
|
className="h-10 w-10"
|
||||||
onClick={() => openExternal("https://github.com/afkarxyz/SpotiFLAC/issues")}
|
onClick={() => openExternal("https://github.com/afkarxyz/SpotiFLAC/issues")}
|
||||||
>
|
>
|
||||||
<Github className="h-5 w-5" />
|
<GithubIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
@@ -71,7 +153,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
|
|||||||
className="h-10 w-10"
|
className="h-10 w-10"
|
||||||
onClick={() => openExternal("https://exyezed.cc/")}
|
onClick={() => openExternal("https://exyezed.cc/")}
|
||||||
>
|
>
|
||||||
<LayoutGrid className="h-5 w-5" />
|
<BlocksIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
@@ -86,7 +168,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
|
|||||||
className="h-10 w-10"
|
className="h-10 w-10"
|
||||||
onClick={() => openExternal("https://ko-fi.com/afkarxyz")}
|
onClick={() => openExternal("https://ko-fi.com/afkarxyz")}
|
||||||
>
|
>
|
||||||
<Coffee className="h-5 w-5" />
|
<CoffeeIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
|
|||||||
@@ -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<HTMLDivElement> {
|
||||||
|
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<ActivityIconHandle, ActivityIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('animate');
|
||||||
|
} else {
|
||||||
|
onMouseEnter?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseLeave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<motion.path
|
||||||
|
d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
|
||||||
|
variants={PATH_VARIANTS}
|
||||||
|
animate={controls}
|
||||||
|
initial="normal"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ActivityIcon.displayName = 'ActivityIcon';
|
||||||
|
|
||||||
|
export { ActivityIcon };
|
||||||
@@ -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<typeof AlertDialogPrimitive.Root>) {
|
|
||||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Overlay
|
|
||||||
data-slot="alert-dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
data-slot="alert-dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogHeader({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="alert-dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogFooter({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="alert-dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Title
|
|
||||||
data-slot="alert-dialog-title"
|
|
||||||
className={cn("text-lg font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Description
|
|
||||||
data-slot="alert-dialog-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogAction({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Action
|
|
||||||
className={cn(buttonVariants(), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogCancel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Cancel
|
|
||||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
}
|
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VARIANTS: Variants = {
|
||||||
|
normal: { translateX: 0, translateY: 0 },
|
||||||
|
animate: { translateX: -4, translateY: 4 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlocksIcon = forwardRef<BlocksIconHandle, BlocksIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('animate');
|
||||||
|
} else {
|
||||||
|
onMouseEnter?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseLeave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3" />
|
||||||
|
<motion.path
|
||||||
|
d="M14 3h7v7h-7z"
|
||||||
|
variants={VARIANTS}
|
||||||
|
animate={controls}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
BlocksIcon.displayName = 'BlocksIcon';
|
||||||
|
|
||||||
|
export { BlocksIcon };
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
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<CoffeeIconHandle, CoffeeIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('animate');
|
||||||
|
} else {
|
||||||
|
onMouseEnter?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseLeave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ overflow: 'visible' }}
|
||||||
|
>
|
||||||
|
<motion.path
|
||||||
|
d="M10 2v2"
|
||||||
|
animate={controls}
|
||||||
|
variants={PATH_VARIANTS}
|
||||||
|
custom={0.2}
|
||||||
|
/>
|
||||||
|
<motion.path
|
||||||
|
d="M14 2v2"
|
||||||
|
animate={controls}
|
||||||
|
variants={PATH_VARIANTS}
|
||||||
|
custom={0.4}
|
||||||
|
/>
|
||||||
|
<motion.path
|
||||||
|
d="M6 2v2"
|
||||||
|
animate={controls}
|
||||||
|
variants={PATH_VARIANTS}
|
||||||
|
custom={0}
|
||||||
|
/>
|
||||||
|
<path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CoffeeIcon.displayName = 'CoffeeIcon';
|
||||||
|
|
||||||
|
export { CoffeeIcon };
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
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<GithubIconHandle, GithubIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
bodyControls.start('normal');
|
||||||
|
tailControls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[bodyControls, tailControls, onMouseLeave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<motion.path
|
||||||
|
variants={BODY_VARIANTS}
|
||||||
|
initial="normal"
|
||||||
|
animate={bodyControls}
|
||||||
|
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
|
||||||
|
/>
|
||||||
|
<motion.path
|
||||||
|
variants={TAIL_VARIANTS}
|
||||||
|
initial="normal"
|
||||||
|
animate={tailControls}
|
||||||
|
d="M9 18c-4.51 2-5-2-7-2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
GithubIcon.displayName = 'GithubIcon';
|
||||||
|
|
||||||
|
export { GithubIcon };
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
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<HomeIconHandle, HomeIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('animate');
|
||||||
|
} else {
|
||||||
|
onMouseEnter?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseLeave]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<motion.path
|
||||||
|
d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"
|
||||||
|
variants={PATH_VARIANTS}
|
||||||
|
transition={DEFAULT_TRANSITION}
|
||||||
|
animate={controls}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
HomeIcon.displayName = 'HomeIcon';
|
||||||
|
|
||||||
|
export { HomeIcon };
|
||||||
@@ -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<typeof RadioGroupPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Root
|
|
||||||
data-slot="radio-group"
|
|
||||||
className={cn("grid gap-3", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioGroupItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Item
|
|
||||||
data-slot="radio-group-item"
|
|
||||||
className={cn(
|
|
||||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<RadioGroupPrimitive.Indicator
|
|
||||||
data-slot="radio-group-indicator"
|
|
||||||
className="relative flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
|
||||||
</RadioGroupPrimitive.Indicator>
|
|
||||||
</RadioGroupPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
|
||||||
@@ -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<typeof ScrollAreaPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative overflow-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
||||||
{children}
|
|
||||||
</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
))
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
ref={ref}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"flex touch-none select-none transition-colors",
|
|
||||||
orientation === "vertical" &&
|
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
||||||
orientation === "horizontal" &&
|
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
))
|
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsIcon = forwardRef<SettingsIconHandle, SettingsIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('animate');
|
||||||
|
} else {
|
||||||
|
onMouseEnter?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseLeave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<motion.svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
transition={{ type: 'spring', stiffness: 50, damping: 10 }}
|
||||||
|
variants={{
|
||||||
|
normal: {
|
||||||
|
rotate: 0,
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
rotate: 180,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
animate={controls}
|
||||||
|
>
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</motion.svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SettingsIcon.displayName = 'SettingsIcon';
|
||||||
|
|
||||||
|
export { SettingsIcon };
|
||||||
@@ -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<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
data-slot="tabs-list"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
data-slot="tabs-trigger"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
||||||
@@ -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<HTMLDivElement> {
|
||||||
|
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<TerminalIconHandle, TerminalIconProps>(
|
||||||
|
({ 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<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('animate');
|
||||||
|
} else {
|
||||||
|
onMouseEnter?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
controls.start('normal');
|
||||||
|
} else {
|
||||||
|
onMouseLeave?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controls, onMouseLeave]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(className)}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="4 17 10 11 4 5" />
|
||||||
|
<motion.line
|
||||||
|
x1="12"
|
||||||
|
x2="20"
|
||||||
|
y1="19"
|
||||||
|
y2="19"
|
||||||
|
variants={LINE_VARIANTS}
|
||||||
|
animate={controls}
|
||||||
|
initial="normal"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TerminalIcon.displayName = 'TerminalIcon';
|
||||||
|
|
||||||
|
export { TerminalIcon };
|
||||||
Reference in New Issue
Block a user