(chore): initial MVP release v0.1.0
7
src-tauri/.gitignore
vendored
Normal 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
48
src-tauri/Cargo.toml
Normal 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"
|
||||
]
|
||||
3
src-tauri/binaries/yt-dlp-aarch64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
|
||||
size 34391824
|
||||
3
src-tauri/binaries/yt-dlp-x86_64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc5bd50ef656f1727d6f1c6c55688b21434e70cb5fb3e439701d1061ae094bf0
|
||||
size 34391824
|
||||
3
src-tauri/binaries/yt-dlp-x86_64-pc-windows-msvc.exe
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:33cfe0a3542db64f7a2a7dfe46eb82716ff4bfad042f95ca530349b87c151d8e
|
||||
size 18143557
|
||||
3
src-tauri/binaries/yt-dlp-x86_64-unknown-linux-gnu
Normal 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
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
42
src-tauri/capabilities/default.json
Normal 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": "**"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
41
src-tauri/capabilities/shell.json
Normal 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
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 124 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 810 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 552 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
15
src-tauri/installer/windows/nsis/hooks.nsi
Normal 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
|
||||
18
src-tauri/installer/windows/wix/reg-fragment.wxs
Normal 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=""[INSTALLDIR]neodlp.exe" --hidden" KeyPath="no" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
17
src-tauri/msghost/Cargo.toml
Normal 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"
|
||||
35
src-tauri/msghost/src/config.rs
Normal 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()
|
||||
}
|
||||
134
src-tauri/msghost/src/main.rs
Normal 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(())
|
||||
}
|
||||
8
src-tauri/resources/autostart/linux/autostart.desktop
Normal 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
|
||||
15
src-tauri/resources/autostart/macos/autostart.plist
Normal 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>
|
||||
3
src-tauri/resources/binaries/ffmpeg-aarch64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e607b7f079c4eb0dc666ffca152f225020f8022c8c014dd94d91e6072f57228d
|
||||
size 79945800
|
||||
3
src-tauri/resources/binaries/ffmpeg-x86_64-apple-darwin
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e607b7f079c4eb0dc666ffca152f225020f8022c8c014dd94d91e6072f57228d
|
||||
size 79945800
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c49b5913c9a107120c86b401af95df7965003f7fc6dbb4436f1f03c8ba391e8b
|
||||
size 127473664
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3ee15e5145c9eb4775c193ab824c592d4ff3744bb7f283f8db29bd3c3c961589
|
||||
size 79928672
|
||||
7
src-tauri/resources/msghost-manifest/linux/chrome.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "com.neosubhamoy.neodlp",
|
||||
"description": "NeoDLP MsgHost",
|
||||
"path": "/usr/bin/neodlp-msghost",
|
||||
"type": "stdio",
|
||||
"allowed_origins": ["chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
||||
}
|
||||
7
src-tauri/resources/msghost-manifest/linux/firefox.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "com.neosubhamoy.neodlp",
|
||||
"description": "NeoDLP MsgHost",
|
||||
"path": "/usr/bin/neodlp-msghost",
|
||||
"type": "stdio",
|
||||
"allowed_extensions": ["neodlp@neosubhamoy.com"]
|
||||
}
|
||||
7
src-tauri/resources/msghost-manifest/macos/chrome.json
Normal 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/"]
|
||||
}
|
||||
7
src-tauri/resources/msghost-manifest/macos/firefox.json
Normal 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"]
|
||||
}
|
||||
7
src-tauri/resources/msghost-manifest/windows/chrome.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "com.neosubhamoy.neodlp",
|
||||
"description": "NeoDLP MsgHost",
|
||||
"path": "neodlp-msghost.exe",
|
||||
"type": "stdio",
|
||||
"allowed_origins": ["chrome-extension://agkddibgemhefmdhlnooiakfnhihhbdb/"]
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
72
src-tauri/src/migrations.rs
Normal 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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src-tauri/tauri.linux.conf.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src-tauri/tauri.macos-aarch64.conf.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src-tauri/tauri.macos.conf.json
Normal 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/x86_64-apple-darwin/release/neodlp-msghost": "neodlp-msghost",
|
||||
"resources/binaries/ffmpeg-x86_64-apple-darwin": "binaries/ffmpeg-x86_64",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src-tauri/tauri.windows.conf.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"identifier": "com.neosubhamoy.neodlp",
|
||||
"build": {
|
||||
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
|
||||
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && 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": ["msi", "nsis"],
|
||||
"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-pc-windows-msvc.exe": "binaries/ffmpeg-x86_64.exe",
|
||||
"target/release/neodlp-msghost.exe": "neodlp-msghost.exe",
|
||||
"resources/msghost-manifest/windows/chrome.json": "neodlp-msghost.json",
|
||||
"resources/msghost-manifest/windows/firefox.json": "neodlp-msghost-moz.json"
|
||||
},
|
||||
"windows": {
|
||||
"wix": {
|
||||
"fragmentPaths": ["installer/windows/wix/reg-fragment.wxs"],
|
||||
"componentRefs": ["NeoDlpRegEntriesFragment"]
|
||||
},
|
||||
"nsis": {
|
||||
"installerHooks": "installer/windows/nsis/hooks.nsi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||