Compare commits
70 Commits
2d82e3404d
...
main
BIN
.github/images/completed-downloads.png
vendored
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 139 KiB |
BIN
.github/images/downloader.png
vendored
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 135 KiB |
BIN
.github/images/flathub/completed-downloads.png
vendored
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
.github/images/flathub/downloader.png
vendored
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
.github/images/flathub/ongoing-downloads.png
vendored
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
.github/images/flathub/settings.png
vendored
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
.github/images/ongoing-downloads.png
vendored
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 78 KiB |
BIN
.github/images/settings.png
vendored
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 86 KiB |
30
CHANGELOG.md
@@ -1,27 +1,15 @@
|
|||||||
### ✨ Changelog
|
### ✨ Changelog
|
||||||
|
|
||||||
- Added support for selective-batch/full-playlist download
|
- Added support for linux Flatpak (some features are unavailable on Flatpak build due to it's strict sandboxing, check 'Settings > Info > Health Check' section for more info)
|
||||||
- Added support for selecting multiple audio streams on combine mode
|
- Other minor fixes and improvements
|
||||||
- Added support for embedding original auto-generated subtitles
|
|
||||||
- Added option to crop thubnails to square (1:1) before embedding
|
|
||||||
- Added 'errored' download state (to better identify errored downloads, which you can retry later)
|
|
||||||
- Added app interface color scheme options on appearance settings
|
|
||||||
- Added app info page under settings
|
|
||||||
- Added copy/clear log buttons in log viewer
|
|
||||||
- Added sponsorblock 'hook' category
|
|
||||||
- Fixed sidebar state not persisting on app re-start
|
|
||||||
- Fixed linux native (deb/rpm) installation downloading appimage update
|
|
||||||
- Bumped up shadcn/ui to v3.5 and lots of under the hood ui improvements
|
|
||||||
- Optimized database and backend performance
|
|
||||||
- Lots of other fixes and improvements
|
|
||||||
|
|
||||||
### 📝 Notes
|
### 📝 Notes
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> This update introduces few breaking changes! Users are adviced to complete/cancel all paused downloads before updating to this version, otherwise paused downloads may not resume properly or re-start from the begining.
|
> Users are always adviced to complete/cancel all paused downloads before updating to a newer version, otherwise paused downloads may not resume properly and re-start from the begining.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Linux users make sure `yt-dlp` and `deno` is not installed in your distro (otherwise you will get package installation conflict). Don't worry, You can still use yt-dlp cli as before (the only difference is that now it will be installed and auto-updated by neo-dlp, which You can also disable from neo-dlp Settings if you don't want to auto-update yt-dlp)
|
> Linux users make sure `yt-dlp`, `aria2c` and `deno` is not installed in your distro (otherwise you will get package installation conflict). Don't worry, You can still use yt-dlp cli as before (the only difference is that now it will be installed and auto-updated by neodlp, which You can also disable from neodlp Settings if you don't want to auto-update yt-dlp) (ignore this if you are installing AppImage/Flatpak)
|
||||||
|
|
||||||
> This is an Un-Signed Build (Windows doesn't trust this Certificate so, it may flag this as malicious software, in that case, disable Windows SmartScreen and Defender, install it, and then re-enable them)
|
> This is an Un-Signed Build (Windows doesn't trust this Certificate so, it may flag this as malicious software, in that case, disable Windows SmartScreen and Defender, install it, and then re-enable them)
|
||||||
|
|
||||||
@@ -29,13 +17,13 @@
|
|||||||
|
|
||||||
### 📦 Shipped Binaries
|
### 📦 Shipped Binaries
|
||||||
|
|
||||||
| yt-dlp (updateable) | ffmpeg | ffprobe | aria2c | deno |
|
| yt-dlp (updateable) | ffmpeg | ffprobe | aria2c | deno | bgutil-pot-rs |
|
||||||
| :---- | :---- | :---- | :---- | :---- |
|
| :---- | :---- | :---- | :---- | :---- | :---- |
|
||||||
| v2026.01.19.233146 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.6.5 |
|
| v2026.03.03.162408 (nightly) | v7.1.1 | v7.1.1 | v1.37.0 | v2.7.4 | v0.7.2-1.3.0 |
|
||||||
|
|
||||||
> ‼️ Linux builds (deb, rpm) does not ships with `ffmpeg` and `ffprobe` (though it will be auto installed as a dependency by your package manager, if you are on fedora make sure to [enable rpmfusion free+nonfree repos](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) before installing the rpm package)
|
> ‼️ Linux builds (deb, rpm) does not ships with `ffmpeg` and `ffprobe` (though it will be auto installed as a dependency by your package manager, if you are on fedora make sure to [enable rpmfusion free+nonfree repos](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) before installing the rpm package)
|
||||||
|
|
||||||
> ‼️ MacOS builds (dmg, app) does not ships with `aria2c`, If you want to use [aria2](https://formulae.brew.sh/formula/aria2) install it via [homebrew](https://brew.sh)
|
> ‼️ MacOS builds (dmg, app) does not ships with `aria2c`, If you want to use [aria2](https://formulae.brew.sh/formula/aria2) install it via [homebrew](https://brew.sh) (though it will be auto installed as a dependency if you install neodlp via homebrew)
|
||||||
|
|
||||||
### ⬇️ Download Section
|
### ⬇️ Download Section
|
||||||
|
|
||||||
@@ -48,7 +36,7 @@
|
|||||||
|
|
||||||
> 🪟 Windows x86_64 binary also works on ARM64 (Windows on ARM) devices with emulation (Not planning to release native Windows ARM64 build anytime soon as, x86_64 one works fine on ARM64 without noticeable performance impact)
|
> 🪟 Windows x86_64 binary also works on ARM64 (Windows on ARM) devices with emulation (Not planning to release native Windows ARM64 build anytime soon as, x86_64 one works fine on ARM64 without noticeable performance impact)
|
||||||
|
|
||||||
> 🚫 Linux AppImage builds are experimental and does not support neodlp's browser intergration features due to it's sandboxed nature. Also, don't run the AppImage with portable (.home, .config) folders, it will break things (it is highly recommended to use native [deb, rpm, AUR] builds if possible for the full experiance, otherwise AppImages are good for trying out NeoDLP without installing)
|
> 🚫 Linux AppImage builds are experimental and does not support neodlp's browser intergration features due to it's limitations. Also, don't run the AppImage with portable (.home, .config) folders, it will break things (it is highly recommended to use native [deb, rpm, AUR] builds if possible for the full experiance, otherwise AppImages are good for trying out NeoDLP without installing)
|
||||||
|
|
||||||
> ⚠️ MacOS ARM64 binary downloads are experimental and may not open on Apple Silicon Macs if downloaded from browser (You will get 'Damaged File' error) it's because the binaries are not signed (signing MacOS binaries requires 99$/year Apple Developer Account subscription, which I can't afford RN!) and Apple Silicon Macs don't allow unsigned apps (downloaded from browser) to be installed on the system. If you want to use NeoDLP on your Apple Silicon Macs, There are few ways you can bypass these restrictions:
|
> ⚠️ MacOS ARM64 binary downloads are experimental and may not open on Apple Silicon Macs if downloaded from browser (You will get 'Damaged File' error) it's because the binaries are not signed (signing MacOS binaries requires 99$/year Apple Developer Account subscription, which I can't afford RN!) and Apple Silicon Macs don't allow unsigned apps (downloaded from browser) to be installed on the system. If you want to use NeoDLP on your Apple Silicon Macs, There are few ways you can bypass these restrictions:
|
||||||
> 1. Using our automated [Curl-Bash Installer](https://neodlp.neosubhamoy.com/download) (Recommended)
|
> 1. Using our automated [Curl-Bash Installer](https://neodlp.neosubhamoy.com/download) (Recommended)
|
||||||
|
|||||||
21
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# NeoDLP - Neo Downloader Plus
|
# NeoDLP - Neo Downloader Plus
|
||||||
|
|
||||||
Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration
|
Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration
|
||||||
|
|
||||||
[](https://github.com/neosubhamoy/neodlp/releases/latest)
|
[](https://github.com/neosubhamoy/neodlp/releases/latest)
|
||||||
[](https://github.com/neosubhamoy/neodlp/releases)
|
[](https://github.com/neosubhamoy/neodlp/releases)
|
||||||
@@ -13,18 +13,21 @@ Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Int
|
|||||||
> **🥰 Liked this project? Please consider giving it a Star (🌟) on github to show us your appreciation and help the algorythm recommend this project to even more awesome people like you!**
|
> **🥰 Liked this project? Please consider giving it a Star (🌟) on github to show us your appreciation and help the algorythm recommend this project to even more awesome people like you!**
|
||||||
|
|
||||||
[](https://github.com/microsoft/winget-pkgs/tree/master/manifests/n/neosubhamoy/neodlp)
|
[](https://github.com/microsoft/winget-pkgs/tree/master/manifests/n/neosubhamoy/neodlp)
|
||||||
|
[](https://flathub.org/en/apps/com.neosubhamoy.neodlp)
|
||||||
[](https://aur.archlinux.org/packages/neodlp)
|
[](https://aur.archlinux.org/packages/neodlp)
|
||||||
|
|
||||||
|
|
||||||
## ✨ Highlighted Features
|
## ✨ Highlighted Features
|
||||||
|
|
||||||
- Download Video/Audio from popular sites (YT, FB, IG, X and other 2.5k+ [supported sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
|
- Download Video/Audio from thousands of popular sites (YT, FB, IG, X and other 2.5k+ [supported sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
|
||||||
|
- Fully Configured YT-DLP Environment Out-of-the-Box (with JS Runtime, PO Token Server, Real-Time Logs etc.)
|
||||||
- Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.)
|
- Download Video/Audio in your preffered format (MP4, WEBM, MKV, MP3 etc.)
|
||||||
- Supports both Video and Playlist download
|
- Supports both Video and Playlist/Batch download
|
||||||
- Supports Combining Video, Audio streams of your choice
|
- Supports Combining Video, Audio streams of your choice
|
||||||
- Supports Multi-Lingual Subtitle/Caption (CC) embeding
|
- Supports Multi-Lingual Subtitle/Caption (CC) embeding
|
||||||
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
|
- Different Video/Audio metadata embeding options (info, chapters, thumbnail etc.)
|
||||||
- SponsorBlock support (mark/remove video segments)
|
- SponsorBlock support (mark/remove video segments)
|
||||||
|
- Aria2 support (for blazing fast downloads)
|
||||||
- Network controls (proxy, rate limit etc.)
|
- Network controls (proxy, rate limit etc.)
|
||||||
- Highly customizable and many more...😉
|
- Highly customizable and many more...😉
|
||||||
|
|
||||||
@@ -49,8 +52,8 @@ After installing the extension you can do the following directly from the browse
|
|||||||
## 💻 Supported Platforms
|
## 💻 Supported Platforms
|
||||||
|
|
||||||
- Windows (10 / 11)
|
- Windows (10 / 11)
|
||||||
- Linux (Debian / Fedora / RHEL / SUSE / Arch Linux base)
|
- Linux (Mostly all modern distros)
|
||||||
- MacOS (>11)
|
- MacOS (>=11)
|
||||||
|
|
||||||
## 🤝 External Dependencies
|
## 🤝 External Dependencies
|
||||||
|
|
||||||
@@ -58,6 +61,7 @@ After installing the extension you can do the following directly from the browse
|
|||||||
- [FFmpeg & FFprobe](https://www.ffmpeg.org) [LGPLv2.1+] - Used for video/audio post-processing
|
- [FFmpeg & FFprobe](https://www.ffmpeg.org) [LGPLv2.1+] - Used for video/audio post-processing
|
||||||
- [Aria2](https://aria2.github.io) [GPLv2+] - Used as an external downloader for blazing fast downloads with yt-dlp (Not included with NeoDLP MacOS builds)
|
- [Aria2](https://aria2.github.io) [GPLv2+] - Used as an external downloader for blazing fast downloads with yt-dlp (Not included with NeoDLP MacOS builds)
|
||||||
- [Deno](https://deno.com) [MIT] - Provides sandboxed javascript runtime environment for yt-dlp (Required for YT downloads, as per the new yt-dlp [announcement](https://github.com/yt-dlp/yt-dlp/issues/14404))
|
- [Deno](https://deno.com) [MIT] - Provides sandboxed javascript runtime environment for yt-dlp (Required for YT downloads, as per the new yt-dlp [announcement](https://github.com/yt-dlp/yt-dlp/issues/14404))
|
||||||
|
- [BgUtils POT Provider (Rust)](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs) [GPLv3+] - Provides PO (Proof-of-Origin) Token for YT downloads
|
||||||
|
|
||||||
## ℹ️ System Pre-Requirements
|
## ℹ️ System Pre-Requirements
|
||||||
|
|
||||||
@@ -80,9 +84,11 @@ After installing the extension you can do the following directly from the browse
|
|||||||
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
|
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
|
||||||
| :---- | :---- | :---- |
|
| :---- | :---- | :---- |
|
||||||
| Windows x86_64 / ARM64 | WinGet | `winget install neosubhamoy.neodlp` |
|
| Windows x86_64 / ARM64 | WinGet | `winget install neosubhamoy.neodlp` |
|
||||||
|
| MacOS x86_64 / ARM64 | Homebrew | `brew install neosubhamoy/tap/neodlp` |
|
||||||
| MacOS x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/macos_installer.sh \| bash` |
|
| MacOS x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/macos_installer.sh \| bash` |
|
||||||
| Linux x86_64 / ARM64 | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/linux_installer.sh \| bash` |
|
| Linux x86_64 / ARM64 (Flatpak) | Flathub | `flatpak install flathub com.neosubhamoy.neodlp` |
|
||||||
| Arch Linux x86_64 / ARM64 | AUR | `yay -S neodlp` |
|
| Linux x86_64 / ARM64 (Native) | Curl-Bash Installer | `curl -sSL https://neodlp.neosubhamoy.com/linux_installer.sh \| bash` |
|
||||||
|
| Arch Linux x86_64 / ARM64 (Native) | AUR | `yay -S neodlp` or `paru -S neodlp` |
|
||||||
|
|
||||||
## 🧪 Package Testing Status
|
## 🧪 Package Testing Status
|
||||||
|
|
||||||
@@ -195,6 +201,7 @@ Noticed any Bug? or Want to give us some suggetions? Always feel free to let us
|
|||||||
- NeoDLP is made possible by the joint efforts of [yt-dlp](https://github.com/yt-dlp/yt-dlp) and [FFmpeg](https://www.ffmpeg.org). Lots of NeoDLP features are actually powered by these tools under the hood! So huge thanks to all the developers/contributers for making these great tools! 🙏
|
- NeoDLP is made possible by the joint efforts of [yt-dlp](https://github.com/yt-dlp/yt-dlp) and [FFmpeg](https://www.ffmpeg.org). Lots of NeoDLP features are actually powered by these tools under the hood! So huge thanks to all the developers/contributers for making these great tools! 🙏
|
||||||
- NeoDLP's 'Format Selection' options are inspired from the [Seal](https://github.com/JunkFood02/Seal) app by [@JunkFood02](https://github.com/JunkFood02)
|
- NeoDLP's 'Format Selection' options are inspired from the [Seal](https://github.com/JunkFood02/Seal) app by [@JunkFood02](https://github.com/JunkFood02)
|
||||||
- Aria2 Linux x86_64 static binaries are built by [@asdo92](https://github.com/asdo92/aria2-static-builds)
|
- Aria2 Linux x86_64 static binaries are built by [@asdo92](https://github.com/asdo92/aria2-static-builds)
|
||||||
|
- NeoDLP's 'POT Server' is based on [@jim60105's Rust Implementation](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs) of [Brainicism/bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider)
|
||||||
|
|
||||||
## ⚖️ License and Usage
|
## ⚖️ License and Usage
|
||||||
|
|
||||||
|
|||||||
10
com.neosubhamoy.neodlp.desktop
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
|
||||||
|
Name=NeoDLP
|
||||||
|
Comment=Modern feature-rich video/audio downloader based on yt-dlp.
|
||||||
|
Icon=com.neosubhamoy.neodlp
|
||||||
|
Exec=neodlp
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
Keywords=neodlp;downloader;yt-dlp-gui;
|
||||||
46
com.neosubhamoy.neodlp.metainfo.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.neosubhamoy.neodlp</id>
|
||||||
|
<name>NeoDLP</name>
|
||||||
|
<summary>Modern feature-rich video/audio downloader based on yt-dlp</summary>
|
||||||
|
<developer id="com.neosubhamoy">
|
||||||
|
<name>Subhamoy Biswas</name>
|
||||||
|
</developer>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>MIT</project_license>
|
||||||
|
<url type="homepage">https://neodlp.neosubhamoy.com</url>
|
||||||
|
<url type="vcs-browser">https://github.com/neosubhamoy/neodlp</url>
|
||||||
|
<url type="bugtracker">https://github.com/neosubhamoy/neodlp/issues</url>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
NeoDLP is a cross-platform desktop application designed for downloading videos and audio from various online sources based on yt-dlp.
|
||||||
|
|
||||||
|
It offers modern user interface, lots of features and customization options.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
<launchable type="desktop-id">com.neosubhamoy.neodlp.desktop</launchable>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/downloader.png</image>
|
||||||
|
<caption>Downloader page of NeoDLP</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/completed-downloads.png</image>
|
||||||
|
<caption>Completed downloads page of NeoDLP</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/ongoing-downloads.png</image>
|
||||||
|
<caption>Ongoing downloads page of NeoDLP</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://raw.githubusercontent.com/neosubhamoy/neodlp/main/.github/images/flathub/settings.png</image>
|
||||||
|
<caption>Settings page of NeoDLP</caption>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<content_rating type="oars-1.1" />
|
||||||
|
<releases>
|
||||||
|
<release version="0.4.3" date="2026-03-07">
|
||||||
|
<url type="details">https://github.com/neosubhamoy/neodlp/releases/tag/v0.4.3</url>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
</component>
|
||||||
3290
package-lock.json
generated
94
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "neodlp",
|
"name": "neodlp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"version": "0.4.3",
|
||||||
"description": "Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration",
|
"description": "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -21,84 +21,50 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@tanstack/devtools-vite": "^0.6.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@tanstack/react-devtools": "^0.10.0",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
"@tanstack/react-pacer": "^0.20.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@tanstack/react-pacer-devtools": "^0.5.5",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@tanstack/react-query": "^5.91.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@tanstack/react-query-devtools": "^5.91.3",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-menubar": "^1.1.16",
|
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@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-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slider": "^1.3.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-toast": "^1.2.15",
|
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@tanstack/devtools-vite": "^0.4.1",
|
|
||||||
"@tanstack/react-devtools": "^0.9.2",
|
|
||||||
"@tanstack/react-pacer": "^0.19.3",
|
|
||||||
"@tanstack/react-pacer-devtools": "^0.5.2",
|
|
||||||
"@tanstack/react-query": "^5.90.19",
|
|
||||||
"@tanstack/react-query-devtools": "^5.91.2",
|
|
||||||
"@tauri-apps/api": "^2.9.1",
|
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||||
|
"@tauri-apps/plugin-log": "^2.8.0",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"@tauri-apps/plugin-os": "^2.3.2",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.4",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"@tauri-apps/plugin-sql": "^2.3.1",
|
"@tauri-apps/plugin-sql": "^2.3.2",
|
||||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"lucide-react": "^0.577.0",
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"embla-carousel-react": "^8.6.0",
|
|
||||||
"input-otp": "^1.4.2",
|
|
||||||
"lucide-react": "^0.562.0",
|
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react-day-picker": "^9.13.0",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.2",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^4.7.3",
|
||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.13.1",
|
||||||
"recharts": "^3.6.0",
|
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"ulid": "^3.0.2",
|
"ulid": "^3.0.2",
|
||||||
"vaul": "^1.1.2",
|
"zod": "^4.3.6",
|
||||||
"zod": "^4.3.5",
|
"zustand": "^5.0.12"
|
||||||
"zustand": "^5.0.10"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tauri-apps/cli": "^2.10.1",
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
"@types/node": "^25.5.0",
|
||||||
"@types/node": "^25.0.9",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react": "^19.2.9",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"postcss": "^8.5.6",
|
"tailwindcss": "^4.2.2",
|
||||||
"tailwindcss": "^4.1.18",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^8.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,11 @@ const targetPlatform = process.argv[2];
|
|||||||
const targetBin = process.argv[3];
|
const targetBin = process.argv[3];
|
||||||
|
|
||||||
const versions = {
|
const versions = {
|
||||||
'yt-dlp': 'latest',
|
'yt-dlp': '2026.03.17.232108',
|
||||||
'ffmpeg-ffprobe': 'latest',
|
'ffmpeg-ffprobe': 'latest',
|
||||||
'deno': 'latest',
|
'deno': '2.7.7',
|
||||||
'aria2c': '1.37.0',
|
'aria2c': '1.37.0',
|
||||||
|
'neodlp-pot': '0.8.1'
|
||||||
};
|
};
|
||||||
|
|
||||||
const binaries = {
|
const binaries = {
|
||||||
@@ -145,10 +146,54 @@ const binaries = {
|
|||||||
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl')
|
path.join(downloadDir, 'ffmpeg-master-latest-linuxarm64-gpl')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'ffmpeg-universal-apple-darwin',
|
||||||
|
// platform: 'darwin',
|
||||||
|
// url: `https://evermeet.cx/ffmpeg/get/zip`,
|
||||||
|
// src: path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
|
||||||
|
// dest: null,
|
||||||
|
// archive: {
|
||||||
|
// type: 'zip',
|
||||||
|
// binSrc: [
|
||||||
|
// path.join(downloadDir, 'ffmpeg'),
|
||||||
|
// path.join(downloadDir, 'ffmpeg')
|
||||||
|
// ],
|
||||||
|
// binDest: [
|
||||||
|
// path.join(binDir, 'ffmpeg-x86_64-apple-darwin'),
|
||||||
|
// path.join(binDir, 'ffmpeg-aarch64-apple-darwin')
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// cleanup: [
|
||||||
|
// path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
|
||||||
|
// path.join(downloadDir, 'ffmpeg')
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'ffprobe-universal-apple-darwin',
|
||||||
|
// platform: 'darwin',
|
||||||
|
// url: `https://evermeet.cx/ffmpeg/get/ffprobe/zip`,
|
||||||
|
// src: path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
|
||||||
|
// dest: null,
|
||||||
|
// archive: {
|
||||||
|
// type: 'zip',
|
||||||
|
// binSrc: [
|
||||||
|
// path.join(downloadDir, 'ffprobe'),
|
||||||
|
// path.join(downloadDir, 'ffprobe')
|
||||||
|
// ],
|
||||||
|
// binDest: [
|
||||||
|
// path.join(binDir, 'ffprobe-x86_64-apple-darwin'),
|
||||||
|
// path.join(binDir, 'ffprobe-aarch64-apple-darwin')
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// cleanup: [
|
||||||
|
// path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
|
||||||
|
// path.join(downloadDir, 'ffprobe')
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
name: 'ffmpeg-universal-apple-darwin',
|
name: 'ffmpeg-universal-apple-darwin',
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
url: `https://evermeet.cx/ffmpeg/get/zip`,
|
url: `https://github.com/neosubhamoy/evermeet-static-ffmpeg/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffmpeg-universal-apple-darwin.zip`,
|
||||||
src: path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
|
src: path.join(downloadDir, 'ffmpeg-universal-apple-darwin.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -170,7 +215,7 @@ const binaries = {
|
|||||||
{
|
{
|
||||||
name: 'ffprobe-universal-apple-darwin',
|
name: 'ffprobe-universal-apple-darwin',
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
url: `https://evermeet.cx/ffmpeg/get/ffprobe/zip`,
|
url: `https://github.com/neosubhamoy/evermeet-static-ffmpeg/releases${versions['ffmpeg-ffprobe'] === 'latest' ? '/latest' : ''}/download${versions['ffmpeg-ffprobe'] !== 'latest' ? '/'+versions['ffmpeg-ffprobe'] : ''}/ffprobe-universal-apple-darwin.zip`,
|
||||||
src: path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
|
src: path.join(downloadDir, 'ffprobe-universal-apple-darwin.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -194,7 +239,7 @@ const binaries = {
|
|||||||
{
|
{
|
||||||
name: 'deno-x86_64-pc-windows-msvc',
|
name: 'deno-x86_64-pc-windows-msvc',
|
||||||
platform: 'win32',
|
platform: 'win32',
|
||||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-x86_64-pc-windows-msvc.zip`,
|
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-x86_64-pc-windows-msvc.zip`,
|
||||||
src: path.join(downloadDir, 'deno-x86_64-pc-windows-msvc.zip'),
|
src: path.join(downloadDir, 'deno-x86_64-pc-windows-msvc.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -214,7 +259,7 @@ const binaries = {
|
|||||||
{
|
{
|
||||||
name: 'deno-x86_64-unknown-linux-gnu',
|
name: 'deno-x86_64-unknown-linux-gnu',
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-x86_64-unknown-linux-gnu.zip`,
|
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-x86_64-unknown-linux-gnu.zip`,
|
||||||
src: path.join(downloadDir, 'deno-x86_64-unknown-linux-gnu.zip'),
|
src: path.join(downloadDir, 'deno-x86_64-unknown-linux-gnu.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -234,7 +279,7 @@ const binaries = {
|
|||||||
{
|
{
|
||||||
name: 'deno-aarch64-unknown-linux-gnu',
|
name: 'deno-aarch64-unknown-linux-gnu',
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-aarch64-unknown-linux-gnu.zip`,
|
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-aarch64-unknown-linux-gnu.zip`,
|
||||||
src: path.join(downloadDir, 'deno-aarch64-unknown-linux-gnu.zip'),
|
src: path.join(downloadDir, 'deno-aarch64-unknown-linux-gnu.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -254,7 +299,7 @@ const binaries = {
|
|||||||
{
|
{
|
||||||
name: 'deno-x86_64-apple-darwin',
|
name: 'deno-x86_64-apple-darwin',
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-x86_64-apple-darwin.zip`,
|
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-x86_64-apple-darwin.zip`,
|
||||||
src: path.join(downloadDir, 'deno-x86_64-apple-darwin.zip'),
|
src: path.join(downloadDir, 'deno-x86_64-apple-darwin.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -274,7 +319,7 @@ const binaries = {
|
|||||||
{
|
{
|
||||||
name: 'deno-aarch64-apple-darwin',
|
name: 'deno-aarch64-apple-darwin',
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/'+versions['deno'] : ''}/deno-aarch64-apple-darwin.zip`,
|
url: `https://github.com/denoland/deno/releases${versions['deno'] === 'latest' ? '/latest' : ''}/download${versions['deno'] !== 'latest' ? '/v'+versions['deno'] : ''}/deno-aarch64-apple-darwin.zip`,
|
||||||
src: path.join(downloadDir, 'deno-aarch64-apple-darwin.zip'),
|
src: path.join(downloadDir, 'deno-aarch64-apple-darwin.zip'),
|
||||||
dest: null,
|
dest: null,
|
||||||
archive: {
|
archive: {
|
||||||
@@ -353,6 +398,73 @@ const binaries = {
|
|||||||
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`)
|
path.join(downloadDir, `aria2-${versions['aria2c']}-aarch64-linux-android-build1`)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
'neodlp-pot': [
|
||||||
|
{
|
||||||
|
name: 'neodlp-pot-x86_64-pc-windows-msvc',
|
||||||
|
platform: 'win32',
|
||||||
|
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-windows-x86_64.exe`,
|
||||||
|
src: path.join(downloadDir, 'bgutil-pot-windows-x86_64.exe'),
|
||||||
|
dest: [
|
||||||
|
path.join(binDir, 'neodlp-pot-x86_64-pc-windows-msvc.exe')
|
||||||
|
],
|
||||||
|
archive: null,
|
||||||
|
cleanup: [
|
||||||
|
path.join(downloadDir, 'bgutil-pot-windows-x86_64.exe')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'neodlp-pot-x86_64-unknown-linux-gnu',
|
||||||
|
platform: 'linux',
|
||||||
|
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-linux-x86_64`,
|
||||||
|
src: path.join(downloadDir, 'bgutil-pot-linux-x86_64'),
|
||||||
|
dest: [
|
||||||
|
path.join(binDir, 'neodlp-pot-x86_64-unknown-linux-gnu')
|
||||||
|
],
|
||||||
|
archive: null,
|
||||||
|
cleanup: [
|
||||||
|
path.join(downloadDir, 'bgutil-pot-linux-x86_64')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'neodlp-pot-aarch64-unknown-linux-gnu',
|
||||||
|
platform: 'linux',
|
||||||
|
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-linux-aarch64`,
|
||||||
|
src: path.join(downloadDir, 'bgutil-pot-linux-aarch64'),
|
||||||
|
dest: [
|
||||||
|
path.join(binDir, 'neodlp-pot-aarch64-unknown-linux-gnu')
|
||||||
|
],
|
||||||
|
archive: null,
|
||||||
|
cleanup: [
|
||||||
|
path.join(downloadDir, 'bgutil-pot-linux-aarch64')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'neodlp-pot-x86_64-apple-darwin',
|
||||||
|
platform: 'darwin',
|
||||||
|
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-macos-x86_64`,
|
||||||
|
src: path.join(downloadDir, 'bgutil-pot-macos-x86_64'),
|
||||||
|
dest: [
|
||||||
|
path.join(binDir, 'neodlp-pot-x86_64-apple-darwin')
|
||||||
|
],
|
||||||
|
archive: null,
|
||||||
|
cleanup: [
|
||||||
|
path.join(downloadDir, 'bgutil-pot-macos-x86_64')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'neodlp-pot-aarch64-apple-darwin',
|
||||||
|
platform: 'darwin',
|
||||||
|
url: `https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/releases${versions['neodlp-pot'] === 'latest' ? '/latest' : ''}/download${versions['neodlp-pot'] !== 'latest' ? '/v'+versions['neodlp-pot'] : ''}/bgutil-pot-macos-aarch64`,
|
||||||
|
src: path.join(downloadDir, 'bgutil-pot-macos-aarch64'),
|
||||||
|
dest: [
|
||||||
|
path.join(binDir, 'neodlp-pot-aarch64-apple-darwin')
|
||||||
|
],
|
||||||
|
archive: null,
|
||||||
|
cleanup: [
|
||||||
|
path.join(downloadDir, 'bgutil-pot-macos-aarch64')
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1754
src-tauri/Cargo.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "neodlp"
|
name = "neodlp"
|
||||||
version = "0.4.0"
|
version = "0.4.3"
|
||||||
description = "Cross-platform Video/Audio Downloader Desktop App with Modern UI and Browser Integration"
|
description = "Cross-platform Video/Audio Downloader Desktop App based on YT-DLP with Modern UI and Browser Integration"
|
||||||
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -29,6 +29,7 @@ sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tl
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
directories = "6.0"
|
directories = "6.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
log = "0.4"
|
||||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
@@ -39,6 +40,7 @@ tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
|||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
tauri-plugin-clipboard-manager = "2"
|
tauri-plugin-clipboard-manager = "2"
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
|
tauri-plugin-log = "2"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
"core:window:allow-hide",
|
"core:window:allow-hide",
|
||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-unmaximize",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"shell:default",
|
"shell:default",
|
||||||
"fs:default",
|
"fs:default",
|
||||||
@@ -25,8 +29,9 @@
|
|||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
|
"log:default",
|
||||||
{
|
{
|
||||||
"identifier": "opener:allow-open-path",
|
"identifier": "fs:scope",
|
||||||
"allow": [
|
"allow": [
|
||||||
{
|
{
|
||||||
"path": "**"
|
"path": "**"
|
||||||
@@ -34,7 +39,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifier": "fs:scope",
|
"identifier": "opener:allow-open-path",
|
||||||
"allow": [
|
"allow": [
|
||||||
{
|
{
|
||||||
"path": "**"
|
"path": "**"
|
||||||
|
|||||||
@@ -35,6 +35,16 @@
|
|||||||
"args": true,
|
"args": true,
|
||||||
"sidecar": true
|
"sidecar": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "binaries/neodlp-pot",
|
||||||
|
"args": true,
|
||||||
|
"sidecar": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yt-dlp",
|
||||||
|
"cmd": "yt-dlp",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ffmpeg",
|
"name": "ffmpeg",
|
||||||
"cmd": "ffmpeg",
|
"cmd": "ffmpeg",
|
||||||
@@ -45,6 +55,11 @@
|
|||||||
"cmd": "aria2c",
|
"cmd": "aria2c",
|
||||||
"args": true
|
"args": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "deno",
|
||||||
|
"cmd": "deno",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pkexec",
|
"name": "pkexec",
|
||||||
"cmd": "pkexec",
|
"cmd": "pkexec",
|
||||||
@@ -54,6 +69,11 @@
|
|||||||
"name": "powershell",
|
"name": "powershell",
|
||||||
"cmd": "powershell",
|
"cmd": "powershell",
|
||||||
"args": true
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sh",
|
||||||
|
"cmd": "sh",
|
||||||
|
"args": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -85,6 +105,16 @@
|
|||||||
"args": true,
|
"args": true,
|
||||||
"sidecar": true
|
"sidecar": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "binaries/neodlp-pot",
|
||||||
|
"args": true,
|
||||||
|
"sidecar": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yt-dlp",
|
||||||
|
"cmd": "yt-dlp",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ffmpeg",
|
"name": "ffmpeg",
|
||||||
"cmd": "ffmpeg",
|
"cmd": "ffmpeg",
|
||||||
@@ -94,6 +124,26 @@
|
|||||||
"name": "aria2c",
|
"name": "aria2c",
|
||||||
"cmd": "aria2c",
|
"cmd": "aria2c",
|
||||||
"args": true
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deno",
|
||||||
|
"cmd": "deno",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pkexec",
|
||||||
|
"cmd": "pkexec",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "powershell",
|
||||||
|
"cmd": "powershell",
|
||||||
|
"args": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sh",
|
||||||
|
"cmd": "sh",
|
||||||
|
"args": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__version__ = '0.8.1'
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import json
|
||||||
|
|
||||||
|
from yt_dlp.extractor.youtube.pot.provider import (
|
||||||
|
ExternalRequestFeature,
|
||||||
|
PoTokenContext,
|
||||||
|
PoTokenProvider,
|
||||||
|
PoTokenProviderRejectedRequest,
|
||||||
|
)
|
||||||
|
from yt_dlp.extractor.youtube.pot.utils import WEBPO_CLIENTS
|
||||||
|
from yt_dlp.utils import js_to_json
|
||||||
|
from yt_dlp.utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class BgUtilPTPBase(PoTokenProvider, abc.ABC):
|
||||||
|
PROVIDER_VERSION = __version__
|
||||||
|
BUG_REPORT_LOCATION = (
|
||||||
|
'https://github.com/jim60105/bgutil-ytdlp-pot-provider/issues'
|
||||||
|
)
|
||||||
|
_SUPPORTED_EXTERNAL_REQUEST_FEATURES = (
|
||||||
|
ExternalRequestFeature.PROXY_SCHEME_HTTP,
|
||||||
|
ExternalRequestFeature.PROXY_SCHEME_HTTPS,
|
||||||
|
ExternalRequestFeature.PROXY_SCHEME_SOCKS4,
|
||||||
|
ExternalRequestFeature.PROXY_SCHEME_SOCKS4A,
|
||||||
|
ExternalRequestFeature.PROXY_SCHEME_SOCKS5,
|
||||||
|
ExternalRequestFeature.PROXY_SCHEME_SOCKS5H,
|
||||||
|
ExternalRequestFeature.SOURCE_ADDRESS,
|
||||||
|
ExternalRequestFeature.DISABLE_TLS_VERIFICATION,
|
||||||
|
)
|
||||||
|
_SUPPORTED_CLIENTS = WEBPO_CLIENTS
|
||||||
|
_SUPPORTED_CONTEXTS = (
|
||||||
|
PoTokenContext.GVS,
|
||||||
|
PoTokenContext.PLAYER,
|
||||||
|
PoTokenContext.SUBS,
|
||||||
|
)
|
||||||
|
_GETPOT_TIMEOUT = 20.0
|
||||||
|
_GET_SERVER_VSN_TIMEOUT = 5.0
|
||||||
|
_MIN_NODE_VSN = (18, 0, 0)
|
||||||
|
|
||||||
|
def _info_and_raise(self, msg, raise_from=None):
|
||||||
|
self.logger.info(msg)
|
||||||
|
raise PoTokenProviderRejectedRequest(msg) from raise_from
|
||||||
|
|
||||||
|
def _warn_and_raise(self, msg, once=True, raise_from=None):
|
||||||
|
self.logger.warning(msg, once=once)
|
||||||
|
raise PoTokenProviderRejectedRequest(msg) from raise_from
|
||||||
|
|
||||||
|
def _get_attestation(self, webpage: str | None):
|
||||||
|
if not webpage:
|
||||||
|
return None
|
||||||
|
raw_cd = (
|
||||||
|
traverse_obj(
|
||||||
|
self.ie._search_regex(
|
||||||
|
r'''(?sx)window\s*\.\s*ytAtN\s*\(\s*
|
||||||
|
(?P<js>\{.+?}\s*)
|
||||||
|
\s*\)\s*;''',
|
||||||
|
webpage,
|
||||||
|
'ytAtN challenge',
|
||||||
|
default=None),
|
||||||
|
({js_to_json}, {json.loads}, 'R'))
|
||||||
|
or traverse_obj(
|
||||||
|
self.ie._search_regex(
|
||||||
|
r'''(?sx)window\.ytAtR\s*=\s*(?P<raw_cd>(?P<q>['"])
|
||||||
|
(?:
|
||||||
|
\\.|
|
||||||
|
(?!(?P=q)).
|
||||||
|
)*
|
||||||
|
(?P=q))\s*;''',
|
||||||
|
webpage,
|
||||||
|
'ytAtR challenge',
|
||||||
|
default=None),
|
||||||
|
({js_to_json}, {json.loads})))
|
||||||
|
if att_txt := traverse_obj(raw_cd, ({json.loads}, 'bgChallenge')):
|
||||||
|
return att_txt
|
||||||
|
self.logger.warning('Failed to extract initial attestation from the webpage')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['__version__']
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from yt_dlp.extractor.youtube.pot.provider import (
|
||||||
|
PoTokenProviderError,
|
||||||
|
PoTokenRequest,
|
||||||
|
PoTokenResponse,
|
||||||
|
register_preference,
|
||||||
|
register_provider,
|
||||||
|
)
|
||||||
|
from yt_dlp.extractor.youtube.pot.utils import get_webpo_content_binding
|
||||||
|
from yt_dlp.utils import Popen
|
||||||
|
|
||||||
|
from yt_dlp_plugins.extractor.getpot_bgutil import BgUtilPTPBase
|
||||||
|
|
||||||
|
|
||||||
|
@register_provider
|
||||||
|
class BgUtilCliPTP(BgUtilPTPBase):
|
||||||
|
PROVIDER_NAME = 'bgutil:cli'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._check_cli = functools.cache(self._check_cli_impl)
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def _cli_path(self):
|
||||||
|
cli_path = self._configuration_arg(
|
||||||
|
'cli_path', casesense=True, default=[None])[0]
|
||||||
|
|
||||||
|
if cli_path:
|
||||||
|
return os.path.expandvars(cli_path)
|
||||||
|
|
||||||
|
# check deprecated arg
|
||||||
|
deprecated_cli_path = self.ie._configuration_arg(
|
||||||
|
ie_key='youtube', key='getpot_bgutil_script', default=[None])[0]
|
||||||
|
|
||||||
|
if deprecated_cli_path:
|
||||||
|
self._warn_and_raise(
|
||||||
|
"'youtube:getpot_bgutil_script' extractor arg is deprecated, "
|
||||||
|
"use 'youtubepot-bgutilcli:cli_path' instead")
|
||||||
|
|
||||||
|
# default if no arg was passed
|
||||||
|
# First, try to find the executable in PATH
|
||||||
|
if self._get_executable_path('bgutil-pot'):
|
||||||
|
self.logger.debug('Found bgutil-pot in PATH')
|
||||||
|
return 'bgutil-pot'
|
||||||
|
|
||||||
|
# Then check common file locations
|
||||||
|
file_paths = [
|
||||||
|
os.path.join(
|
||||||
|
os.getcwd(), 'target', 'debug', 'bgutil-pot'
|
||||||
|
),
|
||||||
|
os.path.join(
|
||||||
|
os.getcwd(), 'target', 'release', 'bgutil-pot'
|
||||||
|
),
|
||||||
|
os.path.expanduser(
|
||||||
|
'~/bgutil-ytdlp-pot-provider/target/debug/bgutil-pot'
|
||||||
|
),
|
||||||
|
os.path.expanduser(
|
||||||
|
'~/bgutil-ytdlp-pot-provider/target/release/'
|
||||||
|
'bgutil-pot'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in file_paths:
|
||||||
|
if self._get_executable_path(path):
|
||||||
|
self.logger.debug(f'Found bgutil-pot at: {path}')
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Fallback to PATH name if no file found
|
||||||
|
default_path = 'bgutil-pot'
|
||||||
|
self.logger.debug(
|
||||||
|
f'No CLI path found, defaulting to {default_path}')
|
||||||
|
return default_path
|
||||||
|
|
||||||
|
def is_available(self):
|
||||||
|
return self._check_cli(self._cli_path)
|
||||||
|
|
||||||
|
def _get_executable_path(self, cli_path):
|
||||||
|
"""Get the actual executable path, checking PATH or file existence."""
|
||||||
|
# For relative names (like 'bgutil-pot-generate'), search in PATH
|
||||||
|
if os.path.sep not in cli_path:
|
||||||
|
executable_path = shutil.which(cli_path)
|
||||||
|
if executable_path:
|
||||||
|
return executable_path
|
||||||
|
|
||||||
|
# For absolute/relative paths, check file existence directly
|
||||||
|
if os.path.isfile(cli_path):
|
||||||
|
return cli_path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _check_cli_impl(self, cli_path):
|
||||||
|
executable_path = self._get_executable_path(cli_path)
|
||||||
|
if not executable_path:
|
||||||
|
self.logger.debug(
|
||||||
|
f"Executable path doesn't exist: {cli_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
stdout, stderr, returncode = Popen.run(
|
||||||
|
[executable_path, '--version'],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=self._GET_SERVER_VSN_TIMEOUT
|
||||||
|
)
|
||||||
|
if returncode:
|
||||||
|
self.logger.warning(
|
||||||
|
f'Failed to check executable version. '
|
||||||
|
f'Executable returned {returncode} exit status. '
|
||||||
|
f'stdout: {stdout}; stderr: {stderr}',
|
||||||
|
once=True)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.logger.debug(f'bgutil-pot version: {stdout.strip()}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _real_request_pot(
|
||||||
|
self,
|
||||||
|
request: PoTokenRequest,
|
||||||
|
) -> PoTokenResponse:
|
||||||
|
# used for CI check
|
||||||
|
self.logger.trace(
|
||||||
|
f'Generating POT via Rust executable: {self._cli_path}')
|
||||||
|
|
||||||
|
executable_path = self._get_executable_path(self._cli_path)
|
||||||
|
if not executable_path:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'Executable not found: {self._cli_path}')
|
||||||
|
|
||||||
|
command_args = [executable_path]
|
||||||
|
if proxy := request.request_proxy:
|
||||||
|
command_args.extend(['-p', proxy])
|
||||||
|
command_args.extend(['-c', get_webpo_content_binding(request)[0]])
|
||||||
|
if request.bypass_cache:
|
||||||
|
command_args.append('--bypass-cache')
|
||||||
|
if request.request_source_address:
|
||||||
|
command_args.extend(
|
||||||
|
['--source-address', request.request_source_address])
|
||||||
|
if request.request_verify_tls is False:
|
||||||
|
command_args.append('--disable-tls-verification')
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f'Generating a {request.context.value} PO Token for '
|
||||||
|
f'{request.internal_client_name} client via bgutil '
|
||||||
|
f'Rust executable',
|
||||||
|
)
|
||||||
|
self.logger.debug(
|
||||||
|
f'Executing command to get POT via Rust executable: '
|
||||||
|
f'{" ".join(command_args)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stdout, stderr, returncode = Popen.run(
|
||||||
|
command_args,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=self._GETPOT_TIMEOUT
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'_get_pot_via_cli failed: Timeout expired when trying '
|
||||||
|
f'to run executable (caused by {e!r})'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'_get_pot_via_cli failed: Unable to run executable '
|
||||||
|
f'(caused by {e!r})'
|
||||||
|
) from e
|
||||||
|
|
||||||
|
msg = ''
|
||||||
|
if stdout_extra := stdout.strip().splitlines()[:-1]:
|
||||||
|
msg = f'stdout:\n{stdout_extra}\n'
|
||||||
|
if stderr_stripped := stderr.strip(): # Empty strings are falsy
|
||||||
|
msg += f'stderr:\n{stderr_stripped}\n'
|
||||||
|
msg = msg.strip()
|
||||||
|
if msg:
|
||||||
|
self.logger.trace(msg)
|
||||||
|
if returncode:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'_get_pot_via_cli failed with returncode {returncode}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_resp = stdout.splitlines()[-1]
|
||||||
|
self.logger.trace(f'JSON response:\n{json_resp}')
|
||||||
|
# The JSON response is always the last line
|
||||||
|
cli_data_resp = json.loads(json_resp)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'Error parsing JSON response from _get_pot_via_cli '
|
||||||
|
f'(caused by {e!r})'
|
||||||
|
) from e
|
||||||
|
if 'poToken' not in cli_data_resp:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
'The executable did not respond with a po_token')
|
||||||
|
return PoTokenResponse(po_token=cli_data_resp['poToken'])
|
||||||
|
|
||||||
|
|
||||||
|
@register_preference(BgUtilCliPTP)
|
||||||
|
def bgutil_cli_getpot_preference(provider, request):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [BgUtilCliPTP.__name__,
|
||||||
|
bgutil_cli_getpot_preference.__name__]
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from yt_dlp.extractor.youtube.pot.provider import (
|
||||||
|
PoTokenProviderError,
|
||||||
|
PoTokenProviderRejectedRequest,
|
||||||
|
PoTokenRequest,
|
||||||
|
PoTokenResponse,
|
||||||
|
register_preference,
|
||||||
|
register_provider,
|
||||||
|
)
|
||||||
|
from yt_dlp.extractor.youtube.pot.utils import get_webpo_content_binding
|
||||||
|
from yt_dlp.networking.common import Request
|
||||||
|
from yt_dlp.networking.exceptions import HTTPError, TransportError
|
||||||
|
|
||||||
|
from yt_dlp_plugins.extractor.getpot_bgutil import BgUtilPTPBase
|
||||||
|
|
||||||
|
|
||||||
|
@register_provider
|
||||||
|
class BgUtilHTTPPTP(BgUtilPTPBase):
|
||||||
|
PROVIDER_NAME = 'bgutil:http'
|
||||||
|
DEFAULT_BASE_URL = 'http://127.0.0.1:4416'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._last_server_check = 0
|
||||||
|
self._server_available = True
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def _base_url(self):
|
||||||
|
base_url = self._configuration_arg('base_url', default=[None])[0]
|
||||||
|
|
||||||
|
if base_url:
|
||||||
|
return base_url
|
||||||
|
|
||||||
|
# check deprecated arg
|
||||||
|
deprecated_base_url = self.ie._configuration_arg(
|
||||||
|
ie_key='youtube', key='getpot_bgutil_baseurl', default=[None])[0]
|
||||||
|
if deprecated_base_url:
|
||||||
|
self._warn_and_raise(
|
||||||
|
"'youtube:getpot_bgutil_baseurl' extractor arg is deprecated, "
|
||||||
|
"use 'youtubepot-bgutilhttp:base_url' instead"
|
||||||
|
)
|
||||||
|
|
||||||
|
# default if no arg was passed
|
||||||
|
self.logger.debug(
|
||||||
|
f'No base_url provided, defaulting to {self.DEFAULT_BASE_URL}')
|
||||||
|
return self.DEFAULT_BASE_URL
|
||||||
|
|
||||||
|
def _check_server_availability(self, ctx: PoTokenRequest):
|
||||||
|
if self._last_server_check + 60 > time.time():
|
||||||
|
return self._server_available
|
||||||
|
|
||||||
|
self._server_available = False
|
||||||
|
try:
|
||||||
|
self.logger.trace(
|
||||||
|
f'Checking server availability at {self._base_url}/ping')
|
||||||
|
response = json.load(self._request_webpage(Request(
|
||||||
|
f'{self._base_url}/ping',
|
||||||
|
extensions={'timeout': self._GET_SERVER_VSN_TIMEOUT},
|
||||||
|
proxies={'all': None}
|
||||||
|
),
|
||||||
|
note=False))
|
||||||
|
except TransportError as e:
|
||||||
|
# the server may be down
|
||||||
|
script_path_provided = self.ie._configuration_arg(
|
||||||
|
ie_key='youtubepot-bgutilscript',
|
||||||
|
key='script_path',
|
||||||
|
default=[None]
|
||||||
|
)[0] is not None
|
||||||
|
|
||||||
|
warning_base = (
|
||||||
|
f'Error reaching GET {self._base_url}/ping '
|
||||||
|
f'(caused by {e.__class__.__name__}). '
|
||||||
|
)
|
||||||
|
if script_path_provided: # server down is expected, log info
|
||||||
|
self._info_and_raise(
|
||||||
|
warning_base +
|
||||||
|
'This is expected if you are using the script method.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._warn_and_raise(
|
||||||
|
warning_base +
|
||||||
|
f'Please make sure that the server is reachable at '
|
||||||
|
f'{self._base_url}.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
except HTTPError as e:
|
||||||
|
# may be an old server, don't raise
|
||||||
|
self.logger.warning(
|
||||||
|
f'HTTP Error reaching GET /ping (caused by {e!r})', once=True)
|
||||||
|
return
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
# invalid server
|
||||||
|
self._warn_and_raise(
|
||||||
|
f'Error parsing ping response JSON (caused by {e!r})')
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self._warn_and_raise(
|
||||||
|
f'Unknown error reaching GET /ping (caused by {e!r})',
|
||||||
|
raise_from=e
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
version = response.get("version", "unknown")
|
||||||
|
self.logger.debug(f'HTTP server version: {version}')
|
||||||
|
self._server_available = True
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
self._last_server_check = time.time()
|
||||||
|
|
||||||
|
def is_available(self):
|
||||||
|
return (self._server_available or
|
||||||
|
self._last_server_check + 60 < int(time.time()))
|
||||||
|
|
||||||
|
def _real_request_pot(
|
||||||
|
self,
|
||||||
|
request: PoTokenRequest,
|
||||||
|
) -> PoTokenResponse:
|
||||||
|
if not self._check_server_availability(request):
|
||||||
|
raise PoTokenProviderRejectedRequest(
|
||||||
|
f'{self.PROVIDER_NAME} server is not available')
|
||||||
|
|
||||||
|
# used for CI check
|
||||||
|
self.logger.trace('Generating POT via HTTP server')
|
||||||
|
|
||||||
|
disable_innertube = bool(
|
||||||
|
self._configuration_arg('disable_innertube', default=[None])[0]
|
||||||
|
)
|
||||||
|
challenge = self._get_attestation(
|
||||||
|
None if disable_innertube else request.video_webpage
|
||||||
|
)
|
||||||
|
# The challenge is falsy when the webpage and the challenge are
|
||||||
|
# unavailable. In this case, we need to disable /att/get since
|
||||||
|
# it's broken for web_music
|
||||||
|
if not challenge and request.internal_client_name == 'web_music':
|
||||||
|
if not disable_innertube: # if not already set, warn the user
|
||||||
|
self.logger.warning(
|
||||||
|
'BotGuard challenges could not be obtained from the '
|
||||||
|
'webpage, overriding disable_innertube=True because '
|
||||||
|
'InnerTube challenges are currently broken for the '
|
||||||
|
'web_music client. Pass disable_innertube=1 to suppress '
|
||||||
|
'this warning.'
|
||||||
|
)
|
||||||
|
disable_innertube = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self._request_webpage(
|
||||||
|
request=Request(
|
||||||
|
f'{self._base_url}/get_pot', data=json.dumps({
|
||||||
|
'bypass_cache': request.bypass_cache,
|
||||||
|
'challenge': challenge,
|
||||||
|
'content_binding': get_webpo_content_binding(
|
||||||
|
request
|
||||||
|
)[0],
|
||||||
|
'disable_innertube': disable_innertube,
|
||||||
|
'disable_tls_verification': (
|
||||||
|
not request.request_verify_tls
|
||||||
|
),
|
||||||
|
'proxy': request.request_proxy,
|
||||||
|
'innertube_context': request.innertube_context,
|
||||||
|
'source_address': request.request_source_address,
|
||||||
|
}).encode(), headers={'Content-Type': 'application/json'},
|
||||||
|
extensions={'timeout': self._GETPOT_TIMEOUT},
|
||||||
|
proxies={'all': None}
|
||||||
|
),
|
||||||
|
note=f'Generating a {request.context.value} PO Token for '
|
||||||
|
f'{request.internal_client_name} client via bgutil '
|
||||||
|
f'HTTP server',
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'Error reaching POST /get_pot (caused by {e!r})') from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_json = json.load(response)
|
||||||
|
except Exception as e:
|
||||||
|
response_data = response.read().decode()
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'Error parsing response JSON (caused by {e!r}). '
|
||||||
|
f'response = {response_data}'
|
||||||
|
) from e
|
||||||
|
|
||||||
|
if error_msg := response_json.get('error'):
|
||||||
|
raise PoTokenProviderError(error_msg)
|
||||||
|
if 'poToken' not in response_json:
|
||||||
|
raise PoTokenProviderError(
|
||||||
|
f'Server did not respond with a poToken. '
|
||||||
|
f'Received response: {response}'
|
||||||
|
)
|
||||||
|
|
||||||
|
po_token = response_json['poToken']
|
||||||
|
self.logger.trace(f'Generated POT: {po_token}')
|
||||||
|
return PoTokenResponse(po_token=po_token)
|
||||||
|
|
||||||
|
|
||||||
|
@register_preference(BgUtilHTTPPTP)
|
||||||
|
def bgutil_HTTP_getpot_preference(provider, request):
|
||||||
|
return 130
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [BgUtilHTTPPTP.__name__,
|
||||||
|
bgutil_HTTP_getpot_preference.__name__]
|
||||||
@@ -21,6 +21,7 @@ use std::{
|
|||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{Menu, MenuItem},
|
menu::{Menu, MenuItem},
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
|
path::BaseDirectory,
|
||||||
Emitter, Manager, State,
|
Emitter, Manager, State,
|
||||||
};
|
};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
@@ -30,6 +31,7 @@ use tokio::{
|
|||||||
time::sleep,
|
time::sleep,
|
||||||
};
|
};
|
||||||
use tokio_tungstenite::accept_async;
|
use tokio_tungstenite::accept_async;
|
||||||
|
use log::{info, error};
|
||||||
|
|
||||||
struct ImageCache(StdMutex<HashMap<String, String>>);
|
struct ImageCache(StdMutex<HashMap<String, String>>);
|
||||||
|
|
||||||
@@ -184,6 +186,16 @@ fn get_current_app_path() -> Result<String, String> {
|
|||||||
.into_owned())
|
.into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn is_flatpak() -> bool {
|
||||||
|
std::env::var("FLATPAK").is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_appimage_path() -> Option<String> {
|
||||||
|
std::env::var("APPDIR").ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn update_config(
|
async fn update_config(
|
||||||
new_config: Config,
|
new_config: Config,
|
||||||
@@ -346,21 +358,48 @@ async fn open_file_with_app(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(name) = &app_name {
|
if let Some(name) = &app_name {
|
||||||
if name == "explorer" {
|
if name == "explorer" {
|
||||||
println!("Revealing file: {} in explorer", file_path);
|
info!("Revealing file: {} in explorer", file_path);
|
||||||
return app_handle
|
return app_handle
|
||||||
.opener()
|
.opener()
|
||||||
.reveal_item_in_dir(file_path)
|
.reveal_item_in_dir(file_path)
|
||||||
.map_err(|e| e.to_string());
|
.map_err(|e| {
|
||||||
|
error!("Failed to reveal file in explorer: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
println!("Opening file: {} with app: {}", file_path, name);
|
info!("Opening file: {} with app: {}", file_path, name);
|
||||||
} else {
|
} else {
|
||||||
println!("Opening file: {} with default app", file_path);
|
info!("Opening file: {} with default app", file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
app_handle
|
app_handle
|
||||||
.opener()
|
.opener()
|
||||||
.open_path(file_path, app_name)
|
.open_path(file_path, app_name)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| {
|
||||||
|
error!("Failed to open file: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_link_with_app(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
url: String,
|
||||||
|
app_name: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(name) = &app_name {
|
||||||
|
info!("Opening link: {} with app: {}", url, name);
|
||||||
|
} else {
|
||||||
|
info!("Opening link: {} with default app", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
app_handle
|
||||||
|
.opener()
|
||||||
|
.open_url(url, app_name)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to open link: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -477,6 +516,11 @@ pub async fn run() {
|
|||||||
let start_hidden = args.contains(&"--hidden".to_string());
|
let start_hidden = args.contains(&"--hidden".to_string());
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_log::Builder::new()
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.max_file_size(5_242_880) /* in bytes = 5MB */
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||||
// Focus the main window when attempting to launch another instance
|
// Focus the main window when attempting to launch another instance
|
||||||
@@ -485,10 +529,9 @@ pub async fn run() {
|
|||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.plugin(
|
.plugin(tauri_plugin_sql::Builder::default()
|
||||||
tauri_plugin_sql::Builder::default()
|
.add_migrations("sqlite:database.db", migrations)
|
||||||
.add_migrations("sqlite:database.db", migrations)
|
.build(),
|
||||||
.build(),
|
|
||||||
)
|
)
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
@@ -570,6 +613,20 @@ pub async fn run() {
|
|||||||
.build(app)
|
.build(app)
|
||||||
.map_err(|e| format!("Failed to create tray: {}", e))?;
|
.map_err(|e| format!("Failed to create tray: {}", e))?;
|
||||||
|
|
||||||
|
// Fix tray icon in sandboxed environments (e.g., Flatpak)
|
||||||
|
// libappindicator uses the full path of the icon in dbus messages,
|
||||||
|
// so the path needs to be accessible from both the host and the sandbox.
|
||||||
|
// The default /tmp path doesn't work across sandbox boundaries.
|
||||||
|
if let Ok(local_data_path) = app
|
||||||
|
.path()
|
||||||
|
.resolve("tray-icon", BaseDirectory::AppLocalData)
|
||||||
|
{
|
||||||
|
let _ = fs::create_dir_all(&local_data_path);
|
||||||
|
let _ = tray.set_temp_dir_path(Some(local_data_path));
|
||||||
|
// Re-set the icon so it gets written to the new temp dir path
|
||||||
|
let _ = tray.set_icon(Some(app.default_window_icon().unwrap().clone()));
|
||||||
|
}
|
||||||
|
|
||||||
app.manage(tray);
|
app.manage(tray);
|
||||||
|
|
||||||
let window = app.get_webview_window("main").unwrap();
|
let window = app.get_webview_window("main").unwrap();
|
||||||
@@ -590,6 +647,7 @@ pub async fn run() {
|
|||||||
kill_all_process,
|
kill_all_process,
|
||||||
fetch_image,
|
fetch_image,
|
||||||
open_file_with_app,
|
open_file_with_app,
|
||||||
|
open_link_with_app,
|
||||||
list_ongoing_downloads,
|
list_ongoing_downloads,
|
||||||
pause_ongoing_downloads,
|
pause_ongoing_downloads,
|
||||||
send_to_extension,
|
send_to_extension,
|
||||||
@@ -600,6 +658,8 @@ pub async fn run() {
|
|||||||
get_config_file_path,
|
get_config_file_path,
|
||||||
restart_websocket_server,
|
restart_websocket_server,
|
||||||
get_current_app_path,
|
get_current_app_path,
|
||||||
|
is_flatpak,
|
||||||
|
get_appimage_path
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -3,5 +3,12 @@
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() {
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
neodlp_lib::run().await
|
neodlp_lib::run().await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "NeoDLP",
|
"productName": "NeoDLP",
|
||||||
"mainBinaryName": "neodlp",
|
"mainBinaryName": "neodlp",
|
||||||
"version": "0.4.0",
|
"version": "0.4.3",
|
||||||
"identifier": "com.neosubhamoy.neodlp",
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1080,
|
||||||
"height": 605,
|
"height": 680,
|
||||||
|
"decorations": false,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"targets": ["deb", "rpm"],
|
"targets": ["deb", "rpm"],
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
"licenseFile": "../LICENSE",
|
"licenseFile": "../LICENSE",
|
||||||
|
"category": "Utility",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -38,8 +40,12 @@
|
|||||||
"externalBin": [
|
"externalBin": [
|
||||||
"binaries/yt-dlp",
|
"binaries/yt-dlp",
|
||||||
"binaries/aria2c",
|
"binaries/aria2c",
|
||||||
"binaries/deno"
|
"binaries/deno",
|
||||||
|
"binaries/neodlp-pot"
|
||||||
],
|
],
|
||||||
|
"resources": {
|
||||||
|
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||||
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": ["ffmpeg"],
|
"depends": ["ffmpeg"],
|
||||||
|
|||||||
46
src-tauri/tauri.linux-flatpak.conf.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"identifier": "com.neosubhamoy.neodlp",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "NeoDLP",
|
||||||
|
"width": 1080,
|
||||||
|
"height": 680,
|
||||||
|
"decorations": false,
|
||||||
|
"visible": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null,
|
||||||
|
"capabilities": [
|
||||||
|
"default",
|
||||||
|
"shell-scope"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["deb"],
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
|
"licenseFile": "../LICENSE",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"externalBin": [
|
||||||
|
"binaries/deno"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,9 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1080,
|
||||||
"height": 605,
|
"height": 680,
|
||||||
|
"decorations": false,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"targets": ["deb", "rpm", "appimage"],
|
"targets": ["deb", "rpm", "appimage"],
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
"licenseFile": "../LICENSE",
|
"licenseFile": "../LICENSE",
|
||||||
|
"category": "Utility",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -38,8 +40,12 @@
|
|||||||
"externalBin": [
|
"externalBin": [
|
||||||
"binaries/yt-dlp",
|
"binaries/yt-dlp",
|
||||||
"binaries/aria2c",
|
"binaries/aria2c",
|
||||||
"binaries/deno"
|
"binaries/deno",
|
||||||
|
"binaries/neodlp-pot"
|
||||||
],
|
],
|
||||||
|
"resources": {
|
||||||
|
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||||
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": ["ffmpeg"],
|
"depends": ["ffmpeg"],
|
||||||
|
|||||||
@@ -39,13 +39,15 @@
|
|||||||
"binaries/yt-dlp",
|
"binaries/yt-dlp",
|
||||||
"binaries/ffmpeg",
|
"binaries/ffmpeg",
|
||||||
"binaries/ffprobe",
|
"binaries/ffprobe",
|
||||||
"binaries/deno"
|
"binaries/deno",
|
||||||
|
"binaries/neodlp-pot"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||||
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
||||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist",
|
||||||
|
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||||
},
|
},
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"providerShortName": "neosubhamoy"
|
"providerShortName": "neosubhamoy"
|
||||||
|
|||||||
@@ -39,13 +39,15 @@
|
|||||||
"binaries/yt-dlp",
|
"binaries/yt-dlp",
|
||||||
"binaries/ffmpeg",
|
"binaries/ffmpeg",
|
||||||
"binaries/ffprobe",
|
"binaries/ffprobe",
|
||||||
"binaries/deno"
|
"binaries/deno",
|
||||||
|
"binaries/neodlp-pot"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
"target/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||||
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
|
||||||
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
|
||||||
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
|
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist",
|
||||||
|
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||||
},
|
},
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"providerShortName": "neosubhamoy"
|
"providerShortName": "neosubhamoy"
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "NeoDLP",
|
"title": "NeoDLP",
|
||||||
"width": 1067,
|
"width": 1080,
|
||||||
"height": 605,
|
"height": 680,
|
||||||
|
"decorations": false,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -40,12 +41,14 @@
|
|||||||
"binaries/ffmpeg",
|
"binaries/ffmpeg",
|
||||||
"binaries/ffprobe",
|
"binaries/ffprobe",
|
||||||
"binaries/aria2c",
|
"binaries/aria2c",
|
||||||
"binaries/deno"
|
"binaries/deno",
|
||||||
|
"binaries/neodlp-pot"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
||||||
"resources/msghost-manifest/windows/chrome.json": "chrome.json",
|
"resources/msghost-manifest/windows/chrome.json": "chrome.json",
|
||||||
"resources/msghost-manifest/windows/firefox.json": "firefox.json"
|
"resources/msghost-manifest/windows/firefox.json": "firefox.json",
|
||||||
|
"resources/plugins/yt-dlp-plugins/": "yt-dlp-plugins/"
|
||||||
},
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
"wix": {
|
"wix": {
|
||||||
|
|||||||
165
src/App.tsx
@@ -4,14 +4,14 @@ import { AppContext } from "@/providers/appContextProvider";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
||||||
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
||||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useEnvironmentStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { isObjEmpty} from "@/utils";
|
import { isObjEmpty} from "@/utils";
|
||||||
import { Command } from "@tauri-apps/plugin-shell";
|
import { Command } from "@tauri-apps/plugin-shell";
|
||||||
import { useUpdateDownloadStatus } from "@/services/mutations";
|
import { useUpdateDownloadStatus } from "@/services/mutations";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useFetchAllDownloadStates, useFetchAllkVPairs, useFetchAllSettings } from "@/services/queries";
|
import { useFetchAllDownloadStates, useFetchAllkVPairs, useFetchAllSettings } from "@/services/queries";
|
||||||
import { config } from "@/config";
|
import { config } from "@/config";
|
||||||
import * as fs from "@tauri-apps/plugin-fs";
|
// import * as fs from "@tauri-apps/plugin-fs";
|
||||||
import { useYtDlpUpdater } from "@/helpers/use-ytdlp-updater";
|
import { useYtDlpUpdater } from "@/helpers/use-ytdlp-updater";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
@@ -25,6 +25,10 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useLogger } from "@/helpers/use-logger";
|
import { useLogger } from "@/helpers/use-logger";
|
||||||
import useDownloader from "@/helpers/use-downloader";
|
import useDownloader from "@/helpers/use-downloader";
|
||||||
|
import usePotServer from "@/helpers/use-pot-server";
|
||||||
|
import { useLinuxRegisterer } from "@/helpers/use-linux-registerer";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
|
||||||
export default function App({ children }: { children: React.ReactNode }) {
|
export default function App({ children }: { children: React.ReactNode }) {
|
||||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||||
@@ -36,9 +40,14 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const setDownloadStates = useDownloadStatesStore((state) => state.setDownloadStates);
|
const setDownloadStates = useDownloadStatesStore((state) => state.setDownloadStates);
|
||||||
const setPath = useBasePathsStore((state) => state.setPath);
|
const setPath = useBasePathsStore((state) => state.setPath);
|
||||||
|
|
||||||
|
const setIsFlatpak = useEnvironmentStore((state) => state.setIsFlatpak);
|
||||||
|
const setIsAppimage = useEnvironmentStore((state) => state.setIsAppimage);
|
||||||
|
const setAppDirPath = useEnvironmentStore((state) => state.setAppDirPath);
|
||||||
|
|
||||||
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
|
||||||
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
|
||||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||||
|
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||||
const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion);
|
const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion);
|
||||||
const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion);
|
const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion);
|
||||||
const setIsFetchingYtDlpVersion = useSettingsPageStatesStore((state) => state.setIsFetchingYtDlpVersion);
|
const setIsFetchingYtDlpVersion = useSettingsPageStatesStore((state) => state.setIsFetchingYtDlpVersion);
|
||||||
@@ -50,6 +59,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
download_dir: DOWNLOAD_DIR,
|
download_dir: DOWNLOAD_DIR,
|
||||||
theme: APP_THEME,
|
theme: APP_THEME,
|
||||||
color_scheme: APP_COLOR_SCHEME,
|
color_scheme: APP_COLOR_SCHEME,
|
||||||
|
use_potoken: USE_POTOKEN,
|
||||||
} = useSettingsPageStatesStore(state => state.settings);
|
} = useSettingsPageStatesStore(state => state.settings);
|
||||||
|
|
||||||
const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds);
|
const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds);
|
||||||
@@ -63,10 +73,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const currentPlatform = platform();
|
const currentPlatform = platform();
|
||||||
const { updateYtDlp } = useYtDlpUpdater();
|
const { updateYtDlp } = useYtDlpUpdater();
|
||||||
const { registerToMac } = useMacOsRegisterer();
|
const { registerToMac } = useMacOsRegisterer();
|
||||||
|
const { registerToLinux } = useLinuxRegisterer();
|
||||||
const { checkForAppUpdate } = useAppUpdater();
|
const { checkForAppUpdate } = useAppUpdater();
|
||||||
|
const { startPotServer, stopPotServer } = usePotServer();
|
||||||
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
|
const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey);
|
||||||
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
|
const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check);
|
||||||
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
|
const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version);
|
||||||
|
const linuxRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.linux_registered_version);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const downloadStatusUpdater = useUpdateDownloadStatus();
|
const downloadStatusUpdater = useUpdateDownloadStatus();
|
||||||
@@ -76,7 +89,9 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||||
const hasRunAppUpdateCheckRef = useRef(false);
|
const hasRunAppUpdateCheckRef = useRef(false);
|
||||||
|
const hasRunPotServerStatusCheckRef = useRef(false);
|
||||||
const isRegisteredToMacOsRef = useRef(false);
|
const isRegisteredToMacOsRef = useRef(false);
|
||||||
|
const isRegisteredToLinuxRef = useRef(false);
|
||||||
const pendingErrorUpdatesRef = useRef<Set<string>>(new Set());
|
const pendingErrorUpdatesRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader();
|
const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader();
|
||||||
@@ -98,6 +113,39 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
appWindow.onCloseRequested(handleCloseRequested);
|
appWindow.onCloseRequested(handleCloseRequested);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup before page refresh/unload
|
||||||
|
useEffect(() => {
|
||||||
|
const handleBeforeUnload = (_event: BeforeUnloadEvent) => {
|
||||||
|
if (isRunningPotServer) {
|
||||||
|
stopPotServer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
};
|
||||||
|
}, [stopPotServer]);
|
||||||
|
|
||||||
|
// Detect sandbox environments
|
||||||
|
useEffect(() => {
|
||||||
|
const detectEnvironment = async () => {
|
||||||
|
try {
|
||||||
|
const isFlatpak = await invoke<boolean>('is_flatpak');
|
||||||
|
const appimagePath = await invoke<string | null>('get_appimage_path');
|
||||||
|
console.log('Environment detection results:', { isFlatpak, appimagePath });
|
||||||
|
|
||||||
|
if (isFlatpak) setIsFlatpak(true);
|
||||||
|
if (appimagePath) {
|
||||||
|
setIsAppimage(true);
|
||||||
|
setAppDirPath(appimagePath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to detect environment:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detectEnvironment();
|
||||||
|
}, [setIsFlatpak, setIsAppimage, setAppDirPath]);
|
||||||
|
|
||||||
// Listen for websocket messages
|
// Listen for websocket messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
|
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
|
||||||
@@ -160,22 +208,23 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
const currentArch = arch();
|
const currentArch = arch();
|
||||||
const currentExeExtension = exeExtension();
|
const currentExeExtension = exeExtension();
|
||||||
|
const isFlatpak = await invoke<boolean>('is_flatpak');
|
||||||
const downloadDirPath = await downloadDir();
|
const downloadDirPath = await downloadDir();
|
||||||
const tempDirPath = await tempDir();
|
const tempDirPath = await tempDir();
|
||||||
const resourceDirPath = await resourceDir();
|
const resourceDirPath = await resourceDir();
|
||||||
|
|
||||||
const ffmpegPath = await join(resourceDirPath, 'binaries', `ffmpeg-${currentArch}${currentExeExtension ? '.' + currentExeExtension : ''}`);
|
const ffmpegPath = await join(resourceDirPath, 'binaries', `ffmpeg-${currentArch}${currentExeExtension ? '.' + currentExeExtension : ''}`);
|
||||||
const tempDownloadDirPath = await join(tempDirPath, config.appPkgName, 'downloads');
|
const tempDownloadDirPath = isFlatpak ? await join(downloadDirPath, config.appName, '.tempdownloads') : await join(tempDirPath, config.appPkgName, 'downloads');
|
||||||
const appDownloadDirPath = await join(downloadDirPath, config.appName);
|
const appDownloadDirPath = await join(downloadDirPath, config.appName);
|
||||||
|
|
||||||
if (!await fs.exists(tempDownloadDirPath)) fs.mkdir(tempDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${tempDownloadDirPath}`) });
|
// if (!await fs.exists(tempDownloadDirPath)) fs.mkdir(tempDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${tempDownloadDirPath}`) });
|
||||||
|
|
||||||
setPath('ffmpegPath', ffmpegPath);
|
setPath('ffmpegPath', ffmpegPath);
|
||||||
setPath('tempDownloadDirPath', tempDownloadDirPath);
|
setPath('tempDownloadDirPath', tempDownloadDirPath);
|
||||||
if (DOWNLOAD_DIR) {
|
if (DOWNLOAD_DIR) {
|
||||||
setPath('downloadDirPath', DOWNLOAD_DIR);
|
setPath('downloadDirPath', DOWNLOAD_DIR);
|
||||||
} else {
|
} else {
|
||||||
if(!await fs.exists(appDownloadDirPath)) fs.mkdir(appDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${appDownloadDirPath}`) });
|
// if(!await fs.exists(appDownloadDirPath)) fs.mkdir(appDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${appDownloadDirPath}`) });
|
||||||
setPath('downloadDirPath', appDownloadDirPath);
|
setPath('downloadDirPath', appDownloadDirPath);
|
||||||
}
|
}
|
||||||
console.log('Paths initialized:', { ffmpegPath, tempDownloadDirPath, downloadDirPath: DOWNLOAD_DIR || appDownloadDirPath });
|
console.log('Paths initialized:', { ffmpegPath, tempDownloadDirPath, downloadDirPath: DOWNLOAD_DIR || appDownloadDirPath });
|
||||||
@@ -208,7 +257,10 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const fetchYtDlpVersion = async () => {
|
const fetchYtDlpVersion = async () => {
|
||||||
setIsFetchingYtDlpVersion(true);
|
setIsFetchingYtDlpVersion(true);
|
||||||
try {
|
try {
|
||||||
const command = Command.sidecar('binaries/yt-dlp', ['--version']);
|
const isFlatpak = await invoke<boolean>('is_flatpak');
|
||||||
|
const command = isFlatpak
|
||||||
|
? Command.create('sh', ['-c', `yt-dlp --version`])
|
||||||
|
: Command.sidecar('binaries/yt-dlp', ['--version']);
|
||||||
const output = await command.execute();
|
const output = await command.execute();
|
||||||
if (output.code === 0) {
|
if (output.code === 0) {
|
||||||
const version = output.stdout.trim();
|
const version = output.stdout.trim();
|
||||||
@@ -245,30 +297,64 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||||
|
|
||||||
// Check for yt-dlp auto-update
|
// Check for yt-dlp auto-update
|
||||||
|
useEffect(() => {
|
||||||
|
const handleYtDlpAutoUpdate = async () => {
|
||||||
|
// Only run once when both settings and KV pairs are loaded
|
||||||
|
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||||
|
console.log("Skipping yt-dlp auto-update check, waiting for configs to load...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip if we've already run the auto-update once
|
||||||
|
if (hasRunYtDlpAutoUpdateRef.current) {
|
||||||
|
console.log("Auto-update check already performed in this session, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isFlatpak = await invoke<boolean>('is_flatpak');
|
||||||
|
if (isFlatpak) {
|
||||||
|
console.log("Flatpak detected! Skipping yt-dlp auto-update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasRunYtDlpAutoUpdateRef.current = true;
|
||||||
|
console.log("Checking yt-dlp auto-update with loaded config values:", {
|
||||||
|
autoUpdate: YTDLP_AUTO_UPDATE,
|
||||||
|
updateChannel: YTDLP_UPDATE_CHANNEL,
|
||||||
|
lastCheck: ytDlpUpdateLastCheck
|
||||||
|
});
|
||||||
|
const currentTimestamp = Date.now()
|
||||||
|
const YTDLP_UPDATE_INTERVAL = 86400000 // 24H;
|
||||||
|
if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) {
|
||||||
|
console.log("Running auto-update for yt-dlp...");
|
||||||
|
updateYtDlp();
|
||||||
|
} else {
|
||||||
|
console.log("Skipping yt-dlp auto-update, either disabled or recently updated.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleYtDlpAutoUpdate()
|
||||||
|
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||||
|
|
||||||
|
// Check POT server status and auto-start if enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only run once when both settings and KV pairs are loaded
|
// Only run once when both settings and KV pairs are loaded
|
||||||
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||||
console.log("Skipping yt-dlp auto-update check, waiting for configs to load...");
|
console.log("Skipping POT server status check, waiting for configs to load...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Skip if we've already run the auto-update once
|
// Skip if we've already run the POT server status check once
|
||||||
if (hasRunYtDlpAutoUpdateRef.current) {
|
if (hasRunPotServerStatusCheckRef.current) {
|
||||||
console.log("Auto-update check already performed in this session, skipping");
|
console.log("POT server status check already performed in this session, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hasRunYtDlpAutoUpdateRef.current = true;
|
hasRunPotServerStatusCheckRef.current = true;
|
||||||
console.log("Checking yt-dlp auto-update with loaded config values:", {
|
console.log("Checking POT server status with loaded config values:", {
|
||||||
autoUpdate: YTDLP_AUTO_UPDATE,
|
usePotoken: USE_POTOKEN,
|
||||||
updateChannel: YTDLP_UPDATE_CHANNEL,
|
|
||||||
lastCheck: ytDlpUpdateLastCheck
|
|
||||||
});
|
});
|
||||||
const currentTimestamp = Date.now()
|
if (USE_POTOKEN) {
|
||||||
const YTDLP_UPDATE_INTERVAL = 86400000 // 24H;
|
console.log("Auto-starting POT server...");
|
||||||
if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) {
|
startPotServer().catch((error) => {
|
||||||
console.log("Running auto-update for yt-dlp...");
|
console.error("Error starting POT server:", error);
|
||||||
updateYtDlp();
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("Skipping yt-dlp auto-update, either disabled or recently updated.");
|
console.log("Skipping POT server auto-start, not enabled.");
|
||||||
}
|
}
|
||||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||||
|
|
||||||
@@ -307,6 +393,41 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||||
|
|
||||||
|
// Check for Linux auto-registration
|
||||||
|
useEffect(() => {
|
||||||
|
// Only run once when both settings and KV pairs are loaded
|
||||||
|
if (!isSettingsStatePropagated || !isKvPairsStatePropagated) {
|
||||||
|
console.log("Skipping Linux auto registration, waiting for configs to load...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip if we've already run the linux auto-registration once
|
||||||
|
if (isRegisteredToLinuxRef.current) {
|
||||||
|
console.log("Linux auto registration check already performed in this session, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRegisteredToLinuxRef.current = true;
|
||||||
|
console.log("Checking Linux auto registration with loaded config values:", {
|
||||||
|
appVersion: appVersion,
|
||||||
|
registeredVersion: linuxRegisteredVersion
|
||||||
|
});
|
||||||
|
if (currentPlatform === 'linux' && (!linuxRegisteredVersion || linuxRegisteredVersion !== appVersion)) {
|
||||||
|
console.log("Running Linux auto registration...");
|
||||||
|
LOG.info('NEODLP', 'Running Linux registration');
|
||||||
|
registerToLinux().then((result: { success: boolean, message: string }) => {
|
||||||
|
if (result.success) {
|
||||||
|
console.log("Linux registration successful:", result.message);
|
||||||
|
LOG.info('NEODLP', 'Linux registration successful');
|
||||||
|
} else {
|
||||||
|
console.error("Linux registration failed:", result.message);
|
||||||
|
LOG.error('NEODLP', `Linux registration failed: ${result.message}`);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Error during Linux registration:", error);
|
||||||
|
LOG.error('NEODLP', `Error during Linux registration: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isSettingsStatePropagated, isKvPairsStatePropagated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSuccessFetchingDownloadStates && downloadStates) {
|
if (isSuccessFetchingDownloadStates && downloadStates) {
|
||||||
// console.log("Download States fetched successfully:", downloadStates);
|
// console.log("Download States fetched successfully:", downloadStates);
|
||||||
@@ -341,7 +462,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutIds: NodeJS.Timeout[] = [];
|
const timeoutIds: ReturnType<typeof setTimeout>[] = [];
|
||||||
unexpectedErrors.forEach((downloadId) => {
|
unexpectedErrors.forEach((downloadId) => {
|
||||||
pendingErrorUpdatesRef.current.add(downloadId);
|
pendingErrorUpdatesRef.current.add(downloadId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { VideoFormat } from "@/types/video"
|
import { VideoFormat } from "@/types/video"
|
||||||
import { determineFileType, formatBitrate, formatCodec, formatFileSize } from "@/utils"
|
import { determineFileType, formatBitrate, formatCodec, formatFileSize } from "@/utils"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
|
||||||
import { type VariantProps } from "class-variance-authority";
|
import { type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { toggleVariants } from "@/components/ui/toggle";
|
import { toggleVariants } from "@/components/ui/toggle";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import { Progress as ProgressPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -33,4 +33,4 @@ const IndeterminateProgress = React.forwardRef<
|
|||||||
));
|
));
|
||||||
IndeterminateProgress.displayName = ProgressPrimitive.Root.displayName;
|
IndeterminateProgress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { IndeterminateProgress };
|
export { IndeterminateProgress };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
||||||
import { type VariantProps } from "class-variance-authority"
|
import { type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -56,4 +56,4 @@ const ToggleGroupItem = React.forwardRef<
|
|||||||
|
|
||||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem }
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
|
|||||||
110
src/components/custom/numberInput.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Minus, Plus } from "lucide-react"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface NumberInputProps
|
||||||
|
extends Omit<React.ComponentProps<"input">, "type" | "onChange" | "value"> {
|
||||||
|
value?: number
|
||||||
|
defaultValue?: number
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
step?: number
|
||||||
|
onChange?: (value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
value: controlledValue,
|
||||||
|
defaultValue = 0,
|
||||||
|
min = -Infinity,
|
||||||
|
max = Infinity,
|
||||||
|
step = 1,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
readOnly,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [internalValue, setInternalValue] = React.useState(defaultValue)
|
||||||
|
const isControlled = controlledValue !== undefined
|
||||||
|
const currentValue = isControlled ? controlledValue : internalValue
|
||||||
|
|
||||||
|
const updateValue = (newValue: number) => {
|
||||||
|
const clamped = Math.min(max, Math.max(min, newValue))
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalValue(clamped)
|
||||||
|
}
|
||||||
|
onChange?.(clamped)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIncrement = () => {
|
||||||
|
if (!disabled && !readOnly) {
|
||||||
|
updateValue(currentValue + step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDecrement = () => {
|
||||||
|
if (!disabled && !readOnly) {
|
||||||
|
updateValue(currentValue - step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const parsed = parseFloat(e.target.value)
|
||||||
|
if (!isNaN(parsed)) {
|
||||||
|
updateValue(parsed)
|
||||||
|
} else if (e.target.value === "" || e.target.value === "-") {
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalValue(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative flex items-center", className)}>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
ref={ref}
|
||||||
|
value={currentValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
disabled={disabled}
|
||||||
|
readOnly={readOnly}
|
||||||
|
className="pr-16 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield] focus-visible:ring-0"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-0 flex h-full items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDecrement}
|
||||||
|
disabled={disabled || readOnly || currentValue <= min}
|
||||||
|
className="flex h-full items-center justify-center px-2 text-muted-foreground hover:text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50 transition-colors border-x"
|
||||||
|
aria-label="Decrement"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<Minus className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleIncrement}
|
||||||
|
disabled={disabled || readOnly || currentValue >= max}
|
||||||
|
className="flex h-full items-center justify-center px-2 text-muted-foreground hover:text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50 transition-colors rounded-r-md"
|
||||||
|
aria-label="Increment"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
NumberInput.displayName = "NumberInput"
|
||||||
|
|
||||||
|
export { NumberInput }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { RawVideoInfo } from "@/types/video"
|
import { RawVideoInfo } from "@/types/video"
|
||||||
import { formatDurationString} from "@/utils"
|
import { formatDurationString} from "@/utils"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
|
||||||
import { type VariantProps } from "class-variance-authority";
|
import { type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { toggleVariants } from "@/components/ui/toggle";
|
import { toggleVariants } from "@/components/ui/toggle";
|
||||||
|
|||||||
12
src/components/icons/close.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export function CloseIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/icons/maximize.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function MaximizeIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
|
||||||
|
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/components/icons/minimize.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function MinimizeIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
|
||||||
|
<path fill="currentColor" d="M14 8v1H3V8z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/components/icons/unmaximize.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function UnmaximizeIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" className={className}>
|
||||||
|
<g fill="currentColor">
|
||||||
|
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
|
||||||
|
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,10 +8,13 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { useLogger } from "@/helpers/use-logger";
|
import { useLogger } from "@/helpers/use-logger";
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import TitleBar from "@/components/titlebar";
|
||||||
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const currentPlatform = platform();
|
||||||
const logger = useLogger();
|
const logger = useLogger();
|
||||||
const logs = logger.getLogs();
|
const logs = logger.getLogs();
|
||||||
const logText = logs.map(log => `${new Date(log.timestamp).toLocaleTimeString()} [${log.level.toUpperCase()}] ${log.context}: ${log.message}`).join('\n');
|
const logText = logs.map(log => `${new Date(log.timestamp).toLocaleTimeString()} [${log.level.toUpperCase()}] ${log.context}: ${log.message}`).join('\n');
|
||||||
@@ -23,67 +26,74 @@ export default function Navbar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-backdrop-filter:bg-background/60 border-b z-50">
|
<div className="sticky top-0 z-50">
|
||||||
<div className="flex justify-center">
|
{currentPlatform === "windows" || currentPlatform === "linux" ? (
|
||||||
<SidebarTrigger />
|
<TitleBar />
|
||||||
<h1 className="text-lg font-semibold ml-4">{getRouteName(location.pathname)}</h1>
|
) : (
|
||||||
</div>
|
null
|
||||||
<div className="flex justify-center items-center">
|
)}
|
||||||
<Dialog>
|
<nav className="flex justify-between items-center py-3 px-4 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
|
||||||
<Tooltip>
|
<div className="flex justify-center">
|
||||||
<TooltipTrigger asChild>
|
<SidebarTrigger />
|
||||||
<DialogTrigger asChild>
|
<h1 className="text-lg font-semibold ml-4">{getRouteName(location.pathname)}</h1>
|
||||||
<Button variant="outline" size="icon">
|
</div>
|
||||||
<Terminal />
|
<div className="flex justify-center items-center">
|
||||||
</Button>
|
<Dialog>
|
||||||
</DialogTrigger>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipContent>
|
<DialogTrigger asChild>
|
||||||
<p>Logs</p>
|
<Button variant="outline" size="icon">
|
||||||
</TooltipContent>
|
<Terminal />
|
||||||
</Tooltip>
|
</Button>
|
||||||
<DialogContent className="sm:max-w-150">
|
</DialogTrigger>
|
||||||
<DialogHeader>
|
</TooltipTrigger>
|
||||||
<DialogTitle>Log Viewer</DialogTitle>
|
<TooltipContent side="bottom">
|
||||||
<DialogDescription>Monitor real-time app session logs (latest on top)</DialogDescription>
|
<p>Logs</p>
|
||||||
</DialogHeader>
|
</TooltipContent>
|
||||||
<div className="flex flex-col gap-2 p-2 max-h-75 overflow-y-scroll overflow-x-hidden bg-muted">
|
</Tooltip>
|
||||||
{logs.length === 0 ? (
|
<DialogContent className="sm:max-w-150">
|
||||||
<p className="text-sm text-muted-foreground">NO LOGS TO SHOW!</p>
|
<DialogHeader>
|
||||||
) : (
|
<DialogTitle>Log Viewer</DialogTitle>
|
||||||
logs.slice().reverse().map((log, index) => (
|
<DialogDescription>Monitor real-time app session logs (latest on top)</DialogDescription>
|
||||||
<div key={index} className={`flex flex-col ${log.level === 'error' ? 'text-red-500' : log.level === 'warning' ? 'text-amber-500' : log.level === 'debug' ? 'text-sky-500' : log.level === 'progress' ? 'text-emerald-500' : 'text-foreground'}`}>
|
</DialogHeader>
|
||||||
<p className="text-xs"><strong>{new Date(log.timestamp).toLocaleTimeString()}</strong> [{log.level.toUpperCase()}] <em>{log.context}</em> :</p>
|
<div className="flex flex-col gap-2 p-2 max-h-75 overflow-y-scroll overflow-x-hidden bg-muted">
|
||||||
<p className="text-xs font-mono break-all">{log.message}</p>
|
{logs.length === 0 ? (
|
||||||
</div>
|
<p className="text-sm text-muted-foreground">NO LOGS TO SHOW!</p>
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
disabled={logs.length === 0}
|
|
||||||
onClick={() => logger.clearLogs()}
|
|
||||||
>
|
|
||||||
<BrushCleaning className="size-4" />
|
|
||||||
Clear Logs
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="transition-all duration-300"
|
|
||||||
disabled={logs.length === 0}
|
|
||||||
onClick={() => handleCopyLogs()}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="size-4" />
|
|
||||||
) : (
|
) : (
|
||||||
<Copy className="size-4" />
|
logs.slice().reverse().map((log, index) => (
|
||||||
|
<div key={index} className={`flex flex-col ${log.level === 'error' ? 'text-red-500' : log.level === 'warning' ? 'text-amber-500' : log.level === 'debug' ? 'text-sky-500' : log.level === 'progress' ? 'text-emerald-500' : 'text-foreground'}`}>
|
||||||
|
<p className="text-xs"><strong>{new Date(log.timestamp).toLocaleTimeString()}</strong> [{log.level.toUpperCase()}] <em>{log.context}</em> :</p>
|
||||||
|
<p className="text-xs font-mono break-all">{log.message}</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
Copy Logs
|
</div>
|
||||||
</Button>
|
<DialogFooter>
|
||||||
</DialogFooter>
|
<Button
|
||||||
</DialogContent>
|
variant="destructive"
|
||||||
</Dialog>
|
disabled={logs.length === 0}
|
||||||
</div>
|
onClick={() => logger.clearLogs()}
|
||||||
</nav>
|
>
|
||||||
|
<BrushCleaning className="size-4" />
|
||||||
|
Clear Logs
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="transition-all duration-300"
|
||||||
|
disabled={logs.length === 0}
|
||||||
|
onClick={() => handleCopyLogs()}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-4" />
|
||||||
|
)}
|
||||||
|
Copy Logs
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ function DownloadConfigDialog({ selectedFormatFileType }: DownloadConfigDialogPr
|
|||||||
|
|
||||||
export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileType, selectedVideoFormat, selectedAudioFormats, containerRef }: BottomBarProps) {
|
export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileType, selectedVideoFormat, selectedAudioFormats, containerRef }: BottomBarProps) {
|
||||||
const { startDownload } = useAppContext();
|
const { startDownload } = useAppContext();
|
||||||
console.log(selectedAudioFormats);
|
|
||||||
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
||||||
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
||||||
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custo
|
|||||||
import { getMergedBestFormat } from "@/utils";
|
import { getMergedBestFormat } from "@/utils";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { FormatToggleGroup, FormatToggleGroupItem } from "@/components/custom/formatToggleGroup";
|
import { FormatToggleGroup, FormatToggleGroupItem } from "@/components/custom/formatToggleGroup";
|
||||||
|
import { Layout } from "react-resizable-panels";
|
||||||
|
|
||||||
interface PlaylistPreviewSelectionProps {
|
interface PlaylistPreviewSelectionProps {
|
||||||
videoMetadata: RawVideoInfo;
|
videoMetadata: RawVideoInfo;
|
||||||
@@ -348,18 +349,22 @@ export function PlaylistDownloader({ videoMetadata, audioOnlyFormats, videoOnlyF
|
|||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction="horizontal"
|
orientation="horizontal"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onLayout={(sizes) => setPlaylistPanelSizes(sizes)}
|
onLayoutChanged={(layout: Layout) => {
|
||||||
|
const firstPanelSize = layout[Object.keys(layout)[0]];
|
||||||
|
const secondPanelSize = layout[Object.keys(layout)[1]];
|
||||||
|
setPlaylistPanelSizes([firstPanelSize, secondPanelSize]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={playlistPanelSizes[0]}
|
defaultSize={`${playlistPanelSizes[0]}%`}
|
||||||
>
|
>
|
||||||
<PlaylistPreviewSelection videoMetadata={videoMetadata} />
|
<PlaylistPreviewSelection videoMetadata={videoMetadata} />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={playlistPanelSizes[1]}
|
defaultSize={`${playlistPanelSizes[1]}%`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col w-full pl-4">
|
<div className="flex flex-col w-full pl-4">
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||||
import { FormatToggleGroup, FormatToggleGroupItem } from "@/components/custom/formatToggleGroup";
|
import { FormatToggleGroup, FormatToggleGroupItem } from "@/components/custom/formatToggleGroup";
|
||||||
|
import { Layout } from "react-resizable-panels";
|
||||||
|
|
||||||
interface VideoPreviewProps {
|
interface VideoPreviewProps {
|
||||||
videoMetadata: RawVideoInfo;
|
videoMetadata: RawVideoInfo;
|
||||||
@@ -325,18 +326,22 @@ export function VideoDownloader({ videoMetadata, audioOnlyFormats, videoOnlyForm
|
|||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction="horizontal"
|
orientation="horizontal"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onLayout={(sizes) => setVideoPanelSizes(sizes)}
|
onLayoutChanged={(layout: Layout) => {
|
||||||
|
const firstPanelSize = layout[Object.keys(layout)[0]];
|
||||||
|
const secondPanelSize = layout[Object.keys(layout)[1]];
|
||||||
|
setVideoPanelSizes([firstPanelSize, secondPanelSize]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={videoPanelSizes[0]}
|
defaultSize={`${videoPanelSizes[0]}%`}
|
||||||
>
|
>
|
||||||
<VideoPreview videoMetadata={videoMetadata} />
|
<VideoPreview videoMetadata={videoMetadata} />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={videoPanelSizes[1]}
|
defaultSize={`${videoPanelSizes[1]}%`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col w-full pl-4">
|
<div className="flex flex-col w-full pl-4">
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore, useEnvironmentStore, useLibraryPageStatesStore } from "@/services/store";
|
||||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, paginate } from "@/utils";
|
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, paginate } from "@/utils";
|
||||||
import { ArrowUpRightIcon, AudioLines, CircleArrowDown, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Music, Play, Search, Trash2, Video } from "lucide-react";
|
import { ArrowUpRightIcon, AudioLines, CircleArrowDown, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Music, Play, Search, Trash2, Video } from "lucide-react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -33,6 +33,8 @@ export function CompletedDownload({ state }: CompletedDownloadProps) {
|
|||||||
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
||||||
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
||||||
|
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const downloadStateDeleter = useDeleteDownloadState();
|
const downloadStateDeleter = useDeleteDownloadState();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -276,10 +278,12 @@ export function CompletedDownload({ state }: CompletedDownloadProps) {
|
|||||||
<Play className="w-4 h-4" />
|
<Play className="w-4 h-4" />
|
||||||
Open
|
Open
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
{!isFlatpak && (
|
||||||
<FolderInput className="w-4 h-4" />
|
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
||||||
Reveal
|
<FolderInput className="w-4 h-4" />
|
||||||
</Button>
|
Reveal
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button size="sm" variant="outline" onClick={() => handleSearch(state.url, state.playlist_id ? true : false)}>
|
<Button size="sm" variant="outline" onClick={() => handleSearch(state.url, state.playlist_id ? true : false)}>
|
||||||
<Search className="w-4 h-4" />
|
<Search className="w-4 h-4" />
|
||||||
Search
|
Search
|
||||||
@@ -352,7 +356,7 @@ export function CompletedDownloads({ downloads }: CompletedDownloadsProps) {
|
|||||||
<Empty className="mt-10">
|
<Empty className="mt-10">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<CircleArrowDown />
|
<CircleArrowDown className="stroke-primary" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No Completed Downloads</EmptyTitle>
|
<EmptyTitle>No Completed Downloads</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
|
|||||||
) : (
|
) : (
|
||||||
<span>{state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)}</span>
|
<span>{state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)}</span>
|
||||||
)} {
|
)} {
|
||||||
(debugMode && state.download_id) || (state.download_status === 'errored' && state.download_id) && (
|
(debugMode && state.download_id) || (state.download_status === 'errored' && state.download_id) ? (
|
||||||
<><span className="text-primary">•</span> ID: {state.download_id.toUpperCase()}</>
|
<><span className="text-primary">•</span> ID: {state.download_id.toUpperCase()}</>
|
||||||
)} {
|
) : null} {
|
||||||
state.download_status === 'downloading' && state.status !== 'finished' && state.speed && (
|
state.download_status === 'downloading' && state.status !== 'finished' && state.speed && (
|
||||||
<><span className="text-primary">•</span> Speed: {formatSpeed(state.speed)}</>
|
<><span className="text-primary">•</span> Speed: {formatSpeed(state.speed)}</>
|
||||||
)} {state.download_status === 'downloading' && state.eta && (
|
)} {state.download_status === 'downloading' && state.eta && (
|
||||||
@@ -280,7 +280,7 @@ export function IncompleteDownloads({ downloads }: IncompleteDownloadsProps) {
|
|||||||
<Empty className="mt-10">
|
<Empty className="mt-10">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<CircleCheck />
|
<CircleCheck className="stroke-primary" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No Incomplete Downloads</EmptyTitle>
|
<EmptyTitle>No Incomplete Downloads</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { useBasePathsStore, useDownloaderPageStatesStore, useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
import { useBasePathsStore, useDownloaderPageStatesStore, useDownloadStatesStore, useEnvironmentStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
import { BadgeCheck, BellRing, BrushCleaning, Bug, CircleCheck, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, KeyRound, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Timer, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -34,6 +34,8 @@ import { config } from "@/config";
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg";
|
import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { NumberInput } from "@/components/custom/numberInput";
|
||||||
|
import usePotServer from "@/helpers/use-pot-server";
|
||||||
|
|
||||||
const proxyUrlSchema = z.object({
|
const proxyUrlSchema = z.object({
|
||||||
url: z.url({
|
url: z.url({
|
||||||
@@ -66,6 +68,59 @@ const filenameTemplateShcema = z.object({
|
|||||||
template: z.string().min(1, { message: "Filename Template is required" }),
|
template: z.string().min(1, { message: "Filename Template is required" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const minMaxSleepIntervalSchema = z.object({
|
||||||
|
min_sleep_interval: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Minimum Sleep Interval is required"
|
||||||
|
: "Minimum Sleep Interval must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Minimum Sleep Interval must be an integer"
|
||||||
|
}).min(1, {
|
||||||
|
message: "Minimum Sleep Interval must be at least 1 second"
|
||||||
|
}).max(3600, {
|
||||||
|
message: "Minimum Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||||
|
}),
|
||||||
|
max_sleep_interval: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Maximum Sleep Interval is required"
|
||||||
|
: "Maximum Sleep Interval must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Maximum Sleep Interval must be an integer"
|
||||||
|
}).min(1, {
|
||||||
|
message: "Maximum Sleep Interval must be at least 1 second"
|
||||||
|
}).max(3600, {
|
||||||
|
message: "Maximum Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestSleepIntervalSchema = z.object({
|
||||||
|
request_sleep_interval: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Request Sleep Interval is required"
|
||||||
|
: "Request Sleep Interval must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Request Sleep Interval must be an integer"
|
||||||
|
}).min(1, {
|
||||||
|
message: "Request Sleep Interval must be at least 1 second"
|
||||||
|
}).max(3600, {
|
||||||
|
message: "Request Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const potServerPortSchema = z.object({
|
||||||
|
port: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "POT Server Port is required"
|
||||||
|
: "POT Server Port must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "POT Server Port must be an integer"
|
||||||
|
}).min(4000, {
|
||||||
|
message: "POT Server Port must be at least 4000"
|
||||||
|
}).max(5000, {
|
||||||
|
message: "POT Server Port must be at most 5000"
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
function AppGeneralSettings() {
|
function AppGeneralSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
@@ -227,9 +282,11 @@ function AppAppearanceSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppFolderSettings() {
|
function AppFilesystemSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||||
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||||
|
|
||||||
@@ -238,6 +295,8 @@ function AppFolderSettings() {
|
|||||||
const setPath = useBasePathsStore((state) => state.setPath);
|
const setPath = useBasePathsStore((state) => state.setPath);
|
||||||
|
|
||||||
const filenameTemplate = useSettingsPageStatesStore(state => state.settings.filename_template);
|
const filenameTemplate = useSettingsPageStatesStore(state => state.settings.filename_template);
|
||||||
|
const windowsFilenames = useSettingsPageStatesStore(state => state.settings.windows_filenames);
|
||||||
|
const restrictFilenames = useSettingsPageStatesStore(state => state.settings.restrict_filenames);
|
||||||
|
|
||||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||||
const ongoingDownloads = downloadStates.filter(state =>
|
const ongoingDownloads = downloadStates.filter(state =>
|
||||||
@@ -309,6 +368,7 @@ function AppFolderSettings() {
|
|||||||
<Input className="focus-visible:ring-0" type="text" placeholder="Select download directory" value={downloadDirPath ?? 'Unknown'} readOnly/>
|
<Input className="focus-visible:ring-0" type="text" placeholder="Select download directory" value={downloadDirPath ?? 'Unknown'} readOnly/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
disabled={isFlatpak}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const folder = await open({
|
const folder = await open({
|
||||||
@@ -340,7 +400,7 @@ function AppFolderSettings() {
|
|||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
disabled={ongoingDownloads.length > 0}
|
disabled={ongoingDownloads.length > 0 || isFlatpak}
|
||||||
>
|
>
|
||||||
<BrushCleaning className="size-4" /> Clean
|
<BrushCleaning className="size-4" /> Clean
|
||||||
</Button>
|
</Button>
|
||||||
@@ -390,6 +450,26 @@ function AppFolderSettings() {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sanitize-filenames">
|
||||||
|
<h3 className="font-semibold">Sanitize Filenames</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Make filenames windows-compatible, allow only ASCII characters and replace spaces with underscore (recommended, disabling it may cause issue with some downloads, also it may cause paused downloads to re-start from begining)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Switch
|
||||||
|
id="windows-filenames"
|
||||||
|
checked={windowsFilenames}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('windows_filenames', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="windows-filenames">Windows Compatibility</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="restrict-filenames"
|
||||||
|
checked={restrictFilenames}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('restrict_filenames', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="restrict-filenames">Force ASCII Only</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -668,9 +748,10 @@ function AppNetworkSettings() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput
|
||||||
className="focus-visible:ring-0"
|
className="w-full"
|
||||||
placeholder="Enter rate limit in bytes/s"
|
placeholder="Enter rate limit in bytes/s"
|
||||||
|
min={0}
|
||||||
readOnly={useCustomCommands}
|
readOnly={useCustomCommands}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -726,6 +807,8 @@ function AppNetworkSettings() {
|
|||||||
function AppCookiesSettings() {
|
function AppCookiesSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
const useCookies = useSettingsPageStatesStore(state => state.settings.use_cookies);
|
const useCookies = useSettingsPageStatesStore(state => state.settings.use_cookies);
|
||||||
const importCookiesFrom = useSettingsPageStatesStore(state => state.settings.import_cookies_from);
|
const importCookiesFrom = useSettingsPageStatesStore(state => state.settings.import_cookies_from);
|
||||||
const cookiesBrowser = useSettingsPageStatesStore(state => state.settings.cookies_browser);
|
const cookiesBrowser = useSettingsPageStatesStore(state => state.settings.cookies_browser);
|
||||||
@@ -742,7 +825,7 @@ function AppCookiesSettings() {
|
|||||||
id="use-cookies"
|
id="use-cookies"
|
||||||
checked={useCookies}
|
checked={useCookies}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_cookies', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_cookies', checked)}
|
||||||
disabled={useCustomCommands}
|
disabled={useCustomCommands || isFlatpak}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-cookies">Use Cookies</Label>
|
<Label htmlFor="use-cookies">Use Cookies</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -751,7 +834,7 @@ function AppCookiesSettings() {
|
|||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
value={importCookiesFrom}
|
value={importCookiesFrom}
|
||||||
onValueChange={(value) => saveSettingsKey('import_cookies_from', value)}
|
onValueChange={(value) => saveSettingsKey('import_cookies_from', value)}
|
||||||
disabled={!useCookies || useCustomCommands}
|
disabled={!useCookies || useCustomCommands || isFlatpak}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="browser" id="cookies-browser" />
|
<RadioGroupItem value="browser" id="cookies-browser" />
|
||||||
@@ -767,7 +850,7 @@ function AppCookiesSettings() {
|
|||||||
<Select
|
<Select
|
||||||
value={cookiesBrowser}
|
value={cookiesBrowser}
|
||||||
onValueChange={(value) => saveSettingsKey('cookies_browser', value)}
|
onValueChange={(value) => saveSettingsKey('cookies_browser', value)}
|
||||||
disabled={importCookiesFrom !== "browser" || !useCookies || useCustomCommands}
|
disabled={importCookiesFrom !== "browser" || !useCookies || useCustomCommands || isFlatpak}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-57.5 ring-0 focus:ring-0">
|
<SelectTrigger className="w-57.5 ring-0 focus:ring-0">
|
||||||
<SelectValue placeholder="Select browser to import cookies" />
|
<SelectValue placeholder="Select browser to import cookies" />
|
||||||
@@ -794,7 +877,7 @@ function AppCookiesSettings() {
|
|||||||
<Input className="focus-visible:ring-0" type="text" placeholder="Select cookies text file" value={cookiesFile ?? ''} disabled={importCookiesFrom !== "file" || !useCookies} readOnly/>
|
<Input className="focus-visible:ring-0" type="text" placeholder="Select cookies text file" value={cookiesFile ?? ''} disabled={importCookiesFrom !== "file" || !useCookies} readOnly/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={importCookiesFrom !== "file" || !useCookies || useCustomCommands}
|
disabled={importCookiesFrom !== "file" || !useCookies || useCustomCommands || isFlatpak}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const file = await open({
|
const file = await open({
|
||||||
@@ -853,7 +936,7 @@ function AppSponsorblockSettings() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="sponsorblock">
|
<div className="sponsorblock">
|
||||||
<h3 className="font-semibold">Sponsor Block</h3>
|
<h3 className="font-semibold">Sponsorblock</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<Switch
|
<Switch
|
||||||
@@ -978,9 +1061,376 @@ function AppSponsorblockSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppDelaySettings() {
|
||||||
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
|
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||||
|
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||||
|
|
||||||
|
const useDelay = useSettingsPageStatesStore(state => state.settings.use_delay);
|
||||||
|
const useSearchDelay = useSettingsPageStatesStore(state => state.settings.use_search_delay);
|
||||||
|
const delayMode = useSettingsPageStatesStore(state => state.settings.delay_mode);
|
||||||
|
const minSleepInterval = useSettingsPageStatesStore(state => state.settings.min_sleep_interval);
|
||||||
|
const maxSleepInterval = useSettingsPageStatesStore(state => state.settings.max_sleep_interval);
|
||||||
|
const requestSleepInterval = useSettingsPageStatesStore(state => state.settings.request_sleep_interval);
|
||||||
|
const delayPlaylistOnly = useSettingsPageStatesStore(state => state.settings.delay_playlist_only);
|
||||||
|
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||||
|
|
||||||
|
const minMaxSleepIntervalForm = useForm<z.infer<typeof minMaxSleepIntervalSchema>>({
|
||||||
|
resolver: zodResolver(minMaxSleepIntervalSchema),
|
||||||
|
defaultValues: {
|
||||||
|
min_sleep_interval: minSleepInterval,
|
||||||
|
max_sleep_interval: maxSleepInterval,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedMinSleepInterval = minMaxSleepIntervalForm.watch("min_sleep_interval");
|
||||||
|
const watchedMaxSleepInterval = minMaxSleepIntervalForm.watch("max_sleep_interval");
|
||||||
|
const { errors: minMaxSleepIntervalFormErrors } = minMaxSleepIntervalForm.formState;
|
||||||
|
|
||||||
|
function handleMinMaxSleepIntervalSubmit(values: z.infer<typeof minMaxSleepIntervalSchema>) {
|
||||||
|
try {
|
||||||
|
saveSettingsKey('min_sleep_interval', values.min_sleep_interval);
|
||||||
|
saveSettingsKey('max_sleep_interval', values.max_sleep_interval);
|
||||||
|
toast.success("Sleep Intervals updated", {
|
||||||
|
description: `Minimum Sleep Interval changed to ${values.min_sleep_interval} seconds, Maximum Sleep Interval changed to ${values.max_sleep_interval} seconds`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing sleep intervals:", error);
|
||||||
|
toast.error("Failed to change sleep intervals", {
|
||||||
|
description: "An error occurred while trying to change the sleep intervals. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestSleepIntervalForm = useForm<z.infer<typeof requestSleepIntervalSchema>>({
|
||||||
|
resolver: zodResolver(requestSleepIntervalSchema),
|
||||||
|
defaultValues: {
|
||||||
|
request_sleep_interval: requestSleepInterval,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedRequestSleepInterval = requestSleepIntervalForm.watch("request_sleep_interval");
|
||||||
|
const { errors: requestSleepIntervalFormErrors } = requestSleepIntervalForm.formState;
|
||||||
|
|
||||||
|
function handleRequestSleepIntervalSubmit(values: z.infer<typeof requestSleepIntervalSchema>) {
|
||||||
|
try {
|
||||||
|
saveSettingsKey('request_sleep_interval', values.request_sleep_interval);
|
||||||
|
toast.success("Request Sleep Interval updated", {
|
||||||
|
description: `Request Sleep Interval changed to ${values.request_sleep_interval} seconds`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing request sleep interval:", error);
|
||||||
|
toast.error("Failed to change request sleep interval", {
|
||||||
|
description: "An error occurred while trying to change the request sleep interval. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formResetTrigger > 0) {
|
||||||
|
minMaxSleepIntervalForm.reset();
|
||||||
|
requestSleepIntervalForm.reset();
|
||||||
|
acknowledgeFormReset();
|
||||||
|
}
|
||||||
|
}, [formResetTrigger]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="delay">
|
||||||
|
<h3 className="font-semibold">Delay</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Use delay to prevent potential issues with some sites (bypass rate-limit, temporary ban, etc.)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Switch
|
||||||
|
id="use-delay"
|
||||||
|
checked={useDelay}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_delay', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-delay">Use Delay in Downloads</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="use-search-delay"
|
||||||
|
checked={useSearchDelay}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_search_delay', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-search-delay">Use Delay in Search</Label>
|
||||||
|
</div>
|
||||||
|
<RadioGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
className="flex items-center gap-4"
|
||||||
|
value={delayMode}
|
||||||
|
onValueChange={(value) => saveSettingsKey('delay_mode', value)}
|
||||||
|
disabled={(!useDelay && !useSearchDelay) || useCustomCommands}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="auto" id="delay-auto" />
|
||||||
|
<Label htmlFor="delay-auto">Auto (Default)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="custom" id="delay-custom" />
|
||||||
|
<Label htmlFor="delay-custom">Custom</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<div className="flex flex-col gap-2 mt-5">
|
||||||
|
<Label className="text-xs mb-1">Minimum, Maximum Sleep Interval (in Seconds)</Label>
|
||||||
|
<Form {...minMaxSleepIntervalForm}>
|
||||||
|
<form onSubmit={minMaxSleepIntervalForm.handleSubmit(handleMinMaxSleepIntervalSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={minMaxSleepIntervalForm.control}
|
||||||
|
name="min_sleep_interval"
|
||||||
|
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Min sleep"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={minMaxSleepIntervalForm.control}
|
||||||
|
name="max_sleep_interval"
|
||||||
|
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Max sleep"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={(!watchedMinSleepInterval || Number(watchedMinSleepInterval) === minSleepInterval) && (!watchedMaxSleepInterval || Number(watchedMaxSleepInterval) === maxSleepInterval) || Object.keys(minMaxSleepIntervalFormErrors).length > 0 || delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 mt-4 mb-2">
|
||||||
|
<Label className="text-xs mb-1">Request Sleep Interval (in Seconds)</Label>
|
||||||
|
<Form {...requestSleepIntervalForm}>
|
||||||
|
<form onSubmit={requestSleepIntervalForm.handleSubmit(handleRequestSleepIntervalSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={requestSleepIntervalForm.control}
|
||||||
|
name="request_sleep_interval"
|
||||||
|
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Request sleep"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!watchedRequestSleepInterval || Number(watchedRequestSleepInterval) === requestSleepInterval || Object.keys(requestSleepIntervalFormErrors).length > 0 || delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<Label className="text-xs text-muted-foreground">(Configured: {minSleepInterval}s - {maxSleepInterval}s & {requestSleepInterval}s, Mode: {delayMode === 'auto' ? 'Auto' : 'Custom'}, Status: {useDelay && delayPlaylistOnly ? 'Playlist Only' : useDelay ? 'Downloads' : ''}{useDelay && useSearchDelay ? ', Search' : useSearchDelay ? 'Search' : !useDelay && !useSearchDelay ? 'Disabled' : ''}) (Default: 10s - 20s & 1s, Range: 1s - 3600s)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="delay-playlist-only">
|
||||||
|
<h3 className="font-semibold">Delay Playlist Only</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Only apply delay for playlist/batch downloads, single video downloads will not be affected (recommended)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="delay-playlist-only"
|
||||||
|
checked={delayPlaylistOnly}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('delay_playlist_only', checked)}
|
||||||
|
disabled={!useDelay || useCustomCommands}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppPoTokenSettings() {
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
|
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||||
|
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||||
|
|
||||||
|
const usePotoken = useSettingsPageStatesStore(state => state.settings.use_potoken);
|
||||||
|
const disableInnertube = useSettingsPageStatesStore(state => state.settings.disable_innertube);
|
||||||
|
const potServerPort = useSettingsPageStatesStore(state => state.settings.pot_server_port);
|
||||||
|
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||||
|
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||||
|
const isStartingPotServer = useSettingsPageStatesStore(state => state.isStartingPotServer);
|
||||||
|
const isChangingPotServerPort = useSettingsPageStatesStore(state => state.isChangingPotServerPort);
|
||||||
|
const setIsChangingPotServerPort = useSettingsPageStatesStore(state => state.setIsChangingPotServerPort);
|
||||||
|
|
||||||
|
const { saveSettingsKey } = useSettings();
|
||||||
|
const { startPotServer, stopPotServer } = usePotServer();
|
||||||
|
|
||||||
|
const potServerPortForm = useForm<z.infer<typeof potServerPortSchema>>({
|
||||||
|
resolver: zodResolver(potServerPortSchema),
|
||||||
|
defaultValues: {
|
||||||
|
port: potServerPort,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedPotServerPort = potServerPortForm.watch("port");
|
||||||
|
const { errors: potServerPortFormErrors } = potServerPortForm.formState;
|
||||||
|
|
||||||
|
async function handlePotServerPortSubmit(values: z.infer<typeof potServerPortSchema>) {
|
||||||
|
setIsChangingPotServerPort(true);
|
||||||
|
try {
|
||||||
|
saveSettingsKey('pot_server_port', values.port);
|
||||||
|
if (isRunningPotServer) {
|
||||||
|
await stopPotServer();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
await startPotServer(values.port);
|
||||||
|
}
|
||||||
|
toast.success("POT Server Port updated", {
|
||||||
|
description: `PO Token Server Port changed to ${values.port}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing PO Token Server Port:", error);
|
||||||
|
toast.error("Failed to change POT Server Port", {
|
||||||
|
description: "An error occurred while trying to change the PO Token Server Port. Please try again.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsChangingPotServerPort(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formResetTrigger > 0) {
|
||||||
|
potServerPortForm.reset();
|
||||||
|
acknowledgeFormReset();
|
||||||
|
}
|
||||||
|
}, [formResetTrigger]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="potoken">
|
||||||
|
<h3 className="font-semibold">PO Token</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Generate proof-of-origin token for youtube to make seem your traffic more legitimate (bypasses some bot-protection checks, sometimes requires cookies)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<Switch
|
||||||
|
id="use-potoken"
|
||||||
|
checked={usePotoken}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
saveSettingsKey('use_potoken', checked);
|
||||||
|
if (checked) {
|
||||||
|
await startPotServer();
|
||||||
|
} else {
|
||||||
|
await stopPotServer();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={useCustomCommands || isStartingPotServer || isChangingPotServerPort || isFlatpak}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-potoken">Use PO Token</Label>
|
||||||
|
</div>
|
||||||
|
<Label className="text-xs text-muted-foreground flex items-center">
|
||||||
|
<span className="mr-1">NeoDLP POT Server is</span>
|
||||||
|
{isStartingPotServer ? (
|
||||||
|
<span className="text-amber-600 dark:text-amber-500 underline">Starting</span>
|
||||||
|
) : isRunningPotServer ? (
|
||||||
|
<span className="text-emerald-600 dark:text-emerald-500 underline">Running</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-red-600 dark:text-red-500 underline">Not Running</span>
|
||||||
|
)}
|
||||||
|
{isRunningPotServer && potServerPort ? (
|
||||||
|
<span className="ml-1">on Port {potServerPort}</span>
|
||||||
|
) : null}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="disable-innertube">
|
||||||
|
<h3 className="font-semibold">Disable Innertube</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Disable the usage of innertube api for potoken generation (falls back to legacy mode, use only if normal potoken is not working)</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="disable-innertube"
|
||||||
|
checked={disableInnertube}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('disable_innertube', checked)}
|
||||||
|
disabled={useCustomCommands || !usePotoken || isFlatpak}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pot-server-port">
|
||||||
|
<h3 className="font-semibold">POT Server Port</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Change neodlp proof-of-origin token server port</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Form {...potServerPortForm}>
|
||||||
|
<form onSubmit={potServerPortForm.handleSubmit(handlePotServerPortSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={potServerPortForm.control}
|
||||||
|
name="port"
|
||||||
|
disabled={!usePotoken || isChangingPotServerPort || isStartingPotServer || isFlatpak}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Enter port number"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Label htmlFor="port" className="text-xs text-muted-foreground">(Current: {potServerPort}) (Default: 4416, Range: 4000-5000)</Label>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!watchedPotServerPort || Number(watchedPotServerPort) === potServerPort || Object.keys(potServerPortFormErrors).length > 0 || !usePotoken || useCustomCommands || isChangingPotServerPort || isStartingPotServer || isFlatpak}
|
||||||
|
>
|
||||||
|
{isChangingPotServerPort ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Changing
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Change'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AppNotificationSettings() {
|
function AppNotificationSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||||
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
||||||
const downloadCompletionNotification = useSettingsPageStatesStore(state => state.settings.download_completion_notification);
|
const downloadCompletionNotification = useSettingsPageStatesStore(state => state.settings.download_completion_notification);
|
||||||
@@ -994,6 +1444,7 @@ function AppNotificationSettings() {
|
|||||||
<Switch
|
<Switch
|
||||||
id="enable-notifications"
|
id="enable-notifications"
|
||||||
checked={enableNotifications}
|
checked={enableNotifications}
|
||||||
|
disabled={isFlatpak}
|
||||||
onCheckedChange={async (checked) => {
|
onCheckedChange={async (checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
const granted = await isPermissionGranted();
|
const granted = await isPermissionGranted();
|
||||||
@@ -1019,7 +1470,7 @@ function AppNotificationSettings() {
|
|||||||
id="update-notification"
|
id="update-notification"
|
||||||
checked={updateNotification}
|
checked={updateNotification}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('update_notification', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('update_notification', checked)}
|
||||||
disabled={!enableNotifications}
|
disabled={!enableNotifications || isFlatpak}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="update-notification">App Updates</Label>
|
<Label htmlFor="update-notification">App Updates</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -1028,7 +1479,7 @@ function AppNotificationSettings() {
|
|||||||
id="download-completion-notification"
|
id="download-completion-notification"
|
||||||
checked={downloadCompletionNotification}
|
checked={downloadCompletionNotification}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('download_completion_notification', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('download_completion_notification', checked)}
|
||||||
disabled={!enableNotifications}
|
disabled={!enableNotifications || isFlatpak}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="download-completion-notification">Download Completion</Label>
|
<Label htmlFor="download-completion-notification">Download Completion</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -1040,12 +1491,14 @@ function AppNotificationSettings() {
|
|||||||
|
|
||||||
function AppCommandSettings() {
|
function AppCommandSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
const { startPotServer, stopPotServer } = usePotServer();
|
||||||
|
|
||||||
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||||
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||||
|
|
||||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||||
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||||
|
const usePotoken = useSettingsPageStatesStore(state => state.settings.use_potoken);
|
||||||
|
|
||||||
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
||||||
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
||||||
@@ -1123,9 +1576,14 @@ function AppCommandSettings() {
|
|||||||
<Switch
|
<Switch
|
||||||
id="use-custom-commands"
|
id="use-custom-commands"
|
||||||
checked={useCustomCommands}
|
checked={useCustomCommands}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={async(checked) => {
|
||||||
saveSettingsKey('use_custom_commands', checked)
|
saveSettingsKey('use_custom_commands', checked)
|
||||||
resetDownloadConfiguration();
|
resetDownloadConfiguration();
|
||||||
|
if (checked && usePotoken) {
|
||||||
|
await stopPotServer();
|
||||||
|
} else if (!checked && usePotoken) {
|
||||||
|
await startPotServer();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-custom-commands">Use Custom Commands</Label>
|
<Label htmlFor="use-custom-commands">Use Custom Commands</Label>
|
||||||
@@ -1260,6 +1718,9 @@ function AppDebugSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AppInfoSettings() {
|
function AppInfoSettings() {
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
const isAppimage = useEnvironmentStore(state => state.isAppimage);
|
||||||
|
|
||||||
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
const appVersion = useSettingsPageStatesStore(state => state.appVersion);
|
||||||
|
|
||||||
const binDepsList = [
|
const binDepsList = [
|
||||||
@@ -1268,6 +1729,7 @@ function AppInfoSettings() {
|
|||||||
{ key: 'ffprobe', name: 'FFprobe', desc: 'Multimedia stream analyzer for retrieving media information', url: 'https://ffmpeg.org/ffprobe.html', license: 'LGPLv2.1+', licenseUrl: 'https://ffmpeg.org/legal.html' },
|
{ key: 'ffprobe', name: 'FFprobe', desc: 'Multimedia stream analyzer for retrieving media information', url: 'https://ffmpeg.org/ffprobe.html', license: 'LGPLv2.1+', licenseUrl: 'https://ffmpeg.org/legal.html' },
|
||||||
{ key: 'deno', name: 'Deno', desc: 'The modern JavaScript/TypeScript runtime', url: 'https://deno.land/', license: 'MIT', licenseUrl: 'https://github.com/denoland/deno/blob/main/LICENSE.md' },
|
{ key: 'deno', name: 'Deno', desc: 'The modern JavaScript/TypeScript runtime', url: 'https://deno.land/', license: 'MIT', licenseUrl: 'https://github.com/denoland/deno/blob/main/LICENSE.md' },
|
||||||
{ key: 'aria2', name: 'Aria2', desc: 'Lightweight multi-protocol & multi-source download utility', url: 'https://aria2.github.io/', license: 'GPLv2+', licenseUrl: 'https://github.com/aria2/aria2/blob/master/COPYING' },
|
{ key: 'aria2', name: 'Aria2', desc: 'Lightweight multi-protocol & multi-source download utility', url: 'https://aria2.github.io/', license: 'GPLv2+', licenseUrl: 'https://github.com/aria2/aria2/blob/master/COPYING' },
|
||||||
|
{ Key: 'bgutil-pot-rs', name: 'BgUtils POT Provider (Rust)', desc: 'A high-performance YouTube POT (Proof-of-Origin Token) provider implemented in Rust', url: 'https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs', license: 'GPLv3+', licenseUrl: 'https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/blob/master/LICENSE' },
|
||||||
];
|
];
|
||||||
const langDepsList = [
|
const langDepsList = [
|
||||||
{ key: 'tauri', name: 'Tauri', desc: 'Framework for building cross-platform, tiny and blazing fast binaries', url: 'https://tauri.app/', license: 'MIT, Apache-2.0', licenseUrl: 'https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT' },
|
{ key: 'tauri', name: 'Tauri', desc: 'Framework for building cross-platform, tiny and blazing fast binaries', url: 'https://tauri.app/', license: 'MIT, Apache-2.0', licenseUrl: 'https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT' },
|
||||||
@@ -1372,6 +1834,35 @@ function AppInfoSettings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="healthcheck">
|
||||||
|
<h3 className="font-semibold">Health Check</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Ensure everything is working fine</p>
|
||||||
|
{isFlatpak ? (
|
||||||
|
<Alert className="">
|
||||||
|
<TriangleAlert className="size-4 stroke-primary" />
|
||||||
|
<AlertTitle className="text-sm">Flatpak Sandbox Detected!</AlertTitle>
|
||||||
|
<AlertDescription className="text-xs">
|
||||||
|
It looks like you are running NeoDLP in a Flatpak sandbox. Some features like browser integration, desktop notifications, cookies, po tokens, changing download folder, revealing completed downloads in explorer, automatic yt-dlp updates and auto-launch on startup are not available in Flatpak due to sandbox restrictions. To use these features, please install the native linux build (DEB, RPM or AUR) of NeoDLP.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : isAppimage ? (
|
||||||
|
<Alert className="">
|
||||||
|
<TriangleAlert className="size-4 stroke-primary" />
|
||||||
|
<AlertTitle className="text-sm">Appimage Environment Detected!</AlertTitle>
|
||||||
|
<AlertDescription className="text-xs">
|
||||||
|
Looks like you are using NeoDLP Appimage. NeoDLP's browser integration features are not available on Appimage environment due to it's limitations. To use NeoDLP's browser integration features please install the native linux build (DEB, RPM or AUR) of NeoDLP.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert className="">
|
||||||
|
<CircleCheck className="size-4 stroke-primary" />
|
||||||
|
<AlertTitle className="text-sm">All Set! Cheers :)</AlertTitle>
|
||||||
|
<AlertDescription className="text-xs">
|
||||||
|
NeoDLP is running as normal without any limitations! You should be able to use all the features of NeoDLP without any issues. If you face any problem, feel free to report it to us.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="bug-report">
|
<div className="bug-report">
|
||||||
<h3 className="font-semibold">Bug Report</h3>
|
<h3 className="font-semibold">Bug Report</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-3">Noticed any bug or inconsistencies? Report it to help us improve</p>
|
<p className="text-xs text-muted-foreground mb-3">Noticed any bug or inconsistencies? Report it to help us improve</p>
|
||||||
@@ -1442,6 +1933,8 @@ function AppInfoSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ApplicationSettings() {
|
export function ApplicationSettings() {
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
const activeSubAppTab = useSettingsPageStatesStore(state => state.activeSubAppTab);
|
const activeSubAppTab = useSettingsPageStatesStore(state => state.activeSubAppTab);
|
||||||
const setActiveSubAppTab = useSettingsPageStatesStore(state => state.setActiveSubAppTab);
|
const setActiveSubAppTab = useSettingsPageStatesStore(state => state.setActiveSubAppTab);
|
||||||
|
|
||||||
@@ -1462,12 +1955,14 @@ export function ApplicationSettings() {
|
|||||||
const tabsList = [
|
const tabsList = [
|
||||||
{ key: 'general', label: 'General', icon: Wrench, component: <AppGeneralSettings /> },
|
{ key: 'general', label: 'General', icon: Wrench, component: <AppGeneralSettings /> },
|
||||||
{ key: 'appearance', label: 'Appearance', icon: WandSparkles, component: <AppAppearanceSettings /> },
|
{ key: 'appearance', label: 'Appearance', icon: WandSparkles, component: <AppAppearanceSettings /> },
|
||||||
{ key: 'folders', label: 'Folders', icon: Folder, component: <AppFolderSettings /> },
|
{ key: 'filesystem', label: 'Filesystem', icon: Folder, component: <AppFilesystemSettings /> },
|
||||||
{ key: 'formats', label: 'Formats', icon: FileVideo, component: <AppFormatSettings /> },
|
{ key: 'formats', label: 'Formats', icon: FileVideo, component: <AppFormatSettings /> },
|
||||||
{ key: 'embedding', label: 'Embedding', icon: FilePen, component: <AppEmbeddingSettings /> },
|
{ key: 'embedding', label: 'Embedding', icon: FilePen, component: <AppEmbeddingSettings /> },
|
||||||
{ key: 'network', label: 'Network', icon: Wifi, component: <AppNetworkSettings /> },
|
{ key: 'network', label: 'Network', icon: Wifi, component: <AppNetworkSettings /> },
|
||||||
{ key: 'cookies', label: 'Cookies', icon: Cookie, component: <AppCookiesSettings /> },
|
{ key: 'cookies', label: 'Cookies', icon: Cookie, component: <AppCookiesSettings /> },
|
||||||
{ key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> },
|
{ key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> },
|
||||||
|
{ key: 'delay', label: 'Delay', icon: Timer, component: <AppDelaySettings /> },
|
||||||
|
{ key: 'potoken', label: 'Potoken', icon: KeyRound, component: <AppPoTokenSettings /> },
|
||||||
{ key: 'notifications', label: 'Notifications', icon: BellRing, component: <AppNotificationSettings /> },
|
{ key: 'notifications', label: 'Notifications', icon: BellRing, component: <AppNotificationSettings /> },
|
||||||
{ key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> },
|
{ key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> },
|
||||||
{ key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> },
|
{ key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> },
|
||||||
@@ -1497,12 +1992,14 @@ export function ApplicationSettings() {
|
|||||||
<Switch
|
<Switch
|
||||||
id="ytdlp-auto-update"
|
id="ytdlp-auto-update"
|
||||||
checked={ytDlpAutoUpdate}
|
checked={ytDlpAutoUpdate}
|
||||||
|
disabled={isFlatpak}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('ytdlp_auto_update', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('ytdlp_auto_update', checked)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="ytdlp-auto-update">Auto Update</Label>
|
<Label htmlFor="ytdlp-auto-update">Auto Update</Label>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={ytDlpUpdateChannel}
|
value={ytDlpUpdateChannel}
|
||||||
|
disabled={isFlatpak}
|
||||||
onValueChange={(value) => saveSettingsKey('ytdlp_update_channel', value)}
|
onValueChange={(value) => saveSettingsKey('ytdlp_update_channel', value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-37.5 ring-0 focus:ring-0">
|
<SelectTrigger className="w-37.5 ring-0 focus:ring-0">
|
||||||
@@ -1517,7 +2014,7 @@ export function ApplicationSettings() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
disabled={ytDlpAutoUpdate || isUpdatingYtDlp || ongoingDownloads.length > 0}
|
disabled={ytDlpAutoUpdate || isUpdatingYtDlp || ongoingDownloads.length > 0 || isFlatpak}
|
||||||
onClick={async () => await updateYtDlp()}
|
onClick={async () => await updateYtDlp()}
|
||||||
>
|
>
|
||||||
{isUpdatingYtDlp ? (
|
{isUpdatingYtDlp ? (
|
||||||
@@ -1553,7 +2050,7 @@ export function ApplicationSettings() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
<div className="min-h-full flex flex-col w-full border-l border-border pl-4">
|
<div className="min-h-full flex flex-col w-full border-l border-border pl-4">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-108.75", tab.key === "info" ? "max-w-[80%]" : "max-w-[70%]")}>
|
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-130", tab.key === "info" ? "max-w-[80%]" : "max-w-[70%]")}>
|
||||||
{tab.component}
|
{tab.component}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { useSettingsPageStatesStore } from "@/services/store";
|
import { useEnvironmentStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ArrowDownToLine, ArrowRight, EthernetPort, Loader2, Radio, RotateCw } from "lucide-react";
|
import { ArrowDownToLine, ArrowRight, EthernetPort, Loader2, Radio, RotateCw } from "lucide-react";
|
||||||
import { useSettings } from "@/helpers/use-settings";
|
import { useSettings } from "@/helpers/use-settings";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
@@ -15,6 +14,8 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from "@/component
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { SlidingButton } from "@/components/custom/slidingButton";
|
import { SlidingButton } from "@/components/custom/slidingButton";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { NumberInput } from "@/components/custom/numberInput";
|
||||||
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
|
||||||
const websocketPortSchema = z.object({
|
const websocketPortSchema = z.object({
|
||||||
port: z.coerce.number<number>({
|
port: z.coerce.number<number>({
|
||||||
@@ -31,9 +32,12 @@ const websocketPortSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function ExtInstallSettings() {
|
function ExtInstallSettings() {
|
||||||
|
const currentPlatform = platform();
|
||||||
|
const isFlatpak = useEnvironmentStore(state => state.isFlatpak);
|
||||||
|
|
||||||
const openLink = async (url: string, app: string | null) => {
|
const openLink = async (url: string, app: string | null) => {
|
||||||
try {
|
try {
|
||||||
await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => {
|
await invoke('open_link_with_app', { url: url, appName: app }).then(() => {
|
||||||
toast.info("Opening link", {
|
toast.info("Opening link", {
|
||||||
description: `Opening link with ${app ? app : 'default app'}.`,
|
description: `Opening link with ${app ? app : 'default app'}.`,
|
||||||
})
|
})
|
||||||
@@ -58,7 +62,7 @@ function ExtInstallSettings() {
|
|||||||
<span>Get Now</span>
|
<span>Get Now</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'chrome')}
|
onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', isFlatpak ? null : currentPlatform === "linux" ? 'google-chrome' : 'chrome')}
|
||||||
>
|
>
|
||||||
<span className="font-semibold flex items-center gap-2">
|
<span className="font-semibold flex items-center gap-2">
|
||||||
<svg className="size-4 fill-primary-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
<svg className="size-4 fill-primary-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
@@ -75,7 +79,7 @@ function ExtInstallSettings() {
|
|||||||
<span>Get Now</span>
|
<span>Get Now</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'firefox')}
|
onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', isFlatpak ? null : 'firefox')}
|
||||||
>
|
>
|
||||||
<span className="font-semibold flex items-center gap-2">
|
<span className="font-semibold flex items-center gap-2">
|
||||||
<svg className="size-4 fill-primary-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
<svg className="size-4 fill-primary-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
@@ -87,11 +91,11 @@ function ExtInstallSettings() {
|
|||||||
</SlidingButton>
|
</SlidingButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'msedge')}>Edge</Button>
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', isFlatpak ? null : 'msedge')}>Edge</Button>
|
||||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'opera')}>Opera</Button>
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', isFlatpak ? null : 'opera')}>Opera</Button>
|
||||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'brave')}>Brave</Button>
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', isFlatpak ? null : 'brave')}>Brave</Button>
|
||||||
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'vivaldi')}>Vivaldi</Button>
|
<Button variant="outline" onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', isFlatpak ? null : 'vivaldi')}>Vivaldi</Button>
|
||||||
<Button variant="outline" onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'zen')}>Zen</Button>
|
<Button variant="outline" onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', isFlatpak ? null : 'zen')}>Zen</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mb-2">* These links opens with coresponding browsers only. Make sure the browser is installed before clicking the link</p>
|
<p className="text-xs text-muted-foreground mb-2">* These links opens with coresponding browsers only. Make sure the browser is installed before clicking the link</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,9 +171,10 @@ function ExtPortSettings() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput
|
||||||
className="focus-visible:ring-0"
|
className="w-full"
|
||||||
placeholder="Enter port number"
|
placeholder="Enter port number"
|
||||||
|
min={0}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function AppSidebar() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeout: NodeJS.Timeout;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
if (open) {
|
if (open) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
setShowBadge(true);
|
setShowBadge(true);
|
||||||
@@ -147,7 +147,7 @@ export function AppSidebar() {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<CircleArrowUp className="size-4" />
|
<CircleArrowUp className="size-4 stroke-primary" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
|
|||||||
58
src/components/titlebar.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
|
import { MaximizeIcon } from "@/components/icons/maximize";
|
||||||
|
import { MinimizeIcon } from "@/components/icons/minimize";
|
||||||
|
import { CloseIcon } from "@/components/icons/close";
|
||||||
|
import { UnmaximizeIcon } from "@/components/icons/unmaximize";
|
||||||
|
|
||||||
|
export default function TitleBar() {
|
||||||
|
const [maximized, setMaximized] = useState<boolean>(false);
|
||||||
|
const appWindow = getCurrentWebviewWindow();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="titlebar flex items-center justify-between border-b bg-background">
|
||||||
|
<div className="flex items-center justify-center grow px-4 py-2.5" data-tauri-drag-region>
|
||||||
|
<h1 className="text-sm text-primary font-semibold">NeoDLP</h1>
|
||||||
|
</div>
|
||||||
|
<div className="controls flex items-center justify-center">
|
||||||
|
<button
|
||||||
|
className="px-4 py-3 hover:bg-muted"
|
||||||
|
id="titlebar-minimize"
|
||||||
|
title="Minimize"
|
||||||
|
onClick={() => appWindow.minimize()}
|
||||||
|
>
|
||||||
|
<MinimizeIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-4 py-3 hover:bg-muted"
|
||||||
|
id="titlebar-maximize"
|
||||||
|
title={maximized ? "Unmaximize" : "Maximize"}
|
||||||
|
onClick={async () => {
|
||||||
|
const isMaximized = await appWindow.isMaximized();
|
||||||
|
if (isMaximized) {
|
||||||
|
await appWindow.unmaximize();
|
||||||
|
setMaximized(false);
|
||||||
|
} else {
|
||||||
|
await appWindow.maximize();
|
||||||
|
setMaximized(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{maximized ? (
|
||||||
|
<UnmaximizeIcon />
|
||||||
|
) : (
|
||||||
|
<MaximizeIcon />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-4 py-3 hover:bg-destructive"
|
||||||
|
id="titlebar-close"
|
||||||
|
title="Close"
|
||||||
|
onClick={() => appWindow.hide()}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
||||||
import { ChevronDown } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-b", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AccordionItem.displayName = "AccordionItem"
|
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Header className="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
))
|
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
))
|
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
import { AspectRatio as AspectRatioPrimitive } from "radix-ui"
|
||||||
|
|
||||||
const AspectRatio = AspectRatioPrimitive.Root
|
const AspectRatio = AspectRatioPrimitive.Root
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import { Avatar as AvatarPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
|
||||||
HTMLElement,
|
|
||||||
React.ComponentPropsWithoutRef<"nav"> & {
|
|
||||||
separator?: React.ReactNode
|
|
||||||
}
|
|
||||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
|
||||||
Breadcrumb.displayName = "Breadcrumb"
|
|
||||||
|
|
||||||
const BreadcrumbList = React.forwardRef<
|
|
||||||
HTMLOListElement,
|
|
||||||
React.ComponentPropsWithoutRef<"ol">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ol
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
BreadcrumbList.displayName = "BreadcrumbList"
|
|
||||||
|
|
||||||
const BreadcrumbItem = React.forwardRef<
|
|
||||||
HTMLLIElement,
|
|
||||||
React.ComponentPropsWithoutRef<"li">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<li
|
|
||||||
ref={ref}
|
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
|
||||||
HTMLAnchorElement,
|
|
||||||
React.ComponentPropsWithoutRef<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}
|
|
||||||
>(({ asChild, className, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
ref={ref}
|
|
||||||
className={cn("transition-colors hover:text-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
|
||||||
|
|
||||||
const BreadcrumbPage = React.forwardRef<
|
|
||||||
HTMLSpanElement,
|
|
||||||
React.ComponentPropsWithoutRef<"span">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<span
|
|
||||||
ref={ref}
|
|
||||||
role="link"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-current="page"
|
|
||||||
className={cn("font-normal text-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) => (
|
|
||||||
<li
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <ChevronRight />}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) => (
|
|
||||||
<span
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
BreadcrumbEllipsis,
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
|
||||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
horizontal:
|
|
||||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
|
||||||
vertical:
|
|
||||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function ButtonGroup({
|
|
||||||
className,
|
|
||||||
orientation,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="button-group"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonGroupText({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "div"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
className={cn(
|
|
||||||
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonGroupSeparator({
|
|
||||||
className,
|
|
||||||
orientation = "vertical",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
data-slot="button-group-separator"
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ButtonGroup,
|
|
||||||
ButtonGroupSeparator,
|
|
||||||
ButtonGroupText,
|
|
||||||
buttonGroupVariants,
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -44,7 +44,7 @@ export interface ButtonProps
|
|||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button, buttonVariants } from "@/components/ui/button"
|
|
||||||
|
|
||||||
function Calendar({
|
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
captionLayout = "label",
|
|
||||||
buttonVariant = "ghost",
|
|
||||||
formatters,
|
|
||||||
components,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DayPicker> & {
|
|
||||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
|
||||||
}) {
|
|
||||||
const defaultClassNames = getDefaultClassNames()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn(
|
|
||||||
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
|
||||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
||||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
captionLayout={captionLayout}
|
|
||||||
formatters={{
|
|
||||||
formatMonthDropdown: (date) =>
|
|
||||||
date.toLocaleString("default", { month: "short" }),
|
|
||||||
...formatters,
|
|
||||||
}}
|
|
||||||
classNames={{
|
|
||||||
root: cn("w-fit", defaultClassNames.root),
|
|
||||||
months: cn(
|
|
||||||
"relative flex flex-col gap-4 md:flex-row",
|
|
||||||
defaultClassNames.months
|
|
||||||
),
|
|
||||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
||||||
nav: cn(
|
|
||||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
|
||||||
defaultClassNames.nav
|
|
||||||
),
|
|
||||||
button_previous: cn(
|
|
||||||
buttonVariants({ variant: buttonVariant }),
|
|
||||||
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
|
|
||||||
defaultClassNames.button_previous
|
|
||||||
),
|
|
||||||
button_next: cn(
|
|
||||||
buttonVariants({ variant: buttonVariant }),
|
|
||||||
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
|
|
||||||
defaultClassNames.button_next
|
|
||||||
),
|
|
||||||
month_caption: cn(
|
|
||||||
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
|
|
||||||
defaultClassNames.month_caption
|
|
||||||
),
|
|
||||||
dropdowns: cn(
|
|
||||||
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
|
|
||||||
defaultClassNames.dropdowns
|
|
||||||
),
|
|
||||||
dropdown_root: cn(
|
|
||||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
|
||||||
defaultClassNames.dropdown_root
|
|
||||||
),
|
|
||||||
dropdown: cn(
|
|
||||||
"bg-popover absolute inset-0 opacity-0",
|
|
||||||
defaultClassNames.dropdown
|
|
||||||
),
|
|
||||||
caption_label: cn(
|
|
||||||
"select-none font-medium",
|
|
||||||
captionLayout === "label"
|
|
||||||
? "text-sm"
|
|
||||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
|
||||||
defaultClassNames.caption_label
|
|
||||||
),
|
|
||||||
table: "w-full border-collapse",
|
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
|
||||||
weekday: cn(
|
|
||||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
|
||||||
defaultClassNames.weekday
|
|
||||||
),
|
|
||||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
|
||||||
week_number_header: cn(
|
|
||||||
"w-[--cell-size] select-none",
|
|
||||||
defaultClassNames.week_number_header
|
|
||||||
),
|
|
||||||
week_number: cn(
|
|
||||||
"text-muted-foreground select-none text-[0.8rem]",
|
|
||||||
defaultClassNames.week_number
|
|
||||||
),
|
|
||||||
day: cn(
|
|
||||||
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
|
|
||||||
defaultClassNames.day
|
|
||||||
),
|
|
||||||
range_start: cn(
|
|
||||||
"bg-accent rounded-l-md",
|
|
||||||
defaultClassNames.range_start
|
|
||||||
),
|
|
||||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
|
||||||
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
|
|
||||||
today: cn(
|
|
||||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
|
||||||
defaultClassNames.today
|
|
||||||
),
|
|
||||||
outside: cn(
|
|
||||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
|
||||||
defaultClassNames.outside
|
|
||||||
),
|
|
||||||
disabled: cn(
|
|
||||||
"text-muted-foreground opacity-50",
|
|
||||||
defaultClassNames.disabled
|
|
||||||
),
|
|
||||||
hidden: cn("invisible", defaultClassNames.hidden),
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
Root: ({ className, rootRef, ...props }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="calendar"
|
|
||||||
ref={rootRef}
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Chevron: ({ className, orientation, ...props }) => {
|
|
||||||
if (orientation === "left") {
|
|
||||||
return (
|
|
||||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orientation === "right") {
|
|
||||||
return (
|
|
||||||
<ChevronRightIcon
|
|
||||||
className={cn("size-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
DayButton: CalendarDayButton,
|
|
||||||
WeekNumber: ({ children, ...props }) => {
|
|
||||||
return (
|
|
||||||
<td {...props}>
|
|
||||||
<div className="flex size-[--cell-size] items-center justify-center text-center">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
...components,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CalendarDayButton({
|
|
||||||
className,
|
|
||||||
day,
|
|
||||||
modifiers,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DayButton>) {
|
|
||||||
const defaultClassNames = getDefaultClassNames()
|
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null)
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (modifiers.focused) ref.current?.focus()
|
|
||||||
}, [modifiers.focused])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
data-day={day.date.toLocaleDateString()}
|
|
||||||
data-selected-single={
|
|
||||||
modifiers.selected &&
|
|
||||||
!modifiers.range_start &&
|
|
||||||
!modifiers.range_end &&
|
|
||||||
!modifiers.range_middle
|
|
||||||
}
|
|
||||||
data-range-start={modifiers.range_start}
|
|
||||||
data-range-end={modifiers.range_end}
|
|
||||||
data-range-middle={modifiers.range_middle}
|
|
||||||
className={cn(
|
|
||||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
|
|
||||||
defaultClassNames.day,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Calendar, CalendarDayButton }
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import useEmblaCarousel, {
|
|
||||||
type UseEmblaCarouselType,
|
|
||||||
} from "embla-carousel-react"
|
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
||||||
type CarouselOptions = UseCarouselParameters[0]
|
|
||||||
type CarouselPlugin = UseCarouselParameters[1]
|
|
||||||
|
|
||||||
type CarouselProps = {
|
|
||||||
opts?: CarouselOptions
|
|
||||||
plugins?: CarouselPlugin
|
|
||||||
orientation?: "horizontal" | "vertical"
|
|
||||||
setApi?: (api: CarouselApi) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type CarouselContextProps = {
|
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
|
||||||
scrollPrev: () => void
|
|
||||||
scrollNext: () => void
|
|
||||||
canScrollPrev: boolean
|
|
||||||
canScrollNext: boolean
|
|
||||||
} & CarouselProps
|
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
|
||||||
|
|
||||||
function useCarousel() {
|
|
||||||
const context = React.useContext(CarouselContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useCarousel must be used within a <Carousel />")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
const Carousel = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
orientation = "horizontal",
|
|
||||||
opts,
|
|
||||||
setApi,
|
|
||||||
plugins,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
|
||||||
{
|
|
||||||
...opts,
|
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
|
||||||
},
|
|
||||||
plugins
|
|
||||||
)
|
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
||||||
if (!api) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanScrollPrev(api.canScrollPrev())
|
|
||||||
setCanScrollNext(api.canScrollNext())
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
|
||||||
api?.scrollPrev()
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
|
||||||
api?.scrollNext()
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (event.key === "ArrowLeft") {
|
|
||||||
event.preventDefault()
|
|
||||||
scrollPrev()
|
|
||||||
} else if (event.key === "ArrowRight") {
|
|
||||||
event.preventDefault()
|
|
||||||
scrollNext()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[scrollPrev, scrollNext]
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!api || !setApi) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setApi(api)
|
|
||||||
}, [api, setApi])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!api) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(api)
|
|
||||||
api.on("reInit", onSelect)
|
|
||||||
api.on("select", onSelect)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
api?.off("select", onSelect)
|
|
||||||
}
|
|
||||||
}, [api, onSelect])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CarouselContext.Provider
|
|
||||||
value={{
|
|
||||||
carouselRef,
|
|
||||||
api: api,
|
|
||||||
opts,
|
|
||||||
orientation:
|
|
||||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
||||||
scrollPrev,
|
|
||||||
scrollNext,
|
|
||||||
canScrollPrev,
|
|
||||||
canScrollNext,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
onKeyDownCapture={handleKeyDown}
|
|
||||||
className={cn("relative", className)}
|
|
||||||
role="region"
|
|
||||||
aria-roledescription="carousel"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CarouselContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Carousel.displayName = "Carousel"
|
|
||||||
|
|
||||||
const CarouselContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { carouselRef, orientation } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={carouselRef} className="overflow-hidden">
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex",
|
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselContent.displayName = "CarouselContent"
|
|
||||||
|
|
||||||
const CarouselItem = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { orientation } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="group"
|
|
||||||
aria-roledescription="slide"
|
|
||||||
className={cn(
|
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselItem.displayName = "CarouselItem"
|
|
||||||
|
|
||||||
const CarouselPrevious = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<typeof Button>
|
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
"absolute h-8 w-8 rounded-full",
|
|
||||||
orientation === "horizontal"
|
|
||||||
? "-left-12 top-1/2 -translate-y-1/2"
|
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
disabled={!canScrollPrev}
|
|
||||||
onClick={scrollPrev}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Previous slide</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselPrevious.displayName = "CarouselPrevious"
|
|
||||||
|
|
||||||
const CarouselNext = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<typeof Button>
|
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
"absolute h-8 w-8 rounded-full",
|
|
||||||
orientation === "horizontal"
|
|
||||||
? "-right-12 top-1/2 -translate-y-1/2"
|
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
disabled={!canScrollNext}
|
|
||||||
onClick={scrollNext}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Next slide</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselNext.displayName = "CarouselNext"
|
|
||||||
|
|
||||||
export {
|
|
||||||
type CarouselApi,
|
|
||||||
Carousel,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
CarouselPrevious,
|
|
||||||
CarouselNext,
|
|
||||||
}
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as RechartsPrimitive from "recharts"
|
|
||||||
import type { LegendPayload } from "recharts/types/component/DefaultLegendContent"
|
|
||||||
import {
|
|
||||||
NameType,
|
|
||||||
Payload,
|
|
||||||
ValueType,
|
|
||||||
} from "recharts/types/component/DefaultTooltipContent"
|
|
||||||
import type { Props as LegendProps } from "recharts/types/component/Legend"
|
|
||||||
import { TooltipContentProps } from "recharts/types/component/Tooltip"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
|
||||||
|
|
||||||
export type ChartConfig = {
|
|
||||||
[k in string]: {
|
|
||||||
label?: React.ReactNode
|
|
||||||
icon?: React.ComponentType
|
|
||||||
} & (
|
|
||||||
| { color?: string; theme?: never }
|
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartContextProps = {
|
|
||||||
config: ChartConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CustomTooltipProps = TooltipContentProps<ValueType, NameType> & {
|
|
||||||
className?: string
|
|
||||||
hideLabel?: boolean
|
|
||||||
hideIndicator?: boolean
|
|
||||||
indicator?: "line" | "dot" | "dashed"
|
|
||||||
nameKey?: string
|
|
||||||
labelKey?: string
|
|
||||||
labelFormatter?: (
|
|
||||||
label: TooltipContentProps<number, string>["label"],
|
|
||||||
payload: TooltipContentProps<number, string>["payload"]
|
|
||||||
) => React.ReactNode
|
|
||||||
formatter?: (
|
|
||||||
value: number | string,
|
|
||||||
name: string,
|
|
||||||
item: Payload<number | string, string>,
|
|
||||||
index: number,
|
|
||||||
payload: ReadonlyArray<Payload<number | string, string>>
|
|
||||||
) => React.ReactNode
|
|
||||||
labelClassName?: string
|
|
||||||
color?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChartLegendContentProps = {
|
|
||||||
className?: string
|
|
||||||
hideIcon?: boolean
|
|
||||||
verticalAlign?: LegendProps["verticalAlign"]
|
|
||||||
payload?: LegendPayload[]
|
|
||||||
nameKey?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
||||||
|
|
||||||
function useChart() {
|
|
||||||
const context = React.useContext(ChartContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChartContainer({
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
config,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
config: ChartConfig
|
|
||||||
children: React.ComponentProps<
|
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
|
||||||
>["children"]
|
|
||||||
}) {
|
|
||||||
const uniqueId = React.useId()
|
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChartContext.Provider value={{ config }}>
|
|
||||||
<div
|
|
||||||
data-slot="chart"
|
|
||||||
data-chart={chartId}
|
|
||||||
className={cn(
|
|
||||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChartStyle id={chartId} config={config} />
|
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</ChartContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
||||||
const colorConfig = Object.entries(config).filter(
|
|
||||||
([, config]) => config.theme || config.color
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: Object.entries(THEMES)
|
|
||||||
.map(
|
|
||||||
([theme, prefix]) => `
|
|
||||||
${prefix} [data-chart=${id}] {
|
|
||||||
${colorConfig
|
|
||||||
.map(([key, itemConfig]) => {
|
|
||||||
const color =
|
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
||||||
itemConfig.color
|
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
|
||||||
})
|
|
||||||
.join("\n")}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
||||||
|
|
||||||
function ChartTooltipContent({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
label,
|
|
||||||
className,
|
|
||||||
indicator = "dot",
|
|
||||||
hideLabel = false,
|
|
||||||
hideIndicator = false,
|
|
||||||
labelFormatter,
|
|
||||||
formatter,
|
|
||||||
labelClassName,
|
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
}: CustomTooltipProps) {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
|
||||||
if (hideLabel || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const [item] = payload
|
|
||||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const value = (() => {
|
|
||||||
const v =
|
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label as keyof typeof config]?.label ?? label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
return typeof v === "string" || typeof v === "number" ? v : undefined
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
|
||||||
return (
|
|
||||||
<div className={cn("font-medium", labelClassName)}>
|
|
||||||
{labelFormatter(value, payload)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
||||||
}, [
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
payload,
|
|
||||||
hideLabel,
|
|
||||||
labelClassName,
|
|
||||||
config,
|
|
||||||
labelKey,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.dataKey}
|
|
||||||
className={cn(
|
|
||||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
|
||||||
indicator === "dot" && "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
||||||
{
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.value && (
|
|
||||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
|
||||||
|
|
||||||
function ChartLegendContent({
|
|
||||||
className,
|
|
||||||
hideIcon = false,
|
|
||||||
payload,
|
|
||||||
verticalAlign = "bottom",
|
|
||||||
nameKey,
|
|
||||||
}: ChartLegendContentProps) {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
if (!payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{payload.map((item) => {
|
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
className={cn(
|
|
||||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
||||||
style={{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{itemConfig?.label}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
|
||||||
function getPayloadConfigFromPayload(
|
|
||||||
config: ChartConfig,
|
|
||||||
payload: unknown,
|
|
||||||
key: string
|
|
||||||
) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadPayload =
|
|
||||||
"payload" in payload &&
|
|
||||||
typeof payload.payload === "object" &&
|
|
||||||
payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let configLabelKey: string = key
|
|
||||||
|
|
||||||
if (
|
|
||||||
key in payload &&
|
|
||||||
typeof payload[key as keyof typeof payload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
|
||||||
} else if (
|
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[
|
|
||||||
key as keyof typeof payloadPayload
|
|
||||||
] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config
|
|
||||||
? config[configLabelKey]
|
|
||||||
: config[key as keyof typeof config]
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||||
import { CheckIcon } from "lucide-react"
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root
|
|
||||||
|
|
||||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
|
||||||
|
|
||||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
|
||||||
import { Search } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Command.displayName = CommandPrimitive.displayName
|
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog {...props}>
|
|
||||||
<DialogContent className="overflow-hidden p-0">
|
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
||||||
{children}
|
|
||||||
</Command>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => (
|
|
||||||
<CommandPrimitive.Empty
|
|
||||||
ref={ref}
|
|
||||||
className="py-6 text-center text-sm"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandShortcut,
|
|
||||||
CommandSeparator,
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ContextMenu = ContextMenuPrimitive.Root
|
|
||||||
|
|
||||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
|
||||||
|
|
||||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const ContextMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const ContextMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const ContextMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const ContextMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const ContextMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
ContextMenuCheckboxItem.displayName =
|
|
||||||
ContextMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const ContextMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-4 w-4 fill-current" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const ContextMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const ContextMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const ContextMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
|
||||||
ContextMenuRadioItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuShortcut,
|
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuRadioGroup,
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Drawer as DrawerPrimitive } from "vaul"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Drawer = ({
|
|
||||||
shouldScaleBackground = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
|
||||||
<DrawerPrimitive.Root
|
|
||||||
shouldScaleBackground={shouldScaleBackground}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
Drawer.displayName = "Drawer"
|
|
||||||
|
|
||||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
|
||||||
|
|
||||||
const DrawerPortal = DrawerPrimitive.Portal
|
|
||||||
|
|
||||||
const DrawerClose = DrawerPrimitive.Close
|
|
||||||
|
|
||||||
const DrawerOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DrawerContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DrawerPortal>
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
|
||||||
{children}
|
|
||||||
</DrawerPrimitive.Content>
|
|
||||||
</DrawerPortal>
|
|
||||||
))
|
|
||||||
DrawerContent.displayName = "DrawerContent"
|
|
||||||
|
|
||||||
const DrawerHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
DrawerHeader.displayName = "DrawerHeader"
|
|
||||||
|
|
||||||
const DrawerFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
DrawerFooter.displayName = "DrawerFooter"
|
|
||||||
|
|
||||||
const DrawerTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const DrawerDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DrawerPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Drawer,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerOverlay,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerDescription,
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
||||||
"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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
||||||
return (
|
|
||||||
<fieldset
|
|
||||||
data-slot="field-set"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-6",
|
|
||||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLegend({
|
|
||||||
className,
|
|
||||||
variant = "legend",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
||||||
return (
|
|
||||||
<legend
|
|
||||||
data-slot="field-legend"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"mb-3 font-medium",
|
|
||||||
"data-[variant=legend]:text-base",
|
|
||||||
"data-[variant=label]:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className={cn(
|
|
||||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
|
||||||
"group/field data-[invalid=true]:text-destructive flex w-full gap-3",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
||||||
horizontal: [
|
|
||||||
"flex-row items-center",
|
|
||||||
"[&>[data-slot=field-label]]:flex-auto",
|
|
||||||
"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
|
|
||||||
],
|
|
||||||
responsive: [
|
|
||||||
"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
|
|
||||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
||||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: "vertical",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Field({
|
|
||||||
className,
|
|
||||||
orientation = "vertical",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="field"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-content"
|
|
||||||
className={cn(
|
|
||||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
||||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
|
|
||||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
||||||
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
|
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
children?: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-separator"
|
|
||||||
data-content={!!children}
|
|
||||||
className={cn(
|
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
|
||||||
{children && (
|
|
||||||
<span
|
|
||||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
|
||||||
data-slot="field-separator-content"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldError({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
errors,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
errors?: Array<{ message?: string } | undefined>
|
|
||||||
}) {
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (children) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors?.length === 1 && errors[0]?.message) {
|
|
||||||
return errors[0].message
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
||||||
{errors.map(
|
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}, [children, errors])
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
data-slot="field-error"
|
|
||||||
className={cn("text-destructive text-sm font-normal", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Field,
|
|
||||||
FieldLabel,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldContent,
|
|
||||||
FieldTitle,
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
@@ -102,13 +102,13 @@ const FormLabel = React.forwardRef<
|
|||||||
FormLabel.displayName = "FormLabel"
|
FormLabel.displayName = "FormLabel"
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
const FormControl = React.forwardRef<
|
||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof SlotPrimitive.Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<SlotPrimitive.Slot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
|
||||||
|
|
||||||
const HoverCardContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<HoverCardPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="input-group"
|
|
||||||
role="group"
|
|
||||||
className={cn(
|
|
||||||
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
|
|
||||||
"h-9 has-[>textarea]:h-auto",
|
|
||||||
|
|
||||||
// Variants based on alignment.
|
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
||||||
|
|
||||||
// Focus state.
|
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
|
|
||||||
|
|
||||||
// Error state.
|
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
||||||
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
|
||||||
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
align: {
|
|
||||||
"inline-start":
|
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
||||||
"inline-end":
|
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]",
|
|
||||||
"block-start":
|
|
||||||
"[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5",
|
|
||||||
"block-end":
|
|
||||||
"[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
align: "inline-start",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupAddon({
|
|
||||||
className,
|
|
||||||
align = "inline-start",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="input-group-addon"
|
|
||||||
data-align={align}
|
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
|
||||||
"flex items-center gap-2 text-sm shadow-none",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
||||||
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
|
||||||
"icon-xs":
|
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: "xs",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupButton({
|
|
||||||
className,
|
|
||||||
type = "button",
|
|
||||||
variant = "ghost",
|
|
||||||
size = "xs",
|
|
||||||
...props
|
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type={type}
|
|
||||||
data-size={size}
|
|
||||||
variant={variant}
|
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupTextarea({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupInput,
|
|
||||||
InputGroupTextarea,
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { OTPInput, OTPInputContext } from "input-otp"
|
|
||||||
import { Minus } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const InputOTP = React.forwardRef<
|
|
||||||
React.ElementRef<typeof OTPInput>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
|
||||||
>(({ className, containerClassName, ...props }, ref) => (
|
|
||||||
<OTPInput
|
|
||||||
ref={ref}
|
|
||||||
containerClassName={cn(
|
|
||||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
|
||||||
containerClassName
|
|
||||||
)}
|
|
||||||
className={cn("disabled:cursor-not-allowed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
InputOTP.displayName = "InputOTP"
|
|
||||||
|
|
||||||
const InputOTPGroup = React.forwardRef<
|
|
||||||
React.ElementRef<"div">,
|
|
||||||
React.ComponentPropsWithoutRef<"div">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
|
||||||
))
|
|
||||||
InputOTPGroup.displayName = "InputOTPGroup"
|
|
||||||
|
|
||||||
const InputOTPSlot = React.forwardRef<
|
|
||||||
React.ElementRef<"div">,
|
|
||||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
|
||||||
>(({ index, className, ...props }, ref) => {
|
|
||||||
const inputOTPContext = React.useContext(OTPInputContext)
|
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
|
||||||
isActive && "z-10 ring-1 ring-ring",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{char}
|
|
||||||
{hasFakeCaret && (
|
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
InputOTPSlot.displayName = "InputOTPSlot"
|
|
||||||
|
|
||||||
const InputOTPSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<"div">,
|
|
||||||
React.ComponentPropsWithoutRef<"div">
|
|
||||||
>(({ ...props }, ref) => (
|
|
||||||
<div ref={ref} role="separator" {...props}>
|
|
||||||
<Minus />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
|
||||||
|
|
||||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="list"
|
|
||||||
data-slot="item-group"
|
|
||||||
className={cn("group/item-group flex flex-col", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
data-slot="item-separator"
|
|
||||||
orientation="horizontal"
|
|
||||||
className={cn("my-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemVariants = cva(
|
|
||||||
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
outline: "border-border",
|
|
||||||
muted: "bg-muted/50",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "gap-4 p-4 ",
|
|
||||||
sm: "gap-2.5 px-4 py-3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Item({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> &
|
|
||||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "div"
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="item"
|
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
className={cn(itemVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemMediaVariants = cva(
|
|
||||||
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
icon: "bg-muted size-8 rounded-sm border [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
image:
|
|
||||||
"size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function ItemMedia({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-media"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(itemMediaVariants({ variant, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-content"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-title"
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit items-center gap-2 text-sm font-medium leading-snug",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="item-description"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground line-clamp-2 text-balance text-sm font-normal leading-normal",
|
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-actions"
|
|
||||||
className={cn("flex items-center gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-header"
|
|
||||||
className={cn(
|
|
||||||
"flex basis-full items-center justify-between gap-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="item-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex basis-full items-center justify-between gap-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Item,
|
|
||||||
ItemMedia,
|
|
||||||
ItemContent,
|
|
||||||
ItemActions,
|
|
||||||
ItemGroup,
|
|
||||||
ItemSeparator,
|
|
||||||
ItemTitle,
|
|
||||||
ItemDescription,
|
|
||||||
ItemHeader,
|
|
||||||
ItemFooter,
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
||||||
return (
|
|
||||||
<kbd
|
|
||||||
data-slot="kbd"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium",
|
|
||||||
"[&_svg:not([class*='size-'])]:size-3",
|
|
||||||
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<kbd
|
|
||||||
data-slot="kbd-group"
|
|
||||||
className={cn("inline-flex items-center gap-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Kbd, KbdGroup }
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import { Label as LabelPrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function MenubarMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
|
||||||
return <MenubarPrimitive.Menu {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
|
||||||
return <MenubarPrimitive.Group {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
|
||||||
return <MenubarPrimitive.Portal {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
|
||||||
return <MenubarPrimitive.RadioGroup {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenubarSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
|
||||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const Menubar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const MenubarTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const MenubarSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</MenubarPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const MenubarSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const MenubarContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
|
||||||
ref
|
|
||||||
) => (
|
|
||||||
<MenubarPrimitive.Portal>
|
|
||||||
<MenubarPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</MenubarPrimitive.Portal>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const MenubarItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const MenubarCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<MenubarPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</MenubarPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenubarPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const MenubarRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<MenubarPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-4 w-4 fill-current" />
|
|
||||||
</MenubarPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenubarPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const MenubarLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const MenubarSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<MenubarPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const MenubarShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MenubarShortcut.displayname = "MenubarShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Menubar,
|
|
||||||
MenubarMenu,
|
|
||||||
MenubarTrigger,
|
|
||||||
MenubarContent,
|
|
||||||
MenubarItem,
|
|
||||||
MenubarSeparator,
|
|
||||||
MenubarLabel,
|
|
||||||
MenubarCheckboxItem,
|
|
||||||
MenubarRadioGroup,
|
|
||||||
MenubarRadioItem,
|
|
||||||
MenubarPortal,
|
|
||||||
MenubarSubContent,
|
|
||||||
MenubarSubTrigger,
|
|
||||||
MenubarGroup,
|
|
||||||
MenubarSub,
|
|
||||||
MenubarShortcut,
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
|
||||||
import { cva } from "class-variance-authority"
|
|
||||||
import { ChevronDown } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const NavigationMenu = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<NavigationMenuViewport />
|
|
||||||
</NavigationMenuPrimitive.Root>
|
|
||||||
))
|
|
||||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const NavigationMenuList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
|
||||||
|
|
||||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
|
||||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
|
||||||
)
|
|
||||||
|
|
||||||
const NavigationMenuTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}{" "}
|
|
||||||
<ChevronDown
|
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</NavigationMenuPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const NavigationMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
|
||||||
|
|
||||||
const NavigationMenuViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
|
||||||
<NavigationMenuPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
NavigationMenuViewport.displayName =
|
|
||||||
NavigationMenuPrimitive.Viewport.displayName
|
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Indicator
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
|
||||||
</NavigationMenuPrimitive.Indicator>
|
|
||||||
))
|
|
||||||
NavigationMenuIndicator.displayName =
|
|
||||||
NavigationMenuPrimitive.Indicator.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuIndicator,
|
|
||||||
NavigationMenuViewport,
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
|
||||||
|
|
||||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
))
|
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import { Progress as ProgressPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
||||||
import { CircleIcon } from "lucide-react"
|
import { CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,45 +1,50 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { GripVertical } from "lucide-react"
|
|
||||||
import * as ResizablePrimitive from "react-resizable-panels"
|
import * as ResizablePrimitive from "react-resizable-panels"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ResizablePanelGroup = ({
|
function ResizablePanelGroup({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
}: ResizablePrimitive.GroupProps) {
|
||||||
<ResizablePrimitive.PanelGroup
|
return (
|
||||||
className={cn(
|
<ResizablePrimitive.Group
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
data-slot="resizable-panel-group"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"flex h-full w-full aria-[orientation=vertical]:flex-col",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
)
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ResizablePanel = ResizablePrimitive.Panel
|
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
|
||||||
|
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const ResizableHandle = ({
|
function ResizableHandle({
|
||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: ResizablePrimitive.SeparatorProps & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) => (
|
}) {
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
return (
|
||||||
className={cn(
|
<ResizablePrimitive.Separator
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
data-slot="resizable-handle"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"bg-border focus-visible:ring-ring ring-offset-background relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
{withHandle && (
|
{...props}
|
||||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
>
|
||||||
<GripVertical className="h-2.5 w-2.5" />
|
{withHandle && (
|
||||||
</div>
|
<div className="bg-border h-6 w-1 rounded-lg z-10 flex shrink-0" />
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import { Select as SelectPrimitive } from "radix-ui"
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot as SlotPrimitive } from "radix-ui"
|
||||||
import { cva, VariantProps } from "class-variance-authority"
|
import { cva, VariantProps } from "class-variance-authority"
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ function SidebarGroupLabel({
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? SlotPrimitive.Slot : "div"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -435,7 +435,7 @@ function SidebarGroupAction({
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -524,7 +524,7 @@ function SidebarMenuButton({
|
|||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
||||||
const { isMobile, state } = useSidebar()
|
const { isMobile, state } = useSidebar()
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
@@ -570,7 +570,7 @@ function SidebarMenuAction({
|
|||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
showOnHover?: boolean
|
showOnHover?: boolean
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? SlotPrimitive.Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -693,7 +693,7 @@ function SidebarMenuSubButton({
|
|||||||
size?: "sm" | "md"
|
size?: "sm" | "md"
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? SlotPrimitive.Slot : "a"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import { Slider as SliderPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import { Switch as SwitchPrimitives } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Table = React.forwardRef<
|
|
||||||
HTMLTableElement,
|
|
||||||
React.HTMLAttributes<HTMLTableElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="relative w-full overflow-auto">
|
|
||||||
<table
|
|
||||||
ref={ref}
|
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
Table.displayName = "Table"
|
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
|
||||||
HTMLTableSectionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
||||||
))
|
|
||||||
TableHeader.displayName = "TableHeader"
|
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
|
||||||
HTMLTableSectionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tbody
|
|
||||||
ref={ref}
|
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TableBody.displayName = "TableBody"
|
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
|
||||||
HTMLTableSectionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tfoot
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TableFooter.displayName = "TableFooter"
|
|
||||||
|
|
||||||
const TableRow = React.forwardRef<
|
|
||||||
HTMLTableRowElement,
|
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tr
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TableRow.displayName = "TableRow"
|
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
|
||||||
HTMLTableCellElement,
|
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<th
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TableHead.displayName = "TableHead"
|
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
|
||||||
HTMLTableCellElement,
|
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<td
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TableCell.displayName = "TableCell"
|
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
|
||||||
HTMLTableCaptionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<caption
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TableCaption.displayName = "TableCaption"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
|
||||||
|
|
||||||
const toastVariants = cva(
|
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "border bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
||||||
VariantProps<typeof toastVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<ToastPrimitives.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(toastVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Close
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
toast-close=""
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</ToastPrimitives.Close>
|
|
||||||
))
|
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm opacity-90", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
|
||||||
|
|
||||||
export {
|
|
||||||
type ToastProps,
|
|
||||||
type ToastActionElement,
|
|
||||||
ToastProvider,
|
|
||||||
ToastViewport,
|
|
||||||
Toast,
|
|
||||||
ToastTitle,
|
|
||||||
ToastDescription,
|
|
||||||
ToastClose,
|
|
||||||
ToastAction,
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useToast } from "@/hooks/use-toast"
|
|
||||||
import {
|
|
||||||
Toast,
|
|
||||||
ToastClose,
|
|
||||||
ToastDescription,
|
|
||||||
ToastProvider,
|
|
||||||
ToastTitle,
|
|
||||||
ToastViewport,
|
|
||||||
} from "@/components/ui/toast"
|
|
||||||
|
|
||||||
export function Toaster() {
|
|
||||||
const { toasts } = useToast()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToastProvider>
|
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
||||||
return (
|
|
||||||
<Toast key={id} {...props}>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
|
||||||
{description && (
|
|
||||||
<ToastDescription>{description}</ToastDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
<ToastClose />
|
|
||||||
</Toast>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<ToastViewport />
|
|
||||||
</ToastProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
||||||
import { type VariantProps } from "class-variance-authority"
|
import { type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
import { Toggle as TogglePrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { relaunch as relaunchApp } from "@tauri-apps/plugin-process";
|
|||||||
import { useSettingsPageStatesStore } from "@/services/store";
|
import { useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { useLogger } from "@/helpers/use-logger";
|
import { useLogger } from "@/helpers/use-logger";
|
||||||
import { sendNotification } from '@tauri-apps/plugin-notification';
|
import { sendNotification } from '@tauri-apps/plugin-notification';
|
||||||
|
import usePotServer from "@/helpers/use-pot-server";
|
||||||
|
|
||||||
export default function useAppUpdater() {
|
export default function useAppUpdater() {
|
||||||
const setIsCheckingAppUpdate = useSettingsPageStatesStore(state => state.setIsCheckingAppUpdate);
|
const setIsCheckingAppUpdate = useSettingsPageStatesStore(state => state.setIsCheckingAppUpdate);
|
||||||
@@ -12,6 +13,9 @@ export default function useAppUpdater() {
|
|||||||
const setDownloadProgress = useSettingsPageStatesStore(state => state.setAppUpdateDownloadProgress);
|
const setDownloadProgress = useSettingsPageStatesStore(state => state.setAppUpdateDownloadProgress);
|
||||||
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications);
|
||||||
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification);
|
||||||
|
const isRunningPotServer = useSettingsPageStatesStore(state => state.isRunningPotServer);
|
||||||
|
|
||||||
|
const { stopPotServer } = usePotServer();
|
||||||
const LOG = useLogger();
|
const LOG = useLogger();
|
||||||
|
|
||||||
const checkForAppUpdate = async () => {
|
const checkForAppUpdate = async () => {
|
||||||
@@ -38,6 +42,10 @@ export default function useAppUpdater() {
|
|||||||
|
|
||||||
const downloadAndInstallAppUpdate = async (update: Update) => {
|
const downloadAndInstallAppUpdate = async (update: Update) => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
|
if (isRunningPotServer) {
|
||||||
|
LOG.info('NEODLP', 'Stopping POT Server before starting app update');
|
||||||
|
await stopPotServer();
|
||||||
|
}
|
||||||
LOG.info('NEODLP', `Downloading and installing app update v${update.version}`);
|
LOG.info('NEODLP', `Downloading and installing app update v${update.version}`);
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let contentLength: number | undefined = 0;
|
let contentLength: number | undefined = 0;
|
||||||
|
|||||||