1
1
mirror of https://github.com/neosubhamoy/pytubepp-helper.git synced 2026-02-04 19:32:21 +05:30

10 Commits

40 changed files with 2163 additions and 386 deletions

23
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
on:
release:
types: [released]
name: 🚀 Publish to WinGet
jobs:
publish:
runs-on: windows-latest
steps:
- name: 🛠️ Get release version
id: get-version
run: |
$VERSION="${{ github.event.release.tag_name }}" -replace '^v|[^0-9.]'
"version=$VERSION" >> $env:GITHUB_OUTPUT
shell: pwsh
- name: 🚀 Send PR to winget-pkgs repo
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: neosubhamoy.pytubepp-helper
version: ${{ steps.get-version.outputs.version }}
installers-regex: '\.exe$'
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -1,14 +1,9 @@
### ✨ Changelog
- Migrated to Tauri v2
- Added support for caption download
- Added new Settings page
- Added new Notifications page (will be used for app, component updates and broadcast messages)
- Added dynamic websocket server port configuration option in settings
- Added Node.js detection and installation option
- Changed RPM ffmpeg-free dependency to ffmpeg (as full ffmpeg is required for embedding captions)
- Improved single instance functionality by switching to tauri_plugin_single_instance from websocket based single instance detection
- Enabled app-updater (for supported packaging formats in Windows - exe, msi, MacOS - app)
- Added support for Arch Linux
- Added Extension Manager (to manage unpacked pytubepp-extension - unpacking, updating)
- Added app theme preference option in settings
- Added update notification preference toggle in settings
- Minor fixes and improvements
### 📎 Minimum Requirements
@@ -18,9 +13,9 @@
### 📝 Notes
> IMPORTANT: Linux (Fedora) users must need to [enable](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) RPM Fusion free+nonfree repos before installing this update (to avoid 'ffmpeg not found' error while installing the RPM package)
> ⭐ **IMPORTANT:** Linux (Fedora) users must need to [enable](https://docs.fedoraproject.org/en-US/quick-docs/rpmfusion-setup/#_enabling_the_rpm_fusion_repositories_using_command_line_utilities) RPM Fusion free+nonfree repos before installing this update (to avoid 'ffmpeg not found' error while installing the RPM package)
> IMPORTANT: MacOS users must re-click the 'Register' button after updating
> ⭐ **IMPORTANT:** MacOS users must re-click the 'register to mac' icon after updating
> 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)
@@ -30,7 +25,9 @@
| Arch\OS | Windows (msi) ⬆️ | Windows (exe) ⬆️ | Linux (deb) | Linux (rpm) | MacOS (dmg) | MacOS (app) ⬆️ |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| x86_64 | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_0.7.0_x64_en-US.msi) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_0.7.0_x64-setup.exe) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_0.7.0_amd64.deb) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper-0.7.0-1.x86_64.rpm) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_0.7.0_x64.dmg) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_x64.app.tar.gz) |
| ARM64 | N/A | N/A | N/A | N/A | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_0.7.0_aarch64.dmg) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.7.0-beta/pytubepp-helper_aarch64.app.tar.gz) |
| x86_64 | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_0.8.0_x64_en-US.msi) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_0.8.0_x64-setup.exe) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_0.8.0_amd64.deb) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper-0.8.0-1.x86_64.rpm) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_0.8.0_x64.dmg) | [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_x64.app.tar.gz) |
| ARM64 | N/A | N/A | N/A | N/A | ⚠️ [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_0.8.0_aarch64.dmg) | ⚠️ [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/download/v0.8.0-beta/pytubepp-helper_aarch64.app.tar.gz) |
> ⬆️ icon indicates this packaging format supports in-built app-updater
> ⬆️ 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 pytubepp-helper in your Apple Silicon Macs then you have to [compile it from source](https://github.com/neosubhamoy/pytubepp-helper?tab=readme-ov-file#%EF%B8%8F-contributing--building-from-source) in your Mac

View File

@@ -12,7 +12,7 @@ A Helper App for PytubePP Extension/Addon to Communicate with Pytube Post Proces
### 💻 Supported Platforms
- Windows 10 (v1803 or later) / 11
- Linux (Debian/Ubuntu, Fedora/RHEL base)
- Linux (Debian / Fedora / Arch Linux base)
- MacOS (v10.13 or later)
### 📎 Pre-Requirements
@@ -25,13 +25,18 @@ A Helper App for PytubePP Extension/Addon to Communicate with Pytube Post Proces
### ⬇️ Download and Installation
1. Download the latest [PytubePP Helper](https://github.com/neosubhamoy/pytubepp-helper/releases/latest) release based on your OS and CPU Architecture
1. Download the latest [PytubePP Helper](https://github.com/neosubhamoy/pytubepp-helper/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/pytubepp-helper/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/latest) | ✅ [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/latest) |
| ARM64 | ❌ N/A | ❌ N/A | ✅ [Download](https://github.com/neosubhamoy/pytubepp-helper/releases/latest) |
| Platform | Distribution Channel | Installation Command / Instruction |
| :---- | :---- | :---- |
| Windows x86_64 | WinGet | `winget install pytubepp-helper` |
| Linux x86_64 (Arch) | AUR | `yay -S pytubepp-helper` |
2. Install all [pre-requirements](https://github.com/neosubhamoy/pytubepp-helper#-pre-requirements) and [PytubePP Extension](https://github.com/neosubhamoy/pytubepp-extension) (follow next instructions based on your OS)
> NOTE: You can install the pre-requirements from PytubePP Helper app GUI or manually running the commands in your system's terminal / command prompt, for manual installation follow this [guide](https://github.com/neosubhamoy/pytubepp#%EF%B8%8F-installation).
@@ -52,7 +57,7 @@ A Helper App for PytubePP Extension/Addon to Communicate with Pytube Post Proces
* **🐧 LINUX:**
> ⚠️ NOTE: Most of the Debian / RHEL based distros are supported. Tested on: debian (v12), ubuntu (v24.04 LTS), fedora (v41). If your distro is not in the tested list it doesn't mean that 'the app will not run at all', so, test it yourself and if it doesn't work then you can request us to add support for your distro via creating a github issue.
> ⚠️ NOTE: Most of the Debian / Fedora / Arch based distros are supported. Tested on: debian (v12), ubuntu (v24.04 LTS), fedora (v41), arch linux (latest rolling). If your distro is not in the tested list it doesn't mean that 'the app will not run at all', so, test it yourself and if it doesn't work then you can request us to add support for your distro via creating a github issue.
> ⚠️ Sandboxed Browsers may not work properly (eg: Flatpak, Snaps) (have issue with: Browser NativeMessaging API [read here](https://github.com/flatpak/xdg-desktop-portal/issues/655)) (But, still try it yourself to see if it works)
@@ -77,7 +82,7 @@ A Helper App for PytubePP Extension/Addon to Communicate with Pytube Post Proces
3. Now, open PytubePP Helper app and click on the (blue) install button on the right side of 'FFmpeg' to install it. Also, install 'Node.js' and 'PytubePP' following the same step.
4. Then, click on the 'Register' botton on the top right corner to register 'PytubePP Helper' in your system and also add it to your system's autostart entry. If you see a MacOS notification saying 'pytubepp-helper' is added as a startup app then it's done.
4. Then, click on the 'register to mac' icon on the top right corner to register 'PytubePP Helper' in your system and also add it to your system's autostart entry. If you see a MacOS notification saying 'pytubepp-helper' is added as a startup app then it's perfectly done.
5. Now click on the 'Refresh' button and you will see the 'Ready' message. Then close PytubePP Helper.
@@ -91,17 +96,17 @@ A Helper App for PytubePP Extension/Addon to Communicate with Pytube Post Proces
### 🤔 How It Works
- PytubePP Helper is an intermediate communicator between PytubePP Extension and Pytube Post Processor CLI interface. It is used as a bridge to estblish communication between the System Shell / CMD and Browser Extension, as a Browser Extension can not directly talk (execute commands) with System Shell / CMD for security reasons. Browser Extensions are isolated from the system too, the only way they can communicate with the system (native apps only) is nativeMessaging API provided by Chrome (other Browsers provides it too). So, PytubePP Helper uses that API to communicate with the Browser Extension and recives it's requests and processes the data from PytubePP CLI then genrates a response and sends it to the Browser Extension. For further understanding view the system design diagram of PytubePP Helper app below:
- PytubePP Helper is an intermediate communicator between PytubePP Extension and Pytube Post Processor CLI interface. It is used as a bridge to estblish communication between the System Shell / CMD and Browser Extension(as Browser Extensions can't directly talk, execute commands with System Shell / CMD for security reasons they are isolated from the system) The only way a browser extension can communicate with the system (native app) is Browser nativeMessaging API (Available on Chrome, Firefox etc.). So, PytubePP Helper uses that API to communicate with the PytubePP Browser Extension (PytubePP Helper recives PytubePP Extension's requests then processes the request using PytubePP CLI and returns back the response to the Extension) For further understanding view the system design diagram of PytubePP Helper app below:
![PytubePPHelperDiagram](./assets/images/pytubepp-helper-diagram.png)
### ⚡ 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)
![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)
### 🛠️ Contributing / Building from Source
@@ -117,6 +122,7 @@ Want to be part of this? Feel free to contribute...!! Pull Requests are always w
npm install
```
4. Run development / build process
> Make sure to run the build command once before running the dev command to avoid errors
```code
npm run tauri dev
```

36
makeFilesExecutable.js Normal file
View File

@@ -0,0 +1,36 @@
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);
const binSrc = path.join(__dirname, 'src-tauri', 'binaries');
function makeFilesExecutable() {
try {
if (!fs.existsSync(binSrc)) {
console.error(`Binaries directory does not exist: ${binSrc}`);
return;
}
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: ${file}`);
count++;
}
}
console.log(`Successfully made ${count} files executable in ${binSrc}`);
} catch (error) {
console.error(`Error making files executable: ${error.message}`);
}
}
makeFilesExecutable();

931
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "pytubepp-helper",
"private": true,
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,27 +11,35 @@
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.7",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.5.0",
"@tauri-apps/plugin-upload": "^2.2.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"lucide-react": "^0.436.0",
"next-themes": "^0.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.54.2",
"react-router-dom": "^7.1.3",
"sonner": "^2.0.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"

450
src-tauri/Cargo.lock generated
View File

@@ -661,10 +661,29 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding 2.3.1",
"time 0.3.36",
"version_check 0.9.5",
]
[[package]]
name = "cookie_store"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
dependencies = [
"cookie",
"document-features",
"idna 1.0.3",
"log 0.4.22",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time 0.3.36",
"url 2.5.2",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -847,6 +866,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "data-url"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "deranged"
version = "0.3.11"
@@ -1012,6 +1037,15 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "document-features"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
dependencies = [
"litrs",
]
[[package]]
name = "dpi"
version = "0.1.1"
@@ -1309,6 +1343,21 @@ version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@@ -1316,6 +1365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -1383,6 +1433,7 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -1709,6 +1760,25 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "h2"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
dependencies = [
"atomic-waker",
"bytes 1.7.1",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.4.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1842,6 +1912,7 @@ dependencies = [
"bytes 1.7.1",
"futures-channel",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
@@ -1932,6 +2003,124 @@ dependencies = [
"objc2",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec 1.13.2",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -1959,6 +2148,27 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec 1.13.2",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -2215,6 +2425,18 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "lock_api"
version = "0.3.4"
@@ -3332,9 +3554,25 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
dependencies = [
"idna 1.0.3",
"psl-types",
]
[[package]]
name = "pytubepp-helper"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"directories",
"fix-path-env",
@@ -3344,12 +3582,14 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-fs",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-upload",
"tokio",
"tokio-tungstenite",
]
@@ -3645,6 +3885,17 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "read-progress-stream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6435842fc2fea44b528719eb8c32203bbc1bb2f5b619fbe0c0a3d8350fd8d2a8"
dependencies = [
"bytes 1.7.1",
"futures 0.3.30",
"pin-project-lite",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
@@ -3719,8 +3970,12 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"base64 0.22.1",
"bytes 1.7.1",
"cookie",
"cookie_store",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
@@ -3742,6 +3997,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -4389,6 +4645,17 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
@@ -4398,6 +4665,27 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.8.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -4630,6 +4918,28 @@ dependencies = [
"uuid",
]
[[package]]
name = "tauri-plugin-http"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8137a106e0741fdd357366178fc6e0597abe7d20796f53f44171a1bcec1683"
dependencies = [
"data-url",
"http",
"regex",
"reqwest",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.11",
"tokio",
"url 2.5.2",
"urlpattern",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.2.1"
@@ -4744,6 +5054,25 @@ dependencies = [
"zip",
]
[[package]]
name = "tauri-plugin-upload"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90ac6d3a783d4406caeae8c75aa05e96346474765517fddfd1dc313ff91aa89"
dependencies = [
"futures-util",
"log 0.4.22",
"read-progress-stream",
"reqwest",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.11",
"tokio",
"tokio-util",
]
[[package]]
name = "tauri-runtime"
version = "2.3.0"
@@ -4959,6 +5288,16 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
@@ -4999,7 +5338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b"
dependencies = [
"bytes 0.4.12",
"futures",
"futures 0.1.31",
"tokio-io",
]
@@ -5010,7 +5349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671"
dependencies = [
"crossbeam-utils 0.7.2",
"futures",
"futures 0.1.31",
]
[[package]]
@@ -5020,7 +5359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674"
dependencies = [
"bytes 0.4.12",
"futures",
"futures 0.1.31",
"log 0.4.22",
]
@@ -5042,7 +5381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351"
dependencies = [
"crossbeam-utils 0.7.2",
"futures",
"futures 0.1.31",
"lazy_static",
"log 0.4.22",
"mio 0.6.23",
@@ -5071,7 +5410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee"
dependencies = [
"fnv",
"futures",
"futures 0.1.31",
]
[[package]]
@@ -5081,7 +5420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72"
dependencies = [
"bytes 0.4.12",
"futures",
"futures 0.1.31",
"iovec",
"mio 0.6.23",
"tokio-io",
@@ -5094,7 +5433,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "354b8cd83825b3c20217a9dc174d6a0c67441a2fae5c41bcb1ea6679f6ae0f7c"
dependencies = [
"futures",
"futures 0.1.31",
"native-tls",
"tokio-io",
]
@@ -5456,6 +5795,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -5733,7 +6084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "319bacd7682c7dfe1444e7cb1aed23bf5b1d837d722925f531e1665bd21a4603"
dependencies = [
"bytes 0.4.12",
"futures",
"futures 0.1.31",
"hyper 0.10.16",
"native-tls",
"rand 0.6.5",
@@ -5757,7 +6108,7 @@ dependencies = [
"bitflags 1.3.2",
"byteorder",
"bytes 0.4.12",
"futures",
"futures 0.1.31",
"native-tls",
"rand 0.6.5",
"sha-1",
@@ -6257,6 +6608,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.48.1"
@@ -6352,6 +6715,30 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zbus"
version = "5.5.0"
@@ -6436,12 +6823,55 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zip"
version = "2.2.2"

View File

@@ -1,6 +1,6 @@
[package]
name = "pytubepp-helper"
version = "0.7.0"
version = "0.8.0"
description = "PytubePP Helper"
authors = ["neosubhamoy"]
edition = "2021"
@@ -25,6 +25,8 @@ tauri-plugin-fs = "2"
tauri-plugin-os = "2"
tauri-plugin-process = "2"
tauri-plugin-notification = "2"
tauri-plugin-http = "2"
tauri-plugin-upload = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -14,8 +14,15 @@
"process:default",
"notification:default",
"updater:default",
"upload:default",
"core:window:allow-hide",
"fs:allow-app-write",
"fs:allow-app-write-recursive"
"fs:allow-app-write-recursive",
{
"identifier": "http:default",
"allow": [
{ "url": "https://github.com" }
]
}
]
}

View File

@@ -49,6 +49,13 @@
"--version"
]
},
{
"name": "is-pacman-installed",
"cmd": "pacman",
"args": [
"--version"
]
},
{
"name": "is-python3-installed",
"cmd": "python3",
@@ -121,6 +128,21 @@
},
"--raw-info"
]
},
{
"name": "binaries/sevenzip",
"args": true,
"sidecar": true
}
]
},
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "binaries/sevenzip",
"args": true,
"sidecar": true
}
]
}

