mirror of
https://github.com/neosubhamoy/neodlp-extension.git
synced 2025-12-19 23:29:33 +05:30
(chore): initial MVP release v0.1.0
This commit is contained in:
90
.github/workflows/release.yml
vendored
Normal file
90
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
name: 🚀 Release on GitHub
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 🚚 Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 📦 Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: 📦 Setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 'lts/*'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: 🛠️ Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: 🤐 Zip extensions
|
||||||
|
run: |
|
||||||
|
pnpm zip
|
||||||
|
pnpm zip:firefox
|
||||||
|
|
||||||
|
- name: ✨ Extract version number
|
||||||
|
id: extract_version
|
||||||
|
run: |
|
||||||
|
VERSION_NUM=$(echo "${{ github.ref_name }}" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
|
||||||
|
echo "version=$VERSION_NUM" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 📄 Read and process changelog
|
||||||
|
id: changelog
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -f CHANGELOG.md ]; then
|
||||||
|
# Read and replace placeholders
|
||||||
|
CONTENT=$(cat CHANGELOG.md)
|
||||||
|
CONTENT=${CONTENT//<release_tag>/${{ github.ref_name }}}
|
||||||
|
CONTENT=${CONTENT//<version>/${{ env.version }}}
|
||||||
|
|
||||||
|
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Also save to a processed file for later use
|
||||||
|
echo "$CONTENT" > CHANGELOG_PROCESSED.md
|
||||||
|
else
|
||||||
|
echo "content=No changelog found" >> $GITHUB_OUTPUT
|
||||||
|
echo "No changelog found" > CHANGELOG_PROCESSED.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 📝 Create latest.json
|
||||||
|
id: create_latest_json
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Create latest.json
|
||||||
|
cat > latest.json << EOF
|
||||||
|
{
|
||||||
|
"version": "${{env.version}}",
|
||||||
|
"notes": $(cat CHANGELOG_PROCESSED.md | jq -s -R .),
|
||||||
|
"browsers": {
|
||||||
|
"chrome": {
|
||||||
|
"url": "https://github.com/${{github.repository}}/releases/download/${{github.ref_name}}/${{github.event.repository.name}}-${{env.version}}-chrome.zip"
|
||||||
|
},
|
||||||
|
"firefox": {
|
||||||
|
"url": "https://github.com/${{github.repository}}/releases/download/${{github.ref_name}}/${{github.event.repository.name}}-${{env.version}}-firefox.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: 🚀 Upload to github releases
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: ${{github.event.repository.name}}-${{github.ref_name}}
|
||||||
|
body_path: CHANGELOG_PROCESSED.md
|
||||||
|
files: |
|
||||||
|
.output/${{github.event.repository.name}}-${{env.version}}-chrome.zip
|
||||||
|
.output/${{github.event.repository.name}}-${{env.version}}-firefox.zip
|
||||||
|
.output/${{github.event.repository.name}}-${{env.version}}-sources.zip
|
||||||
|
latest.json
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
make_latest: true
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.output
|
||||||
|
stats.html
|
||||||
|
stats-*.json
|
||||||
|
.wxt
|
||||||
|
web-ext.config.ts
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
10
CHANGELOG.md
Normal file
10
CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
### ✨ Changelog
|
||||||
|
|
||||||
|
- initial MVP release v0.1.0
|
||||||
|
|
||||||
|
### ⬇️ Download Section
|
||||||
|
|
||||||
|
| Type\Browser | Chrome / Chromium based | Firefox / Firefox based |
|
||||||
|
| :---- | :---- | :---- |
|
||||||
|
| Store Listed (Recommended - Auto updates) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||||
|
| Unpackable ZIP (Manual updates) | [Download](https://github.com/neosubhamoy/neodlp-extension/releases/download/<release_tag>/neodlp-extension-<version>-chrome.zip) | [Download](https://github.com/neosubhamoy/neodlp-extension/releases/download/<release_tag>/neodlp-extension-<version>-firefox.zip) |
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Subhamoy Biswas
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# NeoDLP Extension
|
||||||
|
|
||||||
|
NeoDLP (Neo Downloader Plus) Browser Integration
|
||||||
|
|
||||||
|
[](https://github.com/neosubhamoy/neodlp-extension)
|
||||||
|
[](https://github.com/neosubhamoy/neodlp-extension)
|
||||||
|
[](https://github.com/neosubhamoy/neodlp-extension)
|
||||||
|
|
||||||
|
> **🥰 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!**
|
||||||
|
|
||||||
|
### 📎 Pre-Requirements
|
||||||
|
|
||||||
|
- [NeoDLP](https://github.com/neosubhamoy/neodlp) (Installed and Running)
|
||||||
|
|
||||||
|
### ⬇️ Download and Installation
|
||||||
|
|
||||||
|
#### Manual Installation (in Google Chrome / Chromium based browsers)
|
||||||
|
|
||||||
|
1. Download the `neodlp-extension-<version>-chrome.zip` file from the [latest release](https://github.com/neosubhamoy/neodlp-extension/releases).
|
||||||
|
2. Extract the `.zip` file (using any zip extractor software. eg: 7zip, WinRAR etc.).
|
||||||
|
3. Open Google Chrome and navigate to `chrome://extensions/`.
|
||||||
|
4. Enable "Developer mode" in the top right corner.
|
||||||
|
5. Click on "Load unpacked" and select the newly extracted folder. Done! That's it...!!
|
||||||
|
|
||||||
|
#### Manual Installation (in Mozilla Firefox)
|
||||||
|
|
||||||
|
1. Download the `neodlp-extension-<version>-firefox.zip` file from the [latest release](https://github.com/neosubhamoy/neodlp-extension/releases).
|
||||||
|
2. Open Firefox and navigate to `about:addons`.
|
||||||
|
3. Click on the gear icon (top right) and select "Install Add-on From File...".
|
||||||
|
4. Select the downloaded `.zip` file. Done! That's it...!!
|
||||||
|
|
||||||
|
### ⚡ Technologies Used
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### 🛠️ Contributing / Building from Source
|
||||||
|
|
||||||
|
Want to be part of this? Feel free to contribute...!! Pull Requests are always welcome...!! (^_^) Follow these simple steps to start building:
|
||||||
|
|
||||||
|
* Make sure to install Node.js, Git before proceeding.
|
||||||
|
1. Fork this repo in your github account.
|
||||||
|
2. Git clone the forked repo in your local machine.
|
||||||
|
3. Install Node.js dependencies: `npm install`
|
||||||
|
4. Run development / build / zipping process
|
||||||
|
```code
|
||||||
|
npm run dev # for development (chrome)
|
||||||
|
npm run dev:firefox # for development (firefox)
|
||||||
|
npm run build # for production build (chrome)
|
||||||
|
npm run build:firefox # for production build (firefox)
|
||||||
|
npm run zip # for production zip creation (chrome)
|
||||||
|
npm run zip:firefox # for production zip creation (firefox)
|
||||||
|
```
|
||||||
|
5. Do the changes, Send a Pull Request with proper Description (NOTE: Pull Requests Without Proper Description will be Rejected)
|
||||||
|
|
||||||
|
**⭕ Noticed any Bugs or Want to give us some suggetions? Always feel free to open a GitHub Issue. We would love to hear from you...!!**
|
||||||
|
|
||||||
|
### 📝 License
|
||||||
|
|
||||||
|
NeoDLP Extension is Licensed under the [MIT license](https://github.com/neosubhamoy/neodlp-extension/blob/main/LICENSE). Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions.
|
||||||
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "entrypoints/popup/style.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
13
components/footer.tsx
Normal file
13
components/footer.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const manifest = browser.runtime.getManifest();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="flex flex-col items-center justify-center text-center w-full">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
<a href={manifest.homepage_url} target="_blank" className="">NeoDLP Extension</a> © {new Date().getFullYear()} - <a href="https://github.com/neosubhamoy/neodlp-extension/blob/main/LICENSE" target="_blank">MIT License</a> <br></br> Made with ❤️ by <a href="https://neosubhamoy.com" target="_blank">Subhamoy</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
components/header.tsx
Normal file
18
components/header.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { NeoDlpLogo } from "@/components/icons/neodlp";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const manifest = browser.runtime.getManifest();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-center space-x-4">
|
||||||
|
<NeoDlpLogo className="w-8 h-8 rounded-lg" />
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<h1 className="text-sm font-semibold">{manifest.name}</h1>
|
||||||
|
<p className="text-xs">Extension - v{manifest.version}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
15
components/icons/neodlp.tsx
Normal file
15
components/icons/neodlp.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function NeoDlpLogo({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||||
|
<rect width="1024" height="1024" fill="url(#paint0_linear_10_2)"/>
|
||||||
|
<path d="M529.252 811.098C519.472 820.96 503.528 820.96 493.748 811.098L256.265 571.603C240.619 555.824 251.796 529 274.017 529H748.983C771.204 529 782.381 555.824 766.735 571.603L529.252 811.098Z" fill="#FAFAFA"/>
|
||||||
|
<rect x="355" y="222" width="313" height="346" rx="25" fill="#FAFAFA"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_10_2" x1="129.5" y1="148.5" x2="921" y2="863" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stopColor="#4444FF"/>
|
||||||
|
<stop offset="1" stopColor="#FF43D0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
73
components/theme-provider.tsx
Normal file
73
components/theme-provider.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTheme?: Theme
|
||||||
|
storageKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ThemeProviderState = {
|
||||||
|
theme: "system",
|
||||||
|
setTheme: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
|
||||||
|
root.classList.add(systemTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, theme)
|
||||||
|
setTheme(theme)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext)
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
66
components/ui/alert.tsx
Normal file
66
components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
59
components/ui/button.tsx
Normal file
59
components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
92
components/ui/card.tsx
Normal file
92
components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
165
components/ui/form.tsx
Normal file
165
components/ui/form.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState } = useFormContext()
|
||||||
|
const formState = useFormState({ name: fieldContext.name })
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
||||||
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
29
components/ui/switch.tsx
Normal file
29
components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
18
components/ui/textarea.tsx
Normal file
18
components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
112
entrypoints/background.ts
Normal file
112
entrypoints/background.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { WebsocketMessage } from "@/types/websocket";
|
||||||
|
|
||||||
|
export default defineBackground(() => {
|
||||||
|
const sendMessageToNativeHost = (message: WebsocketMessage) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let port = browser.runtime.connectNative('com.neosubhamoy.neodlp');
|
||||||
|
|
||||||
|
port.onMessage.addListener((response) => {
|
||||||
|
console.log('Received from native host:', response);
|
||||||
|
if (response.status === 'success') {
|
||||||
|
resolve(response.response);
|
||||||
|
} else if (response.status === 'error') {
|
||||||
|
reject(new Error(response.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
port.onDisconnect.addListener(() => {
|
||||||
|
if (browser.runtime.lastError) {
|
||||||
|
reject(new Error(browser.runtime.lastError.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
port.postMessage(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for messages from the popup or content scripts
|
||||||
|
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.action === 'download') {
|
||||||
|
const url = request.url;
|
||||||
|
sendMessageToNativeHost({url: url, command: 'download', argument: ''}).then(response => sendResponse(response))
|
||||||
|
return true; // Keep the message channel open for sendResponse to be called asynchronously
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for the keyboard commands
|
||||||
|
browser.commands.onCommand.addListener(async (command) => {
|
||||||
|
if (command === "neodlp:quick-download") {
|
||||||
|
try {
|
||||||
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const activeTab = tabs[0];
|
||||||
|
|
||||||
|
if (activeTab && activeTab.url) {
|
||||||
|
console.log("Quick download triggered for URL:", activeTab.url);
|
||||||
|
|
||||||
|
const response = await sendMessageToNativeHost({
|
||||||
|
url: activeTab.url,
|
||||||
|
command: 'download',
|
||||||
|
argument: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Quick download response:", response);
|
||||||
|
} else {
|
||||||
|
console.error("No active tab or URL found for quick download");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in quick download:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear existing context menus before creating new ones
|
||||||
|
browser.contextMenus.removeAll().then(() => {
|
||||||
|
// Context menu for quick download
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: "quick-download:page",
|
||||||
|
title: "Download with Neo Downloader Plus",
|
||||||
|
contexts: ["page"]
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: "quick-download:link",
|
||||||
|
title: "Download Link with Neo Downloader Plus",
|
||||||
|
contexts: ["link"]
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: "quick-download:media",
|
||||||
|
title: "Download Media with Neo Downloader Plus",
|
||||||
|
contexts: ["video", "audio"]
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: "quick-download:selection",
|
||||||
|
title: "Download Selection with Neo Downloader Plus",
|
||||||
|
contexts: ["selection"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
|
let url = '';
|
||||||
|
if (info.menuItemId === "quick-download:page") {
|
||||||
|
if(!info.pageUrl) return;
|
||||||
|
url = info.pageUrl;
|
||||||
|
} else if (info.menuItemId === "quick-download:link") {
|
||||||
|
if(!info.linkUrl) return;
|
||||||
|
url = info.linkUrl;
|
||||||
|
} else if (info.menuItemId === "quick-download:media") {
|
||||||
|
if(!info.srcUrl) return;
|
||||||
|
url = info.srcUrl;
|
||||||
|
} else if (info.menuItemId === "quick-download:selection") {
|
||||||
|
if(!info.selectionText) return;
|
||||||
|
url = info.selectionText;
|
||||||
|
}
|
||||||
|
if (!url) return;
|
||||||
|
sendMessageToNativeHost({url: url, command: 'download', argument: ''}).then(response => {
|
||||||
|
console.log("Context menu download response:", response);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Error in context menu download:", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
38
entrypoints/popup/App.tsx
Normal file
38
entrypoints/popup/App.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { Settings } from "@/types/settings";
|
||||||
|
|
||||||
|
function App({ children }: { children: React.ReactNode }) {
|
||||||
|
const [settings, setSettings] = useState<Settings>({
|
||||||
|
theme: "system",
|
||||||
|
autofill_url: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// loading the settings from storage if available, overwriting the default values when the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const result = await browser.storage.local.get('settings');
|
||||||
|
if (result.settings) {
|
||||||
|
// Merge saved settings with default settings
|
||||||
|
// Only override keys that exist in saved settings, keeping defaults otherwise
|
||||||
|
setSettings(prevSettings => ({
|
||||||
|
...prevSettings,
|
||||||
|
...result.settings
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load settings:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider defaultTheme={settings.theme || "system"} storageKey="vite-ui-theme">
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
13
entrypoints/popup/index.html
Normal file
13
entrypoints/popup/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Neo Downloader Plus</title>
|
||||||
|
<meta name="manifest.type" content="browser_action" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
entrypoints/popup/main.tsx
Normal file
21
entrypoints/popup/main.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from '@/entrypoints/popup/App.tsx';
|
||||||
|
import '@/entrypoints/popup/style.css';
|
||||||
|
import { HashRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import RootLayout from '@/pages/layout/root';
|
||||||
|
import HomePage from '@/pages/home';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<HashRouter>
|
||||||
|
<App>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<RootLayout />}>
|
||||||
|
<Route index element={<HomePage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</App>
|
||||||
|
</HashRouter>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
123
entrypoints/popup/style.css
Normal file
123
entrypoints/popup/style.css
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.145 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.145 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.985 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.269 0 0);
|
||||||
|
--input: oklch(0.269 0 0);
|
||||||
|
--ring: oklch(0.439 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(0.269 0 0);
|
||||||
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
8598
package-lock.json
generated
Normal file
8598
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
package.json
Normal file
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "neodlp-extension",
|
||||||
|
"description": "NeoDLP Browser Integration",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wxt",
|
||||||
|
"dev:firefox": "wxt -b firefox",
|
||||||
|
"build": "wxt build",
|
||||||
|
"build:firefox": "wxt build -b firefox",
|
||||||
|
"zip": "wxt zip",
|
||||||
|
"zip:firefox": "wxt zip -b firefox",
|
||||||
|
"compile": "tsc --noEmit",
|
||||||
|
"postinstall": "wxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"@radix-ui/react-label": "^2.1.4",
|
||||||
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
|
"@radix-ui/react-switch": "^1.2.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.8",
|
||||||
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.501.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-hook-form": "^7.56.0",
|
||||||
|
"react-router-dom": "^7.5.1",
|
||||||
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"tailwindcss": "^4.1.4",
|
||||||
|
"tw-animate-css": "^1.2.7",
|
||||||
|
"zod": "^3.24.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.1",
|
||||||
|
"@types/react": "^19.1.0",
|
||||||
|
"@types/react-dom": "^19.1.2",
|
||||||
|
"@wxt-dev/module-react": "^1.1.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"wxt": "^0.20.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
271
pages/home.tsx
Normal file
271
pages/home.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { AlertCircle, Download, Loader2, LucideIcon, Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
import { type Browser } from 'wxt/browser';
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Settings } from "@/types/settings";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/components/theme-provider";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
|
||||||
|
const downloadFormSchema = z.object({
|
||||||
|
url: z.string().min(1, { message: "URL is required" }).url({message: "Invalid URL format." }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const [isLoadingSettings, setIsLoadingSettings] = useState(true);
|
||||||
|
const [isUpdatingSettings, setIsUpdatingSettings] = useState(false);
|
||||||
|
const [showNotRunningAlert, setShowNotRunningAlert] = useState(false);
|
||||||
|
const [settings, setSettings] = useState<Settings>({
|
||||||
|
theme: "system",
|
||||||
|
autofill_url: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeOptions: { value: 'system' | 'dark' | 'light'; icon: LucideIcon; label: string }[] = [
|
||||||
|
{ value: 'light', icon: Sun, label: 'Light' },
|
||||||
|
{ value: 'dark', icon: Moon, label: 'Dark' },
|
||||||
|
{ value: 'system', icon: Monitor, label: 'System' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const downloadForm = useForm<z.infer<typeof downloadFormSchema>>({
|
||||||
|
resolver: zodResolver(downloadFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedUrl = downloadForm.watch("url");
|
||||||
|
|
||||||
|
const handleDownload = async (url?: string) => {
|
||||||
|
setIsDownloading(true);
|
||||||
|
setShowNotRunningAlert(false); // Reset alert status at the beginning
|
||||||
|
|
||||||
|
// Create a timeout reference with undefined type
|
||||||
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tabs = await new Promise<Browser.tabs.Tab[]>(resolve => {
|
||||||
|
browser.tabs.query({active: true, currentWindow: true}, resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTab = tabs[0];
|
||||||
|
|
||||||
|
// Create a race between the actual message and a timeout
|
||||||
|
const response = await Promise.race([
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
action: 'download',
|
||||||
|
url: url ?? activeTab.url
|
||||||
|
}),
|
||||||
|
new Promise((_, reject) => {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error('TIMEOUT'));
|
||||||
|
}, 5000); // 5 second timeout
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If we reach here, the request completed successfully
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
console.log('Response from background script:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Download failed", error);
|
||||||
|
|
||||||
|
// Check if this was a timeout error
|
||||||
|
if (error instanceof Error && error.message === 'TIMEOUT') {
|
||||||
|
setShowNotRunningAlert(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the timeout if it was some other error
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSubmit = async (values: z.infer<typeof downloadFormSchema>) => {
|
||||||
|
await handleDownload(values.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSettings = async <K extends keyof Settings>(key: K, value: Settings[K]) => {
|
||||||
|
setIsUpdatingSettings(true);
|
||||||
|
try {
|
||||||
|
// First, get current settings from storage
|
||||||
|
const result = await browser.storage.local.get('settings');
|
||||||
|
const currentSettings = result.settings || {};
|
||||||
|
|
||||||
|
// Update with new value
|
||||||
|
const updatedSettings = {
|
||||||
|
...currentSettings,
|
||||||
|
[key]: value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to storage
|
||||||
|
await browser.storage.local.set({ settings: updatedSettings });
|
||||||
|
|
||||||
|
// Update state if save was successful
|
||||||
|
setSettings(prevSettings => ({
|
||||||
|
...prevSettings,
|
||||||
|
[key]: value
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`Settings ${key} updated to:`, value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to save settings ${key}:`, error);
|
||||||
|
} finally {
|
||||||
|
setIsUpdatingSettings(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// loading the settings from storage if available, overwriting the default values when the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const result = await browser.storage.local.get('settings');
|
||||||
|
if (result.settings) {
|
||||||
|
// Merge saved settings with default settings
|
||||||
|
// Only override keys that exist in saved settings, keeping defaults otherwise
|
||||||
|
setSettings(prevSettings => ({
|
||||||
|
...prevSettings,
|
||||||
|
...result.settings
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load settings:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSettings(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-fill the URL field with the active tab's URL when the component mounts (if autofill is enabled)
|
||||||
|
useEffect(() => {
|
||||||
|
console.log({isLoadingSettings, settings});
|
||||||
|
const autoFillUrl = async () => {
|
||||||
|
const tabs = await new Promise<Browser.tabs.Tab[]>(resolve => {
|
||||||
|
browser.tabs.query({active: true, currentWindow: true}, resolve);
|
||||||
|
});
|
||||||
|
const activeTab = tabs[0];
|
||||||
|
if (activeTab && activeTab.url) {
|
||||||
|
downloadForm.setValue("url", activeTab.url);
|
||||||
|
await downloadForm.trigger("url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isLoadingSettings && settings.autofill_url) {
|
||||||
|
autoFillUrl();
|
||||||
|
}
|
||||||
|
}, [isLoadingSettings, settings.autofill_url]);
|
||||||
|
|
||||||
|
// Listen for tab URL changes and update the form value accordingly (if autofill is enabled)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoadingSettings || !settings.autofill_url) return;
|
||||||
|
const handleTabUrlChange = async (tabId: number, changeInfo: Browser.tabs.TabChangeInfo) => {
|
||||||
|
if (changeInfo.status === "complete") {
|
||||||
|
browser.tabs.get(tabId).then(async (tab) => {
|
||||||
|
if (tab.active && tab.url) {
|
||||||
|
downloadForm.setValue("url", tab.url);
|
||||||
|
await downloadForm.trigger("url");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
browser.tabs.onUpdated.addListener(handleTabUrlChange);
|
||||||
|
return () => {
|
||||||
|
browser.tabs.onUpdated.removeListener(handleTabUrlChange);
|
||||||
|
}
|
||||||
|
}, [isLoadingSettings, settings.autofill_url]);
|
||||||
|
|
||||||
|
// Update the theme when settings change
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTheme = async () => {
|
||||||
|
setTheme(settings.theme);
|
||||||
|
}
|
||||||
|
updateTheme().catch(console.error);
|
||||||
|
}, [settings.theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content flex flex-col space-y-4 w-full">
|
||||||
|
<div className="theme-selection flex items-center justify-center">
|
||||||
|
<div className={cn('inline-flex gap-1 rounded-lg p-1 bg-muted')}>
|
||||||
|
{themeOptions.map(({ value, icon: Icon, label }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
onClick={() => saveSettings('theme', value)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center rounded-md px-[0.80rem] py-1.5 transition-colors',
|
||||||
|
settings.theme === value
|
||||||
|
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
|
||||||
|
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="-ml-1 h-4 w-4" />
|
||||||
|
<span className="ml-1.5 text-xs">{label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="autofill-url flex items-center justify-center gap-4">
|
||||||
|
<Switch
|
||||||
|
id="autofill-url"
|
||||||
|
checked={settings.autofill_url}
|
||||||
|
onCheckedChange={(checked) => saveSettings("autofill_url", checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="autofill-url">AutoFill Page URL</Label>
|
||||||
|
</div>
|
||||||
|
<Form {...downloadForm}>
|
||||||
|
<form onSubmit={downloadForm.handleSubmit(handleDownloadSubmit)} className="flex flex-col gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={downloadForm.control}
|
||||||
|
name="url"
|
||||||
|
disabled={isDownloading || isLoadingSettings || isUpdatingSettings}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="w-full h-28 resize-none text-sm"
|
||||||
|
placeholder="Enter URL"
|
||||||
|
{...field}
|
||||||
|
readOnly={settings.autofill_url}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
{showNotRunningAlert && (
|
||||||
|
<Alert variant="destructive" className="mt-2">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Host Error</AlertTitle>
|
||||||
|
<AlertDescription className="text-xs">
|
||||||
|
Make sure NeoDLP is Installed and Running
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button className="w-full cursor-pointer" type="submit" disabled={isDownloading || isLoadingSettings || isUpdatingSettings || !watchedUrl || downloadForm.getFieldState("url").invalid}>
|
||||||
|
{isDownloading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Starting
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
Download
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
pages/layout/root.tsx
Normal file
13
pages/layout/root.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Header from "@/components/header";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-w-[270px] p-4 space-y-4">
|
||||||
|
<Header />
|
||||||
|
<Outlet />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
public/icon/128.png
Normal file
BIN
public/icon/128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/icon/16.png
Normal file
BIN
public/icon/16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/32.png
Normal file
BIN
public/icon/32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/48.png
Normal file
BIN
public/icon/48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/icon/96.png
Normal file
BIN
public/icon/96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.wxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
types/settings.ts
Normal file
4
types/settings.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Settings {
|
||||||
|
theme: 'system' | 'dark' | 'light';
|
||||||
|
autofill_url: boolean;
|
||||||
|
}
|
||||||
5
types/websocket.ts
Normal file
5
types/websocket.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface WebsocketMessage {
|
||||||
|
url: string;
|
||||||
|
command: string;
|
||||||
|
argument: string;
|
||||||
|
}
|
||||||
85
wxt.config.ts
Normal file
85
wxt.config.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import path from "path"
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { defineConfig } from 'wxt';
|
||||||
|
|
||||||
|
// See https://wxt.dev/api/config.html
|
||||||
|
export default defineConfig({
|
||||||
|
modules: ['@wxt-dev/module-react'],
|
||||||
|
vite: () => ({
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
manifest: ({ browser }) => {
|
||||||
|
const manifest = {
|
||||||
|
name: "Neo Downloader Plus",
|
||||||
|
description: "Neo Downloader Plus Browser Integration",
|
||||||
|
homepage_url: "https://neodlp.neosubhamoy.com",
|
||||||
|
version: "0.1.0",
|
||||||
|
permissions: ["tabs", "storage", "contextMenus", "nativeMessaging"],
|
||||||
|
}
|
||||||
|
if (browser === 'chrome') {
|
||||||
|
return {
|
||||||
|
...manifest,
|
||||||
|
key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3XQoL6Qur86lyfRRGYiQ544w9fxJiStWvFJaNqqSlRxkoT0wj8mFVwBjtmUJC6AB31Zb9awELVk1jyo83cPoJjhydHQfk7dpQ3gygp5TdZMjwX5++FpYq5QIV1qyf9BNvGbWG1zHDPqRGC/ZtGaxb9FJyYoFMIUKoiJfuPwup0Iy3dRwJex4mxMobQnFtfoxdMRvjx6XA9v7Fz8QF1t/1lVsx9yOiJPyDDzygrVLR3+r+1Sq7CunK0CWMVPkTRMw243KMZBIDpxrjXaVbasIkZsMwVW0vkqIMXzMZGhUPu1SfflwanAJ5F2Yl0dcO3OxKLYL7szTtLJUD/7PFA2PwIDAQAB",
|
||||||
|
commands: {
|
||||||
|
_execute_action: {
|
||||||
|
suggested_key: {
|
||||||
|
default: "Alt+Shift+N",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"neodlp:quick-download": {
|
||||||
|
description: "Quick Download",
|
||||||
|
suggested_key: {
|
||||||
|
default: "Alt+Shift+Q"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (browser === 'firefox') {
|
||||||
|
return {
|
||||||
|
...manifest,
|
||||||
|
browser_specific_settings: {
|
||||||
|
gecko: {
|
||||||
|
id: "neodlp@neosubhamoy.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
_execute_browser_action: {
|
||||||
|
suggested_key: {
|
||||||
|
default: "Alt+Shift+N",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"neodlp:quick-download": {
|
||||||
|
description: "Quick Download",
|
||||||
|
suggested_key: {
|
||||||
|
default: "Alt+Shift+Q"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...manifest,
|
||||||
|
commands: {
|
||||||
|
_execute_action: {
|
||||||
|
suggested_key: {
|
||||||
|
default: "Alt+Shift+N",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"neodlp:quick-download": {
|
||||||
|
description: "Quick Download",
|
||||||
|
suggested_key: {
|
||||||
|
default: "Alt+Shift+Q"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user