(chore): initial MVP release v0.1.0

This commit is contained in:
2025-04-28 23:49:42 +05:30
commit c73022b1a2
200 changed files with 24562 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
src-tauri/binaries/* filter=lfs diff=lfs merge=lfs -text
src-tauri/resources/binaries/* filter=lfs diff=lfs merge=lfs -text

114
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
on:
push:
tags:
- 'v*.*.*' # Matches version tags like v1.0.0 etc.
name: 🚀 Release on GitHub
jobs:
release:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
args: '--target aarch64-apple-darwin --config ./src-tauri/tauri.macos-aarch64.conf.json'
arch: 'aarch64-apple-darwin'
- platform: 'macos-latest'
args: '--target x86_64-apple-darwin'
arch: 'x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
arch: ''
- platform: 'windows-latest'
args: ''
arch: ''
runs-on: ${{ matrix.platform }}
steps:
- name: 🚚 Checkout repository
uses: actions/checkout@v4
- name: 🛠️ Install dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: 📦 Setup node
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: 🛠️ install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: 🛠️ Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: 🛠️ Install frontend dependencies
run: npm install
- name: 📄 Read and Process CHANGELOG (Unix)
if: matrix.platform != 'windows-latest'
id: changelog_unix
shell: bash
run: |
if [ -f CHANGELOG.md ]; then
# Extract version number from tag
VERSION_NUM=$(echo "${{ github.ref_name }}" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$/\1/')
# Read and replace placeholders
CONTENT=$(cat CHANGELOG.md)
CONTENT=${CONTENT//<release_tag>/${{ github.ref_name }}}
CONTENT=${CONTENT//<version>/$VERSION_NUM}
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "content=No changelog found" >> $GITHUB_OUTPUT
fi
- name: 📄 Read and Process CHANGELOG (Windows)
if: matrix.platform == 'windows-latest'
id: changelog_windows
shell: pwsh
run: |
if (Test-Path "CHANGELOG.md") {
# Extract version number from tag
$version_num = "${{ github.ref_name }}" -replace '^v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$','$1'
# Read and replace placeholders
$content = Get-Content -Path CHANGELOG.md -Raw
$content = $content -replace '<release_tag>', "${{ github.ref_name }}"
$content = $content -replace '<version>', "$version_num"
"content<<EOF" >> $env:GITHUB_OUTPUT
$content >> $env:GITHUB_OUTPUT
"EOF" >> $env:GITHUB_OUTPUT
} else {
"content=No changelog found" >> $env:GITHUB_OUTPUT
}
- name: 🚀 Build and publish
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_ARCH: ${{ matrix.arch }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ github.ref_name }}
releaseName: ${{ github.event.repository.name }}-${{ github.ref_name }}
releaseBody: ${{ matrix.platform == 'windows-latest' && steps.changelog_windows.outputs.content || steps.changelog_unix.outputs.content }}
releaseDraft: true
prerelease: false
includeUpdaterJson: true
updaterJsonPreferNsis: true
args: ${{ matrix.args }}

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
### ✨ Changelog
- Initial MVP release (v0.1.0)
### 📝 Notes
> ⚠️ Linux Users: Make sure yt-dlp is not installed in your distro (otherwise you will get package installation conflict)
> This is an Un-Signed Build (Windows doesn't trust this Certificate so, it may flag this as malicious software, in that case, disable Windows SmartScreen and Defender, install it, and then re-enable them)
> This is an Un-Signed Build (MacOS doesn't trust this Certificate so, it may flag this as from 'unverified developer' and prevent it from opening, in that case, open Settings and allow it from 'Settings > Privacy and Security' section to get started)
### ⬇️ Download Section
| Arch\OS | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | MacOS (dmg) ⬆️ | MacOS (app) ⬆️ |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| x86_64 | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64_en-US.msi) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64-setup.exe) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_amd64.deb) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP-<version>-1.x86_64.rpm) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_x64.dmg) | [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_x64.app.tar.gz) |
| ARM64 | N/A | N/A | N/A | N/A | ⚠️ [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_<version>_aarch64.dmg) | ⚠️ [Download](https://github.com/neosubhamoy/neodlp/releases/download/<release_tag>/NeoDLP_aarch64.app.tar.gz) |
> ⬆️ icon indicates this packaging format supports in-built app-updater
> ⚠️ ARM64 binaries are experimental and may not work properly on Apple Silicon Macs (You might see 'Damaged File' warning) it's because the binaries are not signed and Apple Silicon Macs don't allow unsigned apps (downloaded from internet) to be installed on the system (also I'm not planning to sign it soon as it costs 99$/year, which I can't afford RN!). If you want to use NeoDLP in your Apple Silicon Macs then you have to [compile it from source](https://github.com/neosubhamoy/neodlp?tab=readme-ov-file#%EF%B8%8F-contributing--building-from-source) in your Mac

21
LICENSE Normal file
View 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.

72
README.md Normal file
View File

@@ -0,0 +1,72 @@
# NeoDLP - (Neo Downloader Plus)
Crossplatform Video/Audio Downloader Desktop App with Modern UI and Browser Integration
[![status](https://img.shields.io/badge/status-active-brightgreen.svg?style=flat)](https://github.com/neosubhamoy/neodlp)
[![github tag](https://img.shields.io/github/v/tag/neosubhamoy/neodlp?color=yellow)](https://github.com/neosubhamoy/neodlp)
[![PRs](https://img.shields.io/badge/PRs-welcome-blue.svg?style=flat)](https://github.com/neosubhamoy/neodlp)
> **🥰 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!**
### 💻 Supported Platforms
- Windows (10 / 11)
- Linux (Debian / Fedora / Arch Linux base)
- MacOS (>10.3)
### 🌐 Supported Sites
- All [Supported Sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) by [yt-dlp](https://github.com/yt-dlp/yt-dlp) **(2.5K+)**
### 🧩 External Dependencies
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The core CLI Tool used to download Video/Audio from the Web
- [FFmpeg](https://www.ffmpeg.org) - Used for Video/Audio Post-processing
### ⬇️ Download and Installation
1. Download the latest [NeoDLP](https://github.com/neosubhamoy/neodlp/releases/latest) release based on your OS and CPU Architecture then install it or install it directly from an available distribution channel
| Arch\OS | Windows | Linux | MacOS |
| :---- | :---- | :---- | :---- |
| x86_64 | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) |
| ARM64 | ❌ N/A | ❌ N/A | ✅ [Download](https://github.com/neosubhamoy/neodlp/releases/latest) |
| Platform (OS) | Distribution Channel | Installation Command / Instruction |
| :---- | :---- | :---- |
| Windows x86_64 | WinGet | `winget install neodlp` |
| Linux x86_64 (Arch Linux) | AUR | `yay -S neodlp` |
### ⚡ Technologies Used
![Tauri](https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF)
![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)
![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![ShadCnUi](https://img.shields.io/badge/shadcn%2Fui-000000?style=for-the-badge&logo=shadcnui&logoColor=white)
### 🛠️ 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 Rust, Node.js and Git before proceeding.
* Install Tauri [Prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS / platform
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 process
> ⚠️ Make sure to run the build command once before running the dev command for the first time to avoid build time errors
```code
npm run tauri dev # for development
npm run tauri build # for production build
# use these commands instead if you are using apple silicon macs
npm run tauri dev --config "./src-tauri/tauri.macos-aarch64.conf.json"
npm run tauri build --config "./src-tauri/tauri.macos-aarch64.conf.json"
```
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 is Licensed under the [MIT license](https://github.com/neosubhamoy/neodlp/blob/main/LICENSE). Anyone can view, modify, use (personal and commercial) or distribute it's sources without any attribution and extra permissions.

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NeoDLP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

51
makeFilesExecutable.js Normal file
View File

@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Define array of binary source directories
const binSrcDirs = [
path.join(__dirname, 'src-tauri', 'binaries'),
path.join(__dirname, 'src-tauri', 'resources', 'binaries'),
];
function makeFilesExecutable() {
let totalCount = 0;
let successDirs = 0;
for (const binSrc of binSrcDirs) {
try {
if (!fs.existsSync(binSrc)) {
console.error(`Binaries directory does not exist: ${binSrc}`);
continue;
}
const files = fs.readdirSync(binSrc);
const nonExeFiles = files.filter(file => !file.endsWith('.exe'));
let count = 0;
for (const file of nonExeFiles) {
const filePath = path.join(binSrc, file);
if (fs.statSync(filePath).isFile()) {
execSync(`chmod +x "${filePath}"`);
console.log(`Made executable: ${path.relative(__dirname, filePath)}`);
count++;
}
}
console.log(`Successfully made ${count} files executable in ${binSrc}`);
totalCount += count;
successDirs++;
} catch (error) {
console.error(`Error processing directory ${binSrc}: ${error.message}`);
}
}
console.log(`\nSummary: Made ${totalCount} files executable across ${successDirs} directories`);
}
console.log(`RUNNING: 🛠️ Build Script makeFilesExecutable.js`);
makeFilesExecutable();

5711
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

86
package.json Normal file
View File

@@ -0,0 +1,86 @@
{
"name": "neodlp",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@hookform/resolvers": "^4.0.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.67.2",
"@tanstack/react-query-devtools": "^5.67.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.7.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.2",
"input-otp": "^1.4.2",
"lucide-react": "^0.475.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.1.5",
"recharts": "^2.15.1",
"sonner": "^1.7.4",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@types/node": "^22.13.4",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^6.0.3"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
public/tauri.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

6764
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

48
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,48 @@
[package]
name = "neodlp"
version = "0.1.0"
description = "NeoDLP"
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "neodlp_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "*"
sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
base64 = "0.22"
directories = "5.0"
futures-util = "0.3"
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
tauri-plugin-fs = "2"
tauri-plugin-os = "2"
tauri-plugin-dialog = "2"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
tauri-plugin-process = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
[workspace]
members = [
".",
"msghost"
]

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
size 34391824

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
size 34391824

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:33cfe0a3542db64f7a2a7dfe46eb82716ff4bfad042f95ca530349b87c151d8e
size 18143557

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0aa0afe3d2b32c047b73083f3c8e56081d71bb33fe047357820d51d153d1d54f
size 34608400

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,42 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "default capabilities",
"windows": [
"main"
],
"permissions": [
"core:default",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-focus",
"opener:default",
"shell:default",
"fs:default",
"os:default",
"dialog:default",
"shell:allow-open",
"sql:default",
"sql:allow-execute",
"fs:allow-app-write",
"fs:allow-app-write-recursive",
"updater:default",
"process:default",
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "**"
}
]
},
{
"identifier": "fs:scope",
"allow": [
{
"path": "**"
}
]
}
]
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "shell-scope",
"description": "allowed shell scopes",
"windows": [
"main"
],
"permissions": [
"shell:allow-kill",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/yt-dlp",
"args": true,
"sidecar": true
},
{
"name": "pkexec",
"cmd": "pkexec",
"args": true
}
]
},
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "binaries/yt-dlp",
"args": true,
"sidecar": true
}
]
}
],
"platforms": [
"windows",
"macOS",
"linux"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,15 @@
!macro NSIS_HOOK_POSTINSTALL
; Add Registry Keys for Chrome Native Messaging Host
WriteRegStr HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp" "" "$INSTDIR\neodlp-msghost.json"
; Add Registry Keys for Firefox Native Messaging Host
WriteRegStr HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp" "" "$INSTDIR\neodlp-msghost-moz.json"
; Add entry for automatic startup with Windows
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}" "$\"$INSTDIR\neodlp.exe$\" --hidden"
!macroend
!macro NSIS_HOOK_POSTUNINSTALL
; Remove the Registry entries
DeleteRegKey HKCU "Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp"
DeleteRegKey HKCU "Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp"
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}"
!macroend

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<DirectoryRef Id="TARGETDIR">
<Component Id="NeoDlpRegEntriesFragment" Guid="*">
<RegistryKey Root="HKLM" Key="Software\Google\Chrome\NativeMessagingHosts\com.neosubhamoy.neodlp" Action="createAndRemoveOnUninstall">
<RegistryValue Type="string" Value="[INSTALLDIR]neodlp-msghost.json" KeyPath="no" />
</RegistryKey>
<RegistryKey Root="HKLM" Key="Software\Mozilla\NativeMessagingHosts\com.neosubhamoy.neodlp" Action="createAndRemoveOnUninstall">
<RegistryValue Type="string" Value="[INSTALLDIR]neodlp-msghost-moz.json" KeyPath="no" />
</RegistryKey>
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run">
<RegistryValue Name="NeoDLP" Type="string" Value="&quot;[INSTALLDIR]neodlp.exe&quot; --hidden" KeyPath="no" />
</RegistryKey>
</Component>
</DirectoryRef>
</Fragment>
</Wix>

View File

@@ -0,0 +1,17 @@
[package]
name = "neodlp-msghost"
version = "0.1.0"
description = "NeoDLP Native Messaging Host"
authors = ["neosubhamoy <hey@neosubhamoy.com>"]
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "*"
futures-util = "0.3"
directories = "5.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -0,0 +1,35 @@
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub port: u16,
}
impl Default for Config {
fn default() -> Self {
Self { port: 53511 }
}
}
pub fn get_config_dir() -> Option<PathBuf> {
ProjectDirs::from("com", "neosubhamoy", "neodlp")
.map(|proj_dirs| proj_dirs.config_dir().to_path_buf())
}
pub fn get_config_path() -> Option<PathBuf> {
get_config_dir().map(|dir| dir.join("msghost-config.json"))
}
pub fn load_config() -> Config {
if let Some(config_path) = get_config_path() {
if let Ok(content) = fs::read_to_string(config_path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
Config::default()
}

View File

@@ -0,0 +1,134 @@
mod config;
use config::load_config;
use futures_util::{SinkExt, StreamExt};
use serde_json::Value;
use std::io::{self, Read, Write};
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::sleep;
use tokio_tungstenite::{
connect_async, tungstenite::protocol::Message, MaybeTlsStream, WebSocketStream,
};
fn get_websocket_url() -> String {
let config = load_config();
format!("ws://localhost:{}", config.port)
}
async fn connect_with_retry(
url: &str,
max_attempts: u32,
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>, Box<dyn std::error::Error + Send + Sync>> {
let mut attempts = 0;
loop {
match connect_async(url).await {
Ok((ws_stream, _)) => {
eprintln!("Successfully connected to Tauri app :)");
return Ok(ws_stream);
}
Err(e) => {
attempts += 1;
if attempts >= max_attempts {
return Err(Box::new(e));
}
let wait_time = Duration::from_secs(2u64.pow(attempts));
eprintln!(
"Connection attempt {} failed. Retrying in {:?}...",
attempts, wait_time
);
sleep(wait_time).await;
}
}
}
}
fn read_stdin_message() -> Result<String, Box<dyn std::error::Error>> {
let mut stdin = io::stdin();
let mut length_bytes = [0u8; 4];
stdin.read_exact(&mut length_bytes)?;
let length = u32::from_ne_bytes(length_bytes) as usize;
let mut buffer = vec![0u8; length];
stdin.read_exact(&mut buffer)?;
let message = String::from_utf8(buffer)?;
Ok(message)
}
fn write_stdout_message(message: &str) -> Result<(), Box<dyn std::error::Error>> {
let message_bytes = message.as_bytes();
let message_size = message_bytes.len();
io::stdout().write_all(&(message_size as u32).to_ne_bytes())?;
io::stdout().write_all(message_bytes)?;
io::stdout().flush()?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Waiting for message from extension...");
let input = match read_stdin_message() {
Ok(msg) => {
eprintln!("Received message: {}", msg);
msg
}
Err(e) => {
eprintln!("Error reading message: {:?}", e);
return Err(e);
}
};
// Send immediate response to the extension
write_stdout_message(
&serde_json::json!({
"status": "received",
"message": "Message received by native host"
})
.to_string(),
)?;
let parsed: Value = serde_json::from_str(&input)?;
let websocket_url = get_websocket_url();
eprintln!("Attempting to connect to {}", websocket_url);
let mut ws_stream = match connect_with_retry(&websocket_url, 2).await {
Ok(stream) => stream,
Err(e) => {
eprintln!("Failed to connect after multiple attempts: {:?}", e);
write_stdout_message(
&serde_json::json!({
"status": "error",
"message": "Failed to connect to Tauri app"
})
.to_string(),
)?;
return Err(e);
}
};
// Send message to Tauri app
ws_stream
.send(Message::Text(parsed.to_string().into()))
.await?;
// Receive response from Tauri app
if let Some(Ok(msg)) = ws_stream.next().await {
// Send Tauri app's response back to browser extension
if let Message::Text(text) = msg {
write_stdout_message(
&serde_json::json!({
"status": "success",
"response": text.to_string()
})
.to_string(),
)?;
}
}
// Close the connection
ws_stream.close(None).await?;
Ok(())
}

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=neodlp
Icon=neodlp
Comment=NeoDLP Autostart
Exec=/usr/bin/neodlp --hidden
StartupNotify=false
Terminal=false

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.neosubhamoy.neodlp</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/neodlp.app/Contents/MacOS/neodlp</string>
<string>--hidden</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e607b7f079c4eb0dc666ffca152f225020f8022c8c014dd94d91e6072f57228d
size 79945800

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e607b7f079c4eb0dc666ffca152f225020f8022c8c014dd94d91e6072f57228d
size 79945800

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c49b5913c9a107120c86b401af95df7965003f7fc6dbb4436f1f03c8ba391e8b
size 127473664

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ee15e5145c9eb4775c193ab824c592d4ff3744bb7f283f8db29bd3c3c961589
size 79928672

View File

@@ -0,0 +1,7 @@
{
"name": "com.neosubhamoy.neodlp",
"description": "NeoDLP MsgHost",
"path": "/usr/bin/neodlp-msghost",
"type": "stdio",
"allowed_origins": ["chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
}

View File

@@ -0,0 +1,7 @@
{
"name": "com.neosubhamoy.neodlp",
"description": "NeoDLP MsgHost",
"path": "/usr/bin/neodlp-msghost",
"type": "stdio",
"allowed_extensions": ["neodlp@neosubhamoy.com"]
}

View File

@@ -0,0 +1,7 @@
{
"name": "com.neosubhamoy.neodlp",
"description": "NeoDLP MsgHost",
"path": "/Applications/NeoDLP.app/Contents/Resources/neodlp-msghost",
"type": "stdio",
"allowed_origins": ["chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
}

View File

@@ -0,0 +1,7 @@
{
"name": "com.neosubhamoy.neodlp",
"description": "NeoDLP MsgHost",
"path": "/Applications/NeoDLP.app/Contents/Resources/neodlp-msghost",
"type": "stdio",
"allowed_extensions": ["neodlp@neosubhamoy.com"]
}

View File

@@ -0,0 +1,7 @@
{
"name": "com.neosubhamoy.neodlp",
"description": "NeoDLP MsgHost",
"path": "neodlp-msghost.exe",
"type": "stdio",
"allowed_origins": ["chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
}

View File

@@ -0,0 +1,7 @@
{
"name": "com.neosubhamoy.neodlp",
"description": "NeoDLP MsgHost",
"path": "neodlp-msghost.exe",
"type": "stdio",
"allowed_extensions": ["neodlp@neosubhamoy.com"]
}

51
src-tauri/src/config.rs Normal file
View File

@@ -0,0 +1,51 @@
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub port: u16,
}
impl Default for Config {
fn default() -> Self {
Self { port: 53511 }
}
}
pub fn get_config_dir() -> Option<PathBuf> {
ProjectDirs::from("com", "neosubhamoy", "neodlp")
.map(|proj_dirs| proj_dirs.config_dir().to_path_buf())
}
pub fn get_config_path() -> Option<PathBuf> {
get_config_dir().map(|dir| dir.join("msghost-config.json"))
}
pub fn load_config() -> Config {
if let Some(config_path) = get_config_path() {
if let Ok(content) = fs::read_to_string(config_path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
Config::default()
}
pub fn save_config(config: &Config) -> Result<(), String> {
let config_dir =
get_config_dir().ok_or_else(|| "Could not determine config directory".to_string())?;
fs::create_dir_all(&config_dir)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
let config_path = config_dir.join("msghost-config.json");
let content = serde_json::to_string_pretty(config)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
fs::write(config_path, content).map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(())
}

653
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,653 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
mod config;
mod migrations;
use base64::{engine::general_purpose::STANDARD, Engine};
use config::{get_config_path, load_config, save_config, Config};
use futures_util::{SinkExt, StreamExt};
use reqwest;
use serde_json::Value;
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePool},
Pool, Row, Sqlite,
};
use std::{
collections::{hash_map::DefaultHasher, HashMap},
env, fs,
hash::{Hash, Hasher},
process::Command as StdCommand,
sync::{Arc, Mutex as StdMutex},
time::Duration,
};
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Emitter, Manager, State,
};
use tauri_plugin_opener::OpenerExt;
use tokio::{
net::{TcpListener, TcpStream},
sync::{oneshot, Mutex},
time::sleep,
};
use tokio_tungstenite::accept_async;
struct ImageCache(StdMutex<HashMap<String, String>>);
struct ResponseChannel {
sender: Option<oneshot::Sender<String>>,
}
struct WebSocketState {
sender: Option<
futures_util::stream::SplitSink<
tokio_tungstenite::WebSocketStream<TcpStream>,
tokio_tungstenite::tungstenite::Message,
>,
>,
response_channel: ResponseChannel,
server_abort: Option<tokio::sync::oneshot::Sender<()>>,
config: Config,
}
#[derive(Debug, serde::Serialize)]
struct DownloadState {
id: i32,
download_id: String,
download_status: String,
process_id: Option<i32>,
status: String,
}
async fn is_port_available(port: u16) -> bool {
match TcpListener::bind(format!("127.0.0.1:{}", port)).await {
Ok(_) => true,
Err(_) => false,
}
}
async fn wait_for_port_availability(port: u16, max_attempts: u32) -> Result<(), String> {
let mut attempts = 0;
while attempts < max_attempts {
if is_port_available(port).await {
return Ok(());
}
sleep(Duration::from_millis(500)).await;
attempts += 1;
}
Err(format!(
"Port {} did not become available after {} attempts",
port, max_attempts
))
}
async fn start_websocket_server(app_handle: tauri::AppHandle, port: u16) -> Result<(), String> {
let addr = format!("127.0.0.1:{}", port);
// First ensure any existing server is stopped
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
if let Some(old_abort) = state.server_abort.take() {
let _ = old_abort.send(());
// Wait for the port to become available
wait_for_port_availability(port, 6).await?; // Try for 3 seconds (6 attempts * 500ms)
}
}
// Now try to bind to the port
let listener = match TcpListener::bind(&addr).await {
Ok(l) => l,
Err(_e) => {
// One final attempt to wait and retry
sleep(Duration::from_secs(1)).await;
TcpListener::bind(&addr)
.await
.map_err(|e| format!("Failed to bind to port {}: {}", port, e))?
}
};
let (abort_sender, mut abort_receiver) = tokio::sync::oneshot::channel();
// Store the new abort sender
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.server_abort = Some(abort_sender);
}
// Spawn the server task
tokio::spawn(async move {
println!("Starting WebSocket server on port {}", port);
loop {
tokio::select! {
accept_result = listener.accept() => {
match accept_result {
Ok((stream, _)) => {
let app_handle = app_handle.clone();
tokio::spawn(handle_connection(stream, app_handle));
}
Err(e) => {
println!("Error accepting connection: {}", e);
break;
}
}
}
_ = &mut abort_receiver => {
println!("WebSocket server shutting down on port {}...", port);
break;
}
}
}
});
// Wait a moment to ensure the server has started
sleep(Duration::from_millis(100)).await;
Ok(())
}
#[tauri::command]
async fn restart_websocket_server(
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let port = {
let state = state.lock().await;
state.config.port
};
println!("Restarting WebSocket server on port {}", port);
// Start the server (this will also handle stopping the old one)
start_websocket_server(app_handle, port).await
}
#[tauri::command]
async fn get_config(state: tauri::State<'_, Arc<Mutex<WebSocketState>>>) -> Result<Config, String> {
let state = state.lock().await;
Ok(state.config.clone())
}
#[tauri::command]
fn get_config_file_path() -> Result<String, String> {
match get_config_path() {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => Err("Could not determine config path".to_string()),
}
}
#[tauri::command]
async fn update_config(
new_config: Config,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
app_handle: tauri::AppHandle,
) -> Result<Config, String> {
// Save the new config first
save_config(&new_config)?;
// Update the state with new config
{
let mut state = state.lock().await;
state.config = new_config.clone();
}
// Start the new server (this will also handle stopping the old one)
start_websocket_server(app_handle, new_config.port).await?;
Ok(new_config)
}
#[tauri::command]
async fn reset_config(
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
app_handle: tauri::AppHandle,
) -> Result<Config, String> {
let config = Config::default();
save_config(&config)?;
{
let mut state = state.lock().await;
state.config = config.clone();
}
start_websocket_server(app_handle, config.port).await?;
Ok(config)
}
#[tauri::command]
async fn send_to_extension(
message: String,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
) -> Result<(), String> {
let mut state = state.lock().await;
if let Some(sender) = &mut state.sender {
sender
.send(tokio_tungstenite::tungstenite::Message::Text(
message.into(),
))
.await
.map_err(|e| format!("Failed to send message: {}", e))?;
Ok(())
} else {
Err("No active WebSocket connection".to_string())
}
}
#[tauri::command]
async fn receive_frontend_response(
response: String,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
) -> Result<(), String> {
let mut state = state.lock().await;
if let Some(sender) = state.response_channel.sender.take() {
sender
.send(response)
.map_err(|e| format!("Failed to send response: {:?}", e))?;
}
Ok(())
}
#[tauri::command]
async fn kill_all_process(pid: i32) -> Result<(), String> {
#[cfg(unix)]
{
println!("Sending INT signal to process with PID: {}", pid);
let mut kill = StdCommand::new("kill")
.args(["-s", "SIGINT", &pid.to_string()])
.spawn()
.map_err(|e| e.to_string())?;
kill.wait().map_err(|e| e.to_string())?;
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
println!("Sending taskkill to process with PID: {}", pid);
let mut kill = StdCommand::new("taskkill")
.args(["/PID", &pid.to_string(), "/F", "/T"]) // /T flag kills the process tree
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| e.to_string())?;
kill.wait().map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
async fn fetch_image(
app_handle: tauri::AppHandle,
cache: State<'_, ImageCache>,
url: String,
) -> Result<String, String> {
// Check if image is already cached (acquire and release lock quickly)
let cached_path = {
let cache_map = cache.0.lock().unwrap();
cache_map.get(&url).cloned()
};
if let Some(local_path) = cached_path {
return Ok(local_path);
}
// Download image (no lock held during network operations)
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
// Generate path for caching
let app_dir = app_handle
.path()
.app_cache_dir()
.map_err(|_| "Failed to get cache dir".to_string())?
.join("thumbnails");
fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
// Create filename from URL hash
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
let hash = hasher.finish();
let file_name = format!("thumb_{}.jpg", hash);
let file_path = app_dir.join(&file_name);
fs::write(&file_path, &bytes).map_err(|e| e.to_string())?;
// Instead of file://, use a data URI
let image_data = STANDARD.encode(&bytes);
let local_path = format!("data:image/jpeg;base64,{}", image_data);
// Cache the URL to path mapping (acquire lock again briefly)
{
let mut cache_map = cache.0.lock().unwrap();
cache_map.insert(url, local_path.clone());
}
Ok(local_path)
}
#[tauri::command]
async fn open_file_with_app(
app_handle: tauri::AppHandle,
file_path: String,
app_name: Option<String>,
) -> Result<(), String> {
if let Some(name) = &app_name {
if name == "explorer" {
println!("Revealing file: {} in explorer", file_path);
return app_handle
.opener()
.reveal_item_in_dir(file_path)
.map_err(|e| e.to_string());
}
println!("Opening file: {} with app: {}", file_path, name);
} else {
println!("Opening file: {} with default app", file_path);
}
app_handle
.opener()
.open_path(file_path, app_name)
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn list_ongoing_downloads(
state_mutex: State<'_, StdMutex<Pool<Sqlite>>>,
) -> Result<Vec<DownloadState>, String> {
let pool_clone = {
let pool = state_mutex.lock().map_err(|e| e.to_string())?;
pool.clone()
};
let qry = "SELECT * FROM downloads WHERE download_status = 'downloading' OR download_status = 'starting' OR download_status = 'queued'";
match sqlx::query(qry).fetch_all(&pool_clone).await {
Ok(rows) => {
let mut downloads = Vec::new();
for row in rows {
downloads.push(DownloadState {
id: row.get("id"),
download_id: row.get("download_id"),
download_status: row.get("download_status"),
process_id: row.get("process_id"),
status: row.get("status"),
});
}
Ok(downloads)
}
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
async fn pause_ongoing_downloads(
state_mutex: State<'_, StdMutex<Pool<Sqlite>>>,
) -> Result<(), String> {
// Get database connection
let pool_clone = {
let pool = state_mutex.lock().map_err(|e| e.to_string())?;
pool.clone()
};
// Fetch all ongoing downloads
let qry = "SELECT * FROM downloads WHERE download_status = 'downloading' OR download_status = 'starting' OR download_status = 'queued'";
let downloads = match sqlx::query(qry).fetch_all(&pool_clone).await {
Ok(rows) => {
let mut downloads = Vec::new();
for row in rows {
downloads.push(DownloadState {
id: row.get("id"),
download_id: row.get("download_id"),
download_status: row.get("download_status"),
process_id: row.get("process_id"),
status: row.get("status"),
});
}
downloads
}
Err(e) => return Err(e.to_string()),
};
println!("Found {} ongoing downloads to pause", downloads.len());
// Process each download
for download in downloads {
println!(
"Pausing download: {} ({}), Status: {}",
download.download_id, download.id, download.download_status
);
// Kill the process if it exists
if let Some(pid) = download.process_id {
println!("Terminating process with PID: {}", pid);
if let Err(e) = kill_all_process(pid).await {
println!("Failed to kill process {}: {}", pid, e);
} else {
println!("Successfully terminated process {}", pid);
}
}
// Update the download status in the database
let update_qry = "UPDATE downloads SET download_status = 'paused' WHERE id = ?";
if let Err(e) = sqlx::query(update_qry)
.bind(download.id)
.execute(&pool_clone)
.await
{
println!(
"Failed to update download status for ID {}: {}",
download.id, e
);
} else {
println!("Updated download status to 'paused' for ID {}", download.id);
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run() {
let migrations = migrations::get_migrations();
let config = load_config();
let port = config.port;
let websocket_state = Arc::new(Mutex::new(WebSocketState {
sender: None,
response_channel: ResponseChannel { sender: None },
server_abort: None,
config,
}));
let args: Vec<String> = env::args().collect();
let start_hidden = args.contains(&"--hidden".to_string());
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// Focus the main window when attempting to launch another instance
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}))
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:database.db", migrations)
.build(),
)
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.manage(ImageCache(StdMutex::new(HashMap::new())))
.manage(websocket_state.clone())
.setup(move |app| {
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let _ = fs::create_dir_all(app_handle.path().app_data_dir().unwrap());
let db_path = app_handle
.path()
.app_data_dir()
.unwrap()
.join("database.db");
let options = SqliteConnectOptions::new()
.filename(db_path)
.create_if_missing(true);
let pool = SqlitePool::connect_with(options).await;
match pool {
Ok(db) => {
app_handle.manage(StdMutex::new(db.clone()));
}
Err(e) => {
eprintln!("Database connection error: {}", e);
}
}
});
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)
.map_err(|e| format!("Failed to create quit menu item: {}", e))?;
let show = MenuItem::with_id(app, "show", "Show NeoDLP", true, None::<&str>)
.map_err(|e| format!("Failed to create show menu item: {}", e))?;
let menu = Menu::with_items(app, &[&show, &quit])
.map_err(|e| format!("Failed to create menu: {}", e))?;
let tray = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.tooltip("NeoDLP")
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
let state_mutex = app_handle.state::<StdMutex<Pool<Sqlite>>>();
if let Err(e) = pause_ongoing_downloads(state_mutex).await {
println!("Error pausing downloads: {}", e);
}
app_handle.exit(0);
});
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)
.map_err(|e| format!("Failed to create tray: {}", e))?;
app.manage(tray);
let window = app.get_webview_window("main").unwrap();
if !start_hidden {
window.show().unwrap();
}
let websocket_app_handle = app.handle().clone();
tokio::spawn(async move {
if let Err(e) = start_websocket_server(websocket_app_handle, port).await {
println!("Failed to start initial WebSocket server: {}", e);
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
kill_all_process,
fetch_image,
open_file_with_app,
list_ongoing_downloads,
pause_ongoing_downloads,
send_to_extension,
receive_frontend_response,
get_config,
update_config,
reset_config,
get_config_file_path,
restart_websocket_server,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle) {
let ws_stream = accept_async(stream).await.unwrap();
let (ws_sender, mut ws_receiver) = ws_stream.split();
// Store the sender in the shared state
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = Some(ws_sender);
}
println!("New WebSocket connection established");
while let Some(msg) = ws_receiver.next().await {
if let Ok(msg) = msg {
if let Ok(text) = msg.to_text() {
println!("Received message: {}", text);
// Parse the JSON message
if let Ok(json_value) = serde_json::from_str::<Value>(text) {
// Create a new channel for this request
let (response_sender, response_receiver) = oneshot::channel();
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.response_channel.sender = Some(response_sender);
}
// Emit an event to the frontend
app_handle
.emit_to("main", "websocket-message", json_value)
.unwrap();
// Wait for the response from the frontend
let response = response_receiver
.await
.unwrap_or_else(|e| format!("Error receiving response: {:?}", e));
// Send the response back through WebSocket
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
if let Some(sender) = &mut state.sender {
let _ = sender
.send(tokio_tungstenite::tungstenite::Message::Text(
response.into(),
))
.await;
}
}
}
}
}
println!("WebSocket connection closed");
// Remove the sender from the shared state when the connection closes
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = None;
}

7
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,7 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[tokio::main]
async fn main() {
neodlp_lib::run().await
}

View File

@@ -0,0 +1,72 @@
use tauri_plugin_sql::{Migration, MigrationKind};
pub fn get_migrations() -> Vec<Migration> {
vec![Migration {
version: 1,
description: "create_initial_tables",
sql: "
CREATE TABLE IF NOT EXISTS video_info (
id INTEGER PRIMARY KEY NOT NULL,
video_id TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
url TEXT NOT NULL,
host TEXT NOT NULL,
thumbnail TEXT,
channel TEXT,
duration_string TEXT,
release_date TEXT,
view_count INTEGER,
like_count INTEGER
);
CREATE TABLE IF NOT EXISTS playlist_info (
id INTEGER PRIMARY KEY NOT NULL,
playlist_id TEXT UNIQUE NOT NULL,
playlist_title TEXT NOT NULL,
playlist_url TEXT NOT NULL,
playlist_n_entries INTEGER NOT NULL,
playlist_channel TEXT
);
CREATE TABLE IF NOT EXISTS downloads (
id INTEGER PRIMARY KEY NOT NULL,
download_id TEXT UNIQUE NOT NULL,
download_status TEXT NOT NULL,
video_id TEXT NOT NULL,
format_id TEXT NOT NULL,
subtitle_id TEXT,
queue_index INTEGER,
playlist_id TEXT,
playlist_index INTEGER,
resolution TEXT,
ext TEXT,
abr REAL,
vbr REAL,
acodec TEXT,
vcodec TEXT,
dynamic_range TEXT,
process_id INTEGER,
status TEXT,
progress REAL,
total INTEGER,
downloaded INTEGER,
speed REAL,
eta INTEGER,
filepath TEXT,
filetype TEXT,
filesize INTEGER,
FOREIGN KEY (video_id) REFERENCES video_info (video_id),
FOREIGN KEY (playlist_id) REFERENCES playlist_info (playlist_id)
);
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY NOT NULL,
key TEXT UNIQUE NOT NULL,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS kv_store (
id INTEGER PRIMARY KEY NOT NULL,
key TEXT UNIQUE NOT NULL,
value TEXT NOT NULL
);
",
kind: MigrationKind::Up,
}]
}

49
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,49 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "NeoDLP",
"mainBinaryName": "neodlp",
"version": "0.1.0",
"identifier": "com.neosubhamoy.neodlp",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "NeoDLP",
"width": 1067,
"height": 600,
"visible": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDM0I4ODcyODdGOTM4MDIKUldRQ09QbUhjb2c3UENGY1lFUVdTVWhucmJ4QzdGeW9sU3VHVFlGNWY5anZab2s4SU1rMWFsekMK",
"endpoints": [
"https://github.com/neosubhamoy/neodlp/releases/latest/download/latest.json"
],
"windows": {
"installMode": "passive"
}
}
}
}

View File

@@ -0,0 +1,67 @@
{
"identifier": "com.neosubhamoy.neodlp",
"build": {
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev",
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run build",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "NeoDLP",
"width": 1067,
"height": 600,
"visible": false
}
],
"security": {
"csp": null,
"capabilities": [
"default",
"shell-scope"
]
}
},
"bundle": {
"active": true,
"targets": ["deb", "rpm"],
"createUpdaterArtifacts": true,
"licenseFile": "../LICENSE",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"binaries/yt-dlp"
],
"resources": {
"resources/binaries/ffmpeg-x86_64-unknown-linux-gnu": "binaries/ffmpeg-x86_64"
},
"linux": {
"deb": {
"files": {
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
"/etc/chromium/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
"/usr/lib/mozilla/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/firefox.json",
"/usr/bin/neodlp-msghost": "./target/release/neodlp-msghost",
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
}
},
"rpm": {
"epoch": 0,
"release": "1",
"files": {
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
"/etc/chromium/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/chrome.json",
"/usr/lib/mozilla/native-messaging-hosts/com.neosubhamoy.neodlp.json": "./resources/msghost-manifest/linux/firefox.json",
"/usr/bin/neodlp-msghost": "./target/release/neodlp-msghost",
"/etc/xdg/autostart/neodlp-autostart.desktop": "./resources/autostart/linux/autostart.desktop"
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
{
"identifier": "com.neosubhamoy.neodlp",
"build": {
"beforeDevCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run dev",
"beforeBuildCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --release --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node makeFilesExecutable.js && npm run build",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "NeoDLP",
"width": 1067,
"height": 600,
"visible": false
}
],
"security": {
"csp": null,
"capabilities": [
"default",
"shell-scope"
]
}
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"createUpdaterArtifacts": true,
"licenseFile": "../LICENSE",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"binaries/yt-dlp"
],
"resources": {
"target/aarch64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
"resources/binaries/ffmpeg-aarch64-apple-darwin": "binaries/ffmpeg-aarch64",
"resources/msghost-manifest/macos/chrome.json": "neodlp-msghost.json",
"resources/msghost-manifest/macos/firefox.json": "neodlp-msghost-moz.json",
"resources/autostart/macos/autostart.plist": "neodlp-autostart.plist"
},
"macOS": {
"providerShortName": "neosubhamoy"
}
}
}

Some files were not shown because too many files have changed in this diff Show More