View File

@@ -6,11 +6,17 @@ use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub port: u16,
pub theme: String,
pub notify_updates: bool,
}
impl Default for Config {
fn default() -> Self {
Self { port: 3030 }
Self {
port: 3030,
theme: "system".to_string(),
notify_updates: true,
}
}
}

View File

@@ -6,11 +6,17 @@ use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub port: u16,
pub theme: String,
pub notify_updates: bool,
}
impl Default for Config {
fn default() -> Self {
Self { port: 3030 }
Self {
port: 3030,
theme: "system".to_string(),
notify_updates: true
}
}
}

View File

@@ -34,6 +34,28 @@ struct WebSocketState {
config: Config,
}
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);
@@ -43,15 +65,22 @@ async fn start_websocket_server(app_handle: tauri::AppHandle, port: u16) -> Resu
let mut state = state.lock().await;
if let Some(old_abort) = state.server_abort.take() {
let _ = old_abort.send(());
// Give it a moment to shut down
sleep(Duration::from_millis(200)).await;
// 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 = TcpListener::bind(&addr)
.await
.map_err(|e| format!("Failed to bind to port {}: {}", port, e))?;
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();
@@ -331,6 +360,8 @@ pub async fn run() {
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_upload::init())
.manage(websocket_state.clone())
.setup(move |app| {
// Create menu items

View File

@@ -20,7 +20,7 @@
},
"productName": "pytubepp-helper",
"mainBinaryName": "pytubepp-helper",
"version": "0.7.0",
"version": "0.8.0",
"identifier": "com.neosubhamoy.pytubepp.helper",
"plugins": {
"updater": {

View File

@@ -1,7 +1,7 @@
{
"build": {
"beforeDevCommand": "npm run dev && cargo build --manifest-path=./src-tauri/msghost/Cargo.toml",
"beforeBuildCommand": "npm run build && cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml",
"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:1422",
"frontendDist": "../dist"
},
@@ -19,7 +19,22 @@
"csp": null,
"capabilities": [
"main-capability",
"shell-scope"
"shell-scope",
{
"identifier": "fs-scope",
"description": "allowed file system scopes",
"permissions": [
{
"identifier": "fs:scope",
"allow": [
{ "path": "$DOWNLOAD/pytubepp-extension-chrome" },
{ "path": "$DOWNLOAD/pytubepp-extension-chrome/*" },
{ "path": "$TEMP/com.neosubhamoy.pytubepp.helper" },
{ "path": "$TEMP/com.neosubhamoy.pytubepp.helper/*" }
]
}
]
}
]
}
},
@@ -58,6 +73,9 @@
"/etc/xdg/autostart/pytubepp-helper-autostart.desktop": "./autostart/pytubepp-helper-autostart.desktop"
}
}
}
},
"externalBin": [
"binaries/sevenzip"
]
}
}

