mirror of
https://github.com/neosubhamoy/neodlp-extension.git
synced 2025-12-19 05:52:57 +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