View File

@@ -1,7 +1,7 @@
{
"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 && 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 copyFiles.${ARCH}.js && npm run 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 copyFiles.${ARCH}.js && node makeFilesExecutable.js && npm run build",
"devUrl": "http://localhost:1422",
"frontendDist": "../dist"
},
@@ -38,7 +38,11 @@
{ "path": "$RESOURCE/pytubepp-helper-msghost.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost-moz.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost" },
{ "path": "$RESOURCE/pytubepp-helper-autostart.plist" }
{ "path": "$RESOURCE/pytubepp-helper-autostart.plist" },
{ "path": "$DOWNLOAD/pytubepp-extension-chrome" },
{ "path": "$DOWNLOAD/pytubepp-extension-chrome/*" },
{ "path": "$TEMP/com.neosubhamoy.pytubepp.helper" },
{ "path": "$TEMP/com.neosubhamoy.pytubepp.helper/*" }
]
}
]
@@ -67,6 +71,9 @@
"pytubepp-helper-msghost-moz.json",
"pytubepp-helper-msghost",
"pytubepp-helper-autostart.plist"
],
"externalBin": [
"binaries/sevenzip"
]
}
}

View File

@@ -1,7 +1,7 @@
{
"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 && node copyFiles.js && npm run build",
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && node copyFiles.x86_64-pc-windows-msvc.js && npm run build",
"devUrl": "http://localhost:1422",
"frontendDist": "../dist"
},
@@ -29,7 +29,11 @@
"allow": [
{ "path": "$RESOURCE/pytubepp-helper-msghost.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost-moz.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost.exe" }
{ "path": "$RESOURCE/pytubepp-helper-msghost.exe" },
{ "path": "$DOWNLOAD/pytubepp-extension-chrome" },
{ "path": "$DOWNLOAD/pytubepp-extension-chrome/*" },
{ "path": "$TEMP/com.neosubhamoy.pytubepp.helper" },
{ "path": "$TEMP/com.neosubhamoy.pytubepp.helper/*" }
]
}
]
@@ -62,6 +66,9 @@
"pytubepp-helper-msghost.json",
"pytubepp-helper-msghost-moz.json",
"pytubepp-helper-msghost.exe"
],
"externalBin": [
"binaries/sevenzip"
]
}
}

View File

@@ -4,16 +4,21 @@ import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { ThemeProvider } from "@/components/theme-provider";
import { WebSocketMessage } from "@/types";
import { sendStreamInfo } from "@/lib/utils";
import { Toaster } from "@/components/ui/toaster";
import { Config, WebSocketMessage } from "@/types";
import { compareVersions, sendStreamInfo } from "@/lib/utils";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { check as checkAppUpdate } from "@tauri-apps/plugin-updater";
import { isPermissionGranted, requestPermission, sendNotification } from "@tauri-apps/plugin-notification";
import { downloadDir, join } from "@tauri-apps/api/path";
import { fetch } from '@tauri-apps/plugin-http';
import * as fs from "@tauri-apps/plugin-fs"
function App({ children }: { children: React.ReactNode }) {
const appWindow = getCurrentWebviewWindow()
const [appConfig, setAppConfig] = useState<Config | null>(null);
const [isAppUpdateChecked, setIsAppUpdateChecked] = useState(false);
const [isExtensionUpdateChecked, setIsExtensionUpdateChecked] = useState(false);
// Prevent right click context menu in production
if (!import.meta.env.DEV) {
@@ -31,6 +36,16 @@ function App({ children }: { children: React.ReactNode }) {
appWindow.onCloseRequested(handleCloseRequested);
}, []);
useEffect(() => {
const getConfig = async () => {
const config: Config = await invoke("get_config");
if (config) {
setAppConfig(config);
}
}
getConfig().catch(console.error);
}, []);
useEffect(() => {
const unlisten = listen<WebSocketMessage>('websocket-message', (event) => {
if(event.payload.command === 'send-stream-info') {
@@ -80,13 +95,53 @@ function App({ children }: { children: React.ReactNode }) {
}
};
if (!isAppUpdateChecked) {
const checkForExtensionUpdates = async () => {
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
try {
setIsExtensionUpdateChecked(true)
const downloadDirPath = await downloadDir()
const extensionManifestPath = await join(downloadDirPath, "pytubepp-extension-chrome", "manifest.json")
const extensionManifestExists = await fs.exists(extensionManifestPath)
if (extensionManifestExists) {
const currentManifest = JSON.parse(await fs.readTextFile(extensionManifestPath))
const response = await fetch('https://github.com/neosubhamoy/pytubepp-extension/releases/latest/download/latest.json', {
method: 'GET',
});
if (response.ok) {
const data = await response.json()
if (compareVersions(data.version, currentManifest.version) === 1) {
console.log(`extension update available v${data.version}`);
if (permissionGranted) {
sendNotification({ title: `Extension Update Available (v${data.version})`, body: `A newer version of PytubePP Extension is available. Please update to the latest version to get the best experience!` });
}
}
}
else {
console.error('Failed to fetch latest extension version');
}
} else {
console.log('Currently installed extension\'s manifest not found')
}
} catch (error) {
console.error(error);
}
};
if (!isAppUpdateChecked && appConfig?.notify_updates) {
checkForUpdates();
}
}, [])
if (!isExtensionUpdateChecked && appConfig?.notify_updates) {
checkForExtensionUpdates();
}
}, [appConfig])
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<ThemeProvider defaultTheme={appConfig?.theme || "system"} storageKey="vite-ui-theme">
<TooltipProvider delayDuration={1000}>
{children}
<Toaster />

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

BIN
src/assets/images/edge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

BIN
src/assets/images/opera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -66,4 +66,16 @@
body {
@apply bg-background text-foreground;
}
::-webkit-scrollbar {
@apply w-1;
}
::-webkit-scrollbar-track {
@apply bg-background rounded-full;
}
::-webkit-scrollbar-thumb {
@apply bg-muted-foreground rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-foreground;
}
}

View File

@@ -18,7 +18,7 @@ export async function isInstalled(program: string, arg: string): Promise<{ insta
return { installed: false, output: output.stdout };
}
} catch (error) {
console.error(error);
console.error(program + ':', error);
return { installed: false, output: null };
}
}
@@ -103,6 +103,7 @@ export function extractVersion(output: string): string | null {
/OS Version:.*Build (\d+)/, // Pattern for Windows build
/apt (\d+\.\d+\.\d+)/, // Pattern for apt
/(\d+\.\d+\.\d+)/, // Pattern for dnf
/Pacman v(\d+\.\d+\.\d+)/, // Pattern for pacman
/ProductVersion:\s+(\d+\.\d+(\.\d+)?)/, // Pattern for macOS version
/Homebrew (\d+\.\d+\.\d+)/, // Pattern for Homebrew
];

View File

@@ -6,6 +6,7 @@ import App from "@/App";
import HomePage from "@/pages/home";
import SettingsPage from "@/pages/settings";
import NotificationsPage from "@/pages/notifications";
import ExtensionManagerPage from "@/pages/extension-manager";
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
@@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Route path="/" element={<HomePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/notifications" element={<NotificationsPage />} />
<Route path="/extension-manager" element={<ExtensionManagerPage />} />
</Routes>
</BrowserRouter>
</App>

View File

@@ -0,0 +1,279 @@
import clsx from "clsx";
import * as fs from "@tauri-apps/plugin-fs"
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getPlatformInfo } from "@/lib/platform-utils";
import { CurrentExtension, LatestExtensionResponse, PlatformInfo } from "@/types";
import { ArrowLeft, ChevronsUpDown, CircleHelp, Loader2, RefreshCcw } from "lucide-react";
import { Link } from "react-router-dom";
import { downloadDir, tempDir, join } from "@tauri-apps/api/path";
import { compareVersions } from "@/lib/utils";
import { fetch } from '@tauri-apps/plugin-http';
import { Card } from "@/components/ui/card";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { download } from '@tauri-apps/plugin-upload';
import { Command } from "@tauri-apps/plugin-shell";
import { toast } from "sonner"
import chromeLogo from "@/assets/images/chrome.png"
import firefoxLogo from "@/assets/images/firefox.png"
import edgeLogo from "@/assets/images/edge.png"
import operaLogo from "@/assets/images/opera.png"
import pytubeppLogo from "@/assets/images/pytubepp.png"
export default function ExtensionManagerPage() {
const [platformInfo, setPlatformInfo] = useState<PlatformInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isUpdating, setIsUpdating] = useState(false);
const [isCollapsibleOpen, setCollapsibleIsOpen] = useState(false)
const [isAccordionOpen, setAccordionIsOpen] = useState(false)
const [isExtensionInstalled, setIsExtensionInstalled] = useState(false);
const [isExtensionUpdateAvailable, setIsExtensionUpdateAvailable] = useState(false);
const [extensionUpdate, setExtensionUpdate] = useState<LatestExtensionResponse | null>(null)
const [currentExtension, setCurrentExtension] = useState<CurrentExtension | null>(null)
const [updateStatus, setUpdateStatus] = useState<string | null>(null)
async function checkForUpdates() {
setIsLoading(true);
try {
const downloadDirPath = await downloadDir()
const extensionManifestPath = await join(downloadDirPath, "pytubepp-extension-chrome", "manifest.json")
const extensionManifestExists = await fs.exists(extensionManifestPath)
const response = await fetch('https://github.com/neosubhamoy/pytubepp-extension/releases/latest/download/latest.json', {
method: 'GET',
});
if (response.ok) {
const data: LatestExtensionResponse = await response.json()
setExtensionUpdate(data)
if (extensionManifestExists) {
setIsExtensionInstalled(true)
const currentManifest = JSON.parse(await fs.readTextFile(extensionManifestPath))
setCurrentExtension(currentManifest)
setIsExtensionUpdateAvailable(compareVersions(data.version, currentManifest.version) === 1)
} else {
setIsExtensionInstalled(false)
setCurrentExtension(null)
setIsExtensionUpdateAvailable(false)
}
}
else {
setIsExtensionUpdateAvailable(false)
setExtensionUpdate(null)
if (extensionManifestExists) {
setIsExtensionInstalled(true)
const currentManifest = JSON.parse(await fs.readTextFile(extensionManifestPath))
setCurrentExtension(currentManifest)
} else {
setIsExtensionInstalled(false)
setCurrentExtension(null)
}
console.error('Failed to fetch latest extension version');
}
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
const unpackExtension = async (extension: LatestExtensionResponse, operation: "unpack" | "update") => {
setIsUpdating(true)
try {
setUpdateStatus('Preparing')
const downloadDirPath = await downloadDir()
const tempDirPath = await tempDir()
const extensionDirPath = await join(downloadDirPath, "pytubepp-extension-chrome")
const appTempDirPath = await join(tempDirPath, "com.neosubhamoy.pytubepp.helper")
const tempExtensionDownloadPath = await join(appTempDirPath, `pytubepp-extension-chrome-v${extension.version}.zip`)
const extensionDirExists = await fs.exists(extensionDirPath)
const appTempDirExists = await fs.exists(appTempDirPath)
if (!extensionDirExists) await fs.mkdir(extensionDirPath, { recursive: true}).then(() => console.log(`Created: ${extensionDirPath}`))
if (!appTempDirExists) await fs.mkdir(appTempDirPath, { recursive: true}).then(() => console.log(`Created: ${appTempDirPath}`))
setUpdateStatus('Downloading')
await download(
extension.browsers.chrome.url,
tempExtensionDownloadPath,
({ progress, total }) => console.log(`Downloading: ${progress} of ${total} bytes`)
);
setUpdateStatus('Unpacking')
const output = await Command.sidecar('binaries/sevenzip', ['x', tempExtensionDownloadPath, `-o${extensionDirPath}`, '-aoa']).execute()
if (output.code === 0) {
console.log(output.stdout)
console.log(`Unpacked ${tempExtensionDownloadPath} to ${extensionDirPath}`)
} else {
console.log(output.stdout, output.stderr)
}
setUpdateStatus('Cleaning')
await fs.remove(tempExtensionDownloadPath)
console.log(`Deleted: ${tempExtensionDownloadPath}`)
setIsExtensionInstalled(true)
setCurrentExtension({version: extension.version})
setIsExtensionUpdateAvailable(false)
if (operation === "unpack") toast(`Successfully unpacked v${extension.version} to ${extensionDirPath}`)
if (operation === "update") toast(`Successfully updated to v${extension.version}. Please reload the extension to reflect changes`)
} catch (error) {
if (operation === "unpack") toast(`Failed to unpack v${extension.version}`)
if (operation === "update") toast(`Failed to update v${extension.version}`)
console.error(error);
} finally {
setIsUpdating(false);
setUpdateStatus(null)
}
}
useEffect(() => {
getPlatformInfo().then(setPlatformInfo).catch(console.error);
checkForUpdates();
}, [])
return (
<div className="container">
<div className={clsx("topbar flex justify-between items-center mt-5", !platformInfo?.isWindows && "mx-3")}>
<div className="flex items-center">
<Link to="/" className={clsx(isUpdating && "pointer-events-none opacity-50")}>
<ArrowLeft className="w-5 h-5 mr-3"/>
</Link>
<h1 className="text-xl font-bold">Extension Manager</h1>
</div>
<div className="flex items-center">
<Tooltip>
<TooltipTrigger>
<Button className="ml-3" size="icon" disabled={isLoading || isUpdating} onClick={checkForUpdates}>
<RefreshCcw className="w-5 h-5"/>
</Button>
</TooltipTrigger>
<TooltipContent><p>refresh</p></TooltipContent>
</Tooltip>
</div>
</div>
<div className={clsx("mt-5", !platformInfo?.isWindows && "mx-3")}>
{
isLoading ? (
<div className="mt-5 mx-3">
<div className="flex flex-col min-h-[55vh]">
<div className="flex items-center justify-center py-[4.3rem]">
<div className="flex flex-col items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin"/>
<p className="ml-3 mt-2 text-muted-foreground">ckecking...</p>
</div>
</div>
</div>
</div>
) : (
<div className="mt-5">
<div className="flex flex-col min-h-[55vh] max-h-[75vh] overflow-y-scroll">
<Card className="p-2 mb-3 flex flex-col">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="imgwrapper h-12 w-12 relative">
<img src={chromeLogo} alt="chrome" />
<img className="absolute bottom-0 right-0 h-5 w-5" src={pytubeppLogo} alt="pytubepp" />
</div>
<div className="flex flex-col ml-3">
<h3>PytubePP Extension (Chrome)</h3>
<p className="text-xs text-muted-foreground">Unpacked: { currentExtension ? currentExtension.version : 'none' } &nbsp; Latest: { extensionUpdate ? extensionUpdate.version : 'unknown' }</p>
</div>
</div>
<Button className="mr-2" size="sm" onClick={isExtensionUpdateAvailable && extensionUpdate ? () => unpackExtension(extensionUpdate, "update") : isExtensionInstalled ? () => {console.log('Already latest version')} : extensionUpdate ? () => unpackExtension(extensionUpdate, "unpack") : () => {console.error('Download url not available')}} disabled={(!isExtensionUpdateAvailable && isExtensionInstalled) || isUpdating}>{isUpdating ? <><Loader2 className="w-4 h-4 animate-spin"/> {updateStatus}</> : isExtensionUpdateAvailable ? 'Update' : isExtensionInstalled ? 'Unpacked' : 'Unpack'}</Button>
</div>
<div className="px-2 mt-2">
<Collapsible
open={isCollapsibleOpen}
onOpenChange={setCollapsibleIsOpen}
className="w-full space-y-2"
>
<div className="flex items-center justify-between space-x-4">
<h4 className="text-sm flex items-center">
<CircleHelp className="w-3 h-3 mr-2"/> How to use unpacked extension
</h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="">
<ul className="text-xs text-muted-foreground">
<li>1. Clicking on the 'Unpack' button unpacks latest pytubepp-extension for chrome within '~/Downloads/pytubepp-extension-chrome' folder</li>
<li>2. You need to manually <a className="underline" href="https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#load-unpacked" target="_blank">load the unpacked extension folder</a> by visiting 'chrome://extensions' page</li>
<li>3. If an update is available the 'Update' button will show up, Simply click on the button to update and don't forget to <a className="underline" href="https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#reload" target="_blank">reload the extension</a> by visiting 'chrome://extensions' page after updating</li>
</ul>
</CollapsibleContent>
</Collapsible>
</div>
</Card>
<Accordion type="single" collapsible className="overflow-x-hidden" onValueChange={() => setAccordionIsOpen(!isAccordionOpen)}>
<AccordionItem value="store-listings">
<AccordionTrigger>Official Store Listings (Auto-Updates)</AccordionTrigger>
<AccordionContent>
<Card className="p-2 mb-3 flex items-center justify-between">
<div className="flex items-center">
<div className="imgwrapper h-12 w-12 flex justify-center items-center">
<img className="h-10" src={firefoxLogo} alt="firefox" />
</div>
<div className="flex flex-col ml-3">
<h3>PytubePP Addon (Firefox)</h3>
<p className="text-xs text-muted-foreground">Add pytubepp-addon to firefox</p>
</div>
</div>
<Button className="mr-2" size="sm" asChild>
<a href="https://addons.mozilla.org/en-US/firefox/addon/pytubepp-addon/" target="_blank" rel="noopener noreferrer">View</a>
</Button>
</Card>
<Card className="p-2 mb-3 flex items-center justify-between">
<div className="flex items-center">
<div className="imgwrapper h-12 w-12 flex justify-center items-center">
<img className="h-10 w-10" src={edgeLogo} alt="edge" />
</div>
<div className="flex flex-col ml-3">
<h3>PytubePP Extension (Edge)</h3>
<p className="text-xs text-muted-foreground">Add pytubepp-extension to edge</p>
</div>
</div>
<Button className="mr-2" size="sm" asChild>
<a href="https://microsoftedge.microsoft.com/addons/detail/pytubepp-extension-foss/ebneapoekcjelholncnlpdohjbjabhbi" target="_blank" rel="noopener noreferrer">View</a>
</Button>
</Card>
<Card className="p-2 flex items-center justify-between">
<div className="flex items-center">
<div className="imgwrapper h-12 w-12 flex justify-center items-center">
<img className="h-10 w-10" src={operaLogo} alt="opera" />
</div>
<div className="flex flex-col ml-3">
<h3>PytubePP Extension (Opera)</h3>
<p className="text-xs text-muted-foreground">Add pytubepp-extension to opera</p>
</div>
</div>
<Button className="mr-2" size="sm" asChild>
<a href="https://addons.opera.com/en/extensions/details/pytubepp-extension-foss/" target="_blank" rel="noopener noreferrer">View</a>
</Button>
</Card>
</AccordionContent>
</AccordionItem>
</Accordion>
{
!isAccordionOpen && !isCollapsibleOpen && (
<div className="mt-3">
<ul className="text-xs text-muted-foreground">
<li>* Extension Manager helps you manage unpacked pytubepp-extension (installing and updating) as pytubepp-extension is not available on Chrome Web Store under <a href="https://developer.chrome.com/docs/webstore/troubleshooting/#prohibited-products" target="_blank" className="underline">Blue Zinc</a> guidelines. (unpacked chrome extension works for all chromium based browsers)</li>
</ul>
</div>
)
}
</div>
</div>
)
}
</div>
</div>
)
}

View File

@@ -6,15 +6,17 @@ import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { InstalledPrograms } from "@/types";
import { compareVersions, extractVersion, isInstalled, registerMacFiles } from "@/lib/utils";
import { CircleCheck, TriangleAlert, CircleAlert, Settings, RefreshCcw, Loader2, PackagePlus, Bell } from "lucide-react";
import { CircleCheck, TriangleAlert, CircleAlert, Settings, RefreshCcw, Loader2, PackagePlus, Bell, Puzzle } from "lucide-react";
import { getPlatformInfo } from "@/lib/platform-utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/hooks/use-toast";
import { toast } from "sonner"
import { NotificationBadge } from "@/components/ui/notification-badge";
import { check as checkAppUpdate } from "@tauri-apps/plugin-updater";
import { fetch } from '@tauri-apps/plugin-http';
import { join, downloadDir } from "@tauri-apps/api/path";
import * as fs from "@tauri-apps/plugin-fs"
export default function HomePage() {
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(true);
const [isWindows, setIsWindows] = useState<boolean>(false)
const [windowsVersion, setWindowsVersion] = useState<string | null>(null)
@@ -23,6 +25,7 @@ export default function HomePage() {
const [distroId, setDistroId] = useState<string | null>(null)
const [distroPkgMngr, setDistroPkgMngr] = useState<string | null>(null)
const [isAppUpdateAvailable, setIsAppUpdateAvailable] = useState(false);
const [isExtensionUpdateAvailable, setIsExtensionUpdateAvailable] = useState(false);
const [installedPrograms, setInstalledPrograms] = useState<InstalledPrograms>({
winget: {
installed: false,
@@ -36,6 +39,10 @@ export default function HomePage() {
installed: false,
version: null,
},
pacman: {
installed: false,
version: null,
},
brew: {
installed: false,
version: null,
@@ -99,6 +106,15 @@ export default function HomePage() {
}
}));
}),
isInstalled('pacman', '--version').then((result) => {
setInstalledPrograms((prevState) => ({
...prevState,
pacman: {
installed: result.installed,
version: result.output ? extractVersion(result.output) : null,
}
}));
}),
isInstalled('homebrew', '--version').then((result) => {
setInstalledPrograms((prevState) => ({
...prevState,
@@ -214,6 +230,34 @@ export default function HomePage() {
checkForUpdates();
}, []);
useEffect(() => {
const checkForExtensionUpdates = async () => {
try {
const downloadDirPath = await downloadDir()
const extensionManifestPath = await join(downloadDirPath, "pytubepp-extension-chrome", "manifest.json")
const extensionManifestExists = await fs.exists(extensionManifestPath)
if (extensionManifestExists) {
const currentManifest = JSON.parse(await fs.readTextFile(extensionManifestPath))
const response = await fetch('https://github.com/neosubhamoy/pytubepp-extension/releases/latest/download/latest.json', {
method: 'GET',
});
if (response.ok) {
const data = await response.json()
setIsExtensionUpdateAvailable(compareVersions(data.version, currentManifest.version) === 1)
}
else {
console.error('Failed to fetch latest extension version');
}
} else {
console.log('Currently installed extension\'s manifest not found')
}
} catch (error) {
console.error(error);
}
};
checkForExtensionUpdates();
}, []);
return (
<div className="container">
<div className={clsx("topbar flex justify-between items-center mt-5", !isWindows && "mx-3")}>
@@ -224,9 +268,25 @@ export default function HomePage() {
<NotificationBadge
label='1'
className='bg-green-700 text-white hover:bg-green-700 hover:cursor-default'
show={isAppUpdateAvailable}
show={isExtensionUpdateAvailable}
>
<Button variant="outline" size="icon" asChild>
<Link to="/extension-manager">
<Puzzle className="w-5 h-5"/>
</Link>
</Button>
</NotificationBadge>
</TooltipTrigger>
<TooltipContent>extension manager</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<NotificationBadge
label='1'
className='bg-green-700 text-white hover:bg-green-700 hover:cursor-default'
show={isAppUpdateAvailable}
>
<Button className="ml-3" variant="outline" size="icon" asChild>
<Link to="/notifications">
<Bell className="w-5 h-5"/>
</Link>
@@ -250,7 +310,7 @@ export default function HomePage() {
<TooltipTrigger>
<Button className="ml-3" size="icon" onClick={async () => {
const result = await registerMacFiles();
toast({ title: result.message, variant: result.success ? 'default' : 'destructive' });
toast(result.message);
}}>
<PackagePlus className="w-5 h-5"/>
</Button>
@@ -299,7 +359,7 @@ export default function HomePage() {
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp || pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
</div>
{(!installedPrograms.apt.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
{(!installedPrograms.apt.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed || !installedPrograms.nodejs.installed)) ?
<Alert className="mt-5" variant="destructive">
<CircleAlert className="h-5 w-5" />
<AlertTitle>APT Not Found</AlertTitle>
@@ -317,7 +377,7 @@ export default function HomePage() {
</AlertDescription>
</Alert>
: null}
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.nodejs.installed && installedPrograms.pytubepp.installed) ?
<Alert className="mt-5">
<CircleCheck className="h-5 w-5" />
<AlertTitle>Ready</AlertTitle>
@@ -345,7 +405,7 @@ export default function HomePage() {
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp || pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
</div>
{(!installedPrograms.dnf.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
{(!installedPrograms.dnf.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed || !installedPrograms.nodejs.installed)) ?
<Alert className="mt-5" variant="destructive">
<CircleAlert className="h-5 w-5" />
<AlertTitle>DNF Not Found</AlertTitle>
@@ -363,7 +423,53 @@ export default function HomePage() {
</AlertDescription>
</Alert>
: null}
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.nodejs.installed && installedPrograms.pytubepp.installed) ?
<Alert className="mt-5">
<CircleCheck className="h-5 w-5" />
<AlertTitle>Ready</AlertTitle>
<AlertDescription>
Everything looks ok! You can close this window now. Make sure it's always running in the background.
</AlertDescription>
</Alert>
: null}
</div>
: distroId && distroPkgMngr && distroPkgMngr === 'pacman' ? /* Section for Arch Linux */
<div className="programstats mt-5 mx-3">
<div className="programitem flex items-center justify-between">
<p><b>Python:</b> {installedPrograms.python3.installed ? 'installed' : 'not installed'} {installedPrograms.python3.version ? `(${installedPrograms.python3.version})` : ''}</p>
{installedPrograms.python3.installed ? installedPrograms.python3.version ? compareVersions(installedPrograms.python3.version, '3.8') < 0 ? <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pacman.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo pacman -Sy python'})}}>install</Button> : <TriangleAlert className="w-5 h-5 my-2 text-orange-400"/> : null}
</div>
<div className="programitem flex items-center justify-between">
<p><b>FFmpeg:</b> {installedPrograms.ffmpeg.installed ? 'installed' : 'not installed'} {installedPrograms.ffmpeg.version ? `(${installedPrograms.ffmpeg.version})` : ''}</p>
{installedPrograms.ffmpeg.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pacman.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo pacman -Sy ffmpeg'})}}>install</Button> : null}
</div>
<div className="programitem flex items-center justify-between">
<p><b>Node.js:</b> {installedPrograms.nodejs.installed ? 'installed' : 'not installed'} {installedPrograms.nodejs.version ? `(${installedPrograms.nodejs.version})` : ''}</p>
{installedPrograms.nodejs.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pacman.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo pacman -Sy nodejs-lts-iron npm'})}}>install</Button> : null}
</div>
<div className="programitem flex items-center justify-between">
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp || pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
</div>
{(!installedPrograms.pacman.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed || !installedPrograms.nodejs.installed)) ?
<Alert className="mt-5" variant="destructive">
<CircleAlert className="h-5 w-5" />
<AlertTitle>Pacman Not Found</AlertTitle>
<AlertDescription>
Pacman is required to install necessary packages. Please install it manually for your distro.
</AlertDescription>
</Alert>
: null}
{(!installedPrograms.pip3.installed && !installedPrograms.pytubepp.installed) ?
<Alert className="mt-5" variant="destructive">
<CircleAlert className="h-5 w-5" />
<AlertTitle>PIP Not Found</AlertTitle>
<AlertDescription>
PIP is required to install necessary python packages. Please install it now to continue: <Button variant="link" className="text-blue-600 p-0" onClick={async () => { await invoke('install_program', {icommand: 'sudo pacman -Sy python-pip'})}}>install</Button>
</AlertDescription>
</Alert>
: null}
{(installedPrograms.python3.installed && installedPrograms.ffmpeg.installed && installedPrograms.nodejs.installed && installedPrograms.pytubepp.installed) ?
<Alert className="mt-5">
<CircleCheck className="h-5 w-5" />
<AlertTitle>Ready</AlertTitle>
@@ -391,7 +497,7 @@ export default function HomePage() {
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip install pytubepp'})}}>install</Button> : null}
</div>
{(!installedPrograms.winget.installed && (!installedPrograms.python.installed || !installedPrograms.ffmpeg.installed)) ?
{(!installedPrograms.winget.installed && (!installedPrograms.python.installed || !installedPrograms.ffmpeg.installed || !installedPrograms.nodejs.installed)) ?
<Alert className="mt-5" variant="destructive">
<CircleAlert className="h-5 w-5" />
<AlertTitle>WinGet Not Found</AlertTitle>
@@ -400,7 +506,7 @@ export default function HomePage() {
</AlertDescription>
</Alert>
: null}
{(installedPrograms.python.installed && installedPrograms.ffmpeg.installed && installedPrograms.pytubepp.installed) ?
{(installedPrograms.python.installed && installedPrograms.ffmpeg.installed && installedPrograms.nodejs.installed && installedPrograms.pytubepp.installed) ?
<Alert className="mt-5">
<CircleCheck className="h-5 w-5" />
<AlertTitle>Ready</AlertTitle>
@@ -428,7 +534,7 @@ export default function HomePage() {
<p><b>PytubePP:</b> {installedPrograms.pytubepp.installed ? 'installed' : 'not installed'} {installedPrograms.pytubepp.version ? `(${installedPrograms.pytubepp.version})` : ''}</p>
{installedPrograms.pytubepp.installed ? <CircleCheck className="w-5 h-5 my-2 text-green-400"/> : installedPrograms.pip3.installed ? <Button variant="link" className="text-blue-600 px-0" onClick={async () => { await invoke('install_program', {icommand: 'pip3 install pytubepp || pip3 install pytubepp --break-system-packages'})}}>install</Button> : null}
</div>
{(!installedPrograms.brew.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed)) ?
{(!installedPrograms.brew.installed && (!installedPrograms.python3.installed || !installedPrograms.ffmpeg.installed || !installedPrograms.nodejs.installed)) ?
<Alert className="mt-5" variant="destructive">
<CircleAlert className="h-5 w-5" />
<AlertTitle>Homebrew Not Found</AlertTitle>

View File

@@ -10,18 +10,25 @@ import { Input } from "@/components/ui/input";
import { Config, PlatformInfo } from "@/types";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useToast } from "@/hooks/use-toast";
import { toast } from "sonner"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getPlatformInfo } from "@/lib/platform-utils";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useTheme } from "@/components/theme-provider"
import { Switch } from "@/components/ui/switch";
const DEFAULT_PORT = 3030;
const DEFAULT_THEME = "system";
const DEFAULT_NOTIFY_UPDATES = true;
const settingsFormSchema = z.object({
port: z.number().min(3000, { message: "Port must be greater than 3000" }).max(3999, { message: "Port must be less than 3999" }),
theme: z.enum(["system", "dark", "light"], { message: "Invalid theme" }),
notify_updates: z.boolean({ message: "Not a boolean value" }),
})
export default function SettingsPage() {
const { toast } = useToast();
const { setTheme } = useTheme();
const [platformInfo, setPlatformInfo] = useState<PlatformInfo | null>(null);
const [appConfig, setAppConfig] = useState<Config | null>(null);
const [appVersion, setAppVersion] = useState<string | null>(null);
@@ -32,13 +39,15 @@ export default function SettingsPage() {
resolver: zodResolver(settingsFormSchema),
defaultValues: {
port: DEFAULT_PORT,
theme: DEFAULT_THEME,
notify_updates: DEFAULT_NOTIFY_UPDATES,
},
});
useEffect(() => {
const subscription = settingsForm.watch((value) => {
if (appConfig) {
setIsFormDirty(value.port !== appConfig.port);
setIsFormDirty(value.port !== appConfig.port || value.theme !== appConfig.theme || value.notify_updates !== appConfig.notify_updates);
}
});
return () => subscription.unsubscribe();
@@ -49,7 +58,7 @@ export default function SettingsPage() {
const config: Config = await invoke("get_config");
if (config) {
setAppConfig(config);
settingsForm.reset({ port: config.port });
settingsForm.reset({ port: config.port, theme: config.theme, notify_updates: config.notify_updates });
}
}
getConfig().catch(console.error);
@@ -64,24 +73,28 @@ export default function SettingsPage() {
getAppVersion().catch(console.error);
}, [])
useEffect(() => {
const updateTheme = async () => {
setTheme(appConfig?.theme || DEFAULT_THEME);
}
updateTheme().catch(console.error);
}, [appConfig?.theme]);
const updateConfig = async () => {
try {
const updatedConfig: Config = await invoke("update_config", {
newConfig: {
port: Number(settingsForm.getValues().port)
port: Number(settingsForm.getValues().port),
theme: settingsForm.getValues().theme,
notify_updates: settingsForm.getValues().notify_updates,
}
});
setAppConfig(updatedConfig);
setIsFormDirty(false);
toast({
title: "Settings updated"
});
toast("Settings updated");
} catch (error) {
console.error("Failed to update config:", error);
toast({
title: "Failed to update settings",
variant: "destructive"
});
toast("Failed to update settings");
}
}
@@ -89,21 +102,16 @@ export default function SettingsPage() {
try {
const config: Config = await invoke("reset_config");
setAppConfig(config);
settingsForm.reset({ port: config.port });
settingsForm.reset({ port: config.port, theme: config.theme, notify_updates: config.notify_updates });
setIsFormDirty(false);
toast({
title: "Using default settings"
});
toast("Settings reset to default");
} catch (error) {
console.error("Failed to reset config:", error);
toast({
title: "Failed to reset settings",
variant: "destructive"
});
toast("Failed to reset settings");
}
}
const isUsingDefaultConfig = appConfig?.port === DEFAULT_PORT;
const isUsingDefaultConfig = appConfig?.port === DEFAULT_PORT && appConfig?.theme === DEFAULT_THEME && appConfig?.notify_updates === DEFAULT_NOTIFY_UPDATES;
return (
<div className="container">
@@ -149,17 +157,18 @@ export default function SettingsPage() {
</div>
</div>
<div className={clsx("mt-5", !platformInfo?.isWindows && "mx-3")}>
<div className="flex flex-col min-h-[55vh]">
<div className="flex flex-col min-h-[55vh] max-h-[58vh] overflow-y-scroll">
<Form {...settingsForm}>
<form onSubmit={settingsForm.handleSubmit(updateConfig)}>
<FormField
control={settingsForm.control}
name="port"
render={({ field }) => (
<FormItem>
<FormItem className="mb-2">
<FormLabel>Port</FormLabel>
<FormControl>
<Input
<Input
className="focus-visible:ring-0"
type="text"
{...field}
onChange={(e) => {
@@ -174,17 +183,65 @@ export default function SettingsPage() {
/>
</FormControl>
<FormDescription>
The port to use for the websocket server
The port to use for websocket communication with msghost
</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={settingsForm.control}
name="theme"
render={({ field }) => (
<FormItem className="mb-2">
<FormLabel>Theme</FormLabel>
<FormControl>
<Select {...field} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full ring-0 focus:ring-0">
<SelectValue placeholder="Select App Theme" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="system">Follow System</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Choose app interface theme
</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={settingsForm.control}
name="notify_updates"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm mb-4 mt-3">
<div className="space-y-0.5">
<FormLabel>Notify Updates</FormLabel>
<FormDescription>
Notify for app and component updates (Recommended)
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Button className="hidden" ref={saveButtonRef} type="submit">Save</Button>
</form>
</Form>
</div>
<div className="flex justify-between items-center border-t border-muted-foreground/50 pt-2">
<div className="flex justify-between items-center border-t border-muted-foreground/50 pt-2 relative">
<div className="tintbar absolute -top-[0.05rem] left-0 -translate-y-full w-full h-5 bg-gradient-to-b from-transparent to-background"></div>
<div className="flex flex-col">
<p>PytubePP Helper <span className="text-muted-foreground">|</span> <span className="text-sm text-muted-foreground">v{appVersion}-beta</span></p>
<p className="text-xs text-muted-foreground">© {new Date().getFullYear()} - <a href="https://github.com/neosubhamoy/pytubepp-helper/blob/main/LICENSE" target="_blank">MIT License</a> - Made with by <a href="https://neosubhamoy.com" target="_blank">Subhamoy</a></p>

View File

@@ -1,5 +1,7 @@
export interface Config {
port: number;
theme: "system" | "dark" | "light";
notify_updates: boolean;
}
export interface PlatformInfo {
@@ -24,6 +26,10 @@ export interface InstalledPrograms {
installed: boolean;
version: string | null;
};
pacman: {
installed: boolean;
version: string | null;
};
brew: {
installed: boolean;
version: string | null;
@@ -70,4 +76,21 @@ export interface Stream {
res: string;
fps: string;
vcodec: string;
}
export interface LatestExtensionResponse {
version: string;
notes: string;
browsers: {
chrome: {
url: string;
},
firefox: {
url: string;
}
}
}
export interface CurrentExtension {
version: string;
}