├── _config.yml ├── .prettierignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── semantic_release.yaml ├── tsconfig.json ├── .gitattributes ├── icons ├── NZBDonkey.ico ├── NZBDonkey_16.png ├── NZBDonkey_32.png ├── NZBDonkey_48.png ├── NZBDonkey_96.png ├── NZBDonkey_128.png ├── NZBDonkey_256.png └── NZBDonkey_512.png ├── .vscode ├── extensions.json └── settings.json ├── src ├── public │ ├── icon │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 96.png │ │ ├── 128.png │ │ ├── 256.png │ │ ├── warn_16.png │ │ ├── warn_32.png │ │ ├── warn_48.png │ │ ├── warn_96.png │ │ ├── error_128.png │ │ ├── error_16.png │ │ ├── error_256.png │ │ ├── error_32.png │ │ ├── error_48.png │ │ ├── error_96.png │ │ ├── warn_128.png │ │ ├── warn_256.png │ │ ├── success_128.png │ │ ├── success_16.png │ │ ├── success_256.png │ │ ├── success_32.png │ │ ├── success_48.png │ │ └── success_96.png │ └── img │ │ ├── download.png │ │ ├── nzbget.png │ │ ├── sabnzbd.png │ │ ├── synology.png │ │ ├── torbox.png │ │ ├── premiumize.png │ │ ├── jdownloader.png │ │ └── searchengine.png ├── services │ ├── searchengines │ │ ├── defaultEngine │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── easyNewsEngine │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── index.ts │ │ ├── settings.ts │ │ └── functions.ts │ ├── lists │ │ ├── index.ts │ │ ├── categoriesRegexpList.ts │ │ └── functions.ts │ ├── interception │ │ ├── libarchive-wasm │ │ │ ├── libarchive.wasm │ │ │ ├── index.ts │ │ │ ├── libarchiveWasm.ts │ │ │ ├── libarchiveWasm.test.ts │ │ │ ├── libarchive.d.ts │ │ │ ├── ArchiveReaderEntry.test.ts │ │ │ ├── ArchiveReaderEntry.ts │ │ │ └── ArchiveReader.test.ts │ │ ├── index.ts │ │ └── settings.ts │ ├── categories │ │ ├── index.ts │ │ └── settings.ts │ ├── general │ │ ├── index.ts │ │ └── settings.ts │ ├── nzbfile │ │ ├── index.ts │ │ └── settings.ts │ ├── notifications │ │ ├── index.ts │ │ └── functions.ts │ ├── targets │ │ ├── functions.ts │ │ ├── nzbget │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── sabnzbd │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── torbox │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── download │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── jdownloader │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── premiumize │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── synology │ │ │ ├── index.ts │ │ │ └── settings.ts │ │ ├── settings.ts │ │ └── index.ts │ ├── messengers │ │ └── extensionMessenger.ts │ └── logger │ │ ├── debugLoggerContent.ts │ │ └── loggerDB.ts ├── assets │ └── primevue │ │ ├── inputotp.css │ │ ├── orderlist.css │ │ ├── ripple.css │ │ ├── confirmdialog.css │ │ ├── blockui.css │ │ ├── skeleton.css │ │ ├── picklist.css │ │ ├── toolbar.css │ │ ├── overlaybadge.css │ │ ├── iconfield.css │ │ ├── inplace.css │ │ ├── buttongroup.css │ │ ├── scrolltop.css │ │ ├── card.css │ │ ├── terminal.css │ │ ├── rating.css │ │ ├── selectbutton.css │ │ ├── panel.css │ │ ├── chip.css │ │ ├── dataview.css │ │ ├── splitbutton.css │ │ ├── knob.css │ │ ├── avatar.css │ │ ├── iftalabel.css │ │ ├── tag.css │ │ ├── tooltip.css │ │ ├── scrollpanel.css │ │ ├── inputtext.css │ │ ├── badge.css │ │ ├── textarea.css │ │ ├── breadcrumb.css │ │ ├── slider.css │ │ ├── imagecompare.css │ │ ├── fieldset.css │ │ ├── speeddial.css │ │ ├── fileupload.css │ │ ├── password.css │ │ ├── divider.css │ │ ├── metergroup.css │ │ ├── progressspinner.css │ │ ├── splitter.css │ │ ├── menu.css │ │ ├── popover.css │ │ ├── carousel.css │ │ ├── colorpicker.css │ │ ├── image.css │ │ ├── paginator.css │ │ ├── common.css │ │ ├── progressbar.css │ │ ├── confirmpopup.css │ │ ├── dock.css │ │ ├── accordion.css │ │ ├── inputgroup.css │ │ ├── drawer.css │ │ ├── toggleswitch.css │ │ ├── listbox.css │ │ ├── checkbox.css │ │ ├── togglebutton.css │ │ ├── tailwind.css │ │ ├── tabs.css │ │ ├── floatlabel.css │ │ ├── organizationchart.css │ │ ├── inputnumber.css │ │ └── radiobutton.css ├── entrypoints │ ├── background │ │ ├── general │ │ │ ├── connectionTestHandler.ts │ │ │ ├── index.ts │ │ │ ├── nzblnkHandler.ts │ │ │ ├── searchEnginesUpdate.ts │ │ │ └── registerContextMenus.ts │ │ ├── interception │ │ │ ├── index.ts │ │ │ ├── interceptionDomainsUpdate.ts │ │ │ └── helperFunction.ts │ │ ├── settingsUpdate │ │ │ ├── v1_2_0.ts │ │ │ ├── v1_4_0.ts │ │ │ ├── v1_3_0.ts │ │ │ └── v1_4_3.ts │ │ └── index.ts │ ├── nzbdialog │ │ ├── main.ts │ │ └── index.html │ ├── nzbdonkey │ │ ├── main.ts │ │ └── index.html │ ├── categoryselection │ │ ├── main.ts │ │ └── index.html │ ├── options │ │ ├── index.html │ │ └── main.ts │ ├── popup │ │ ├── main.ts │ │ └── index.html │ ├── selection.content │ │ └── index.ts │ ├── interception.content │ │ └── index.ts │ └── nzblnk.content │ │ └── index.ts ├── utils │ ├── permissionUtilities.ts │ ├── stringUtilities.ts │ └── settingsUtilities.ts ├── components │ ├── pages │ │ ├── licensePage.vue │ │ ├── changelogPage.vue │ │ └── privacypolicyPage.vue │ ├── targets │ │ ├── targetChooseDialog.vue │ │ ├── downloadSettings.vue │ │ └── premiumizeSettings.vue │ ├── inputs │ │ └── timeoutInput.vue │ ├── logger │ │ └── logDialog.vue │ ├── categories │ │ └── categoryRegExpDialog.vue │ ├── searchengines │ │ └── defaultSearchEnginesDialog.vue │ └── interception │ │ └── defaultDomainsDialog.vue └── modules │ └── libarchive-wasm.ts ├── KNOWN_ISSUES.md ├── postcss.config.ts ├── tailwind.config.js ├── .prettierrc ├── lists └── categoriesRegExpList.json ├── .gitignore ├── global.d.ts ├── SOURCE_CODE_REVIEW_README.md ├── scripts └── vite-plugin-to-utf8.ts ├── LICENSE.md ├── LICENSE.DE.md ├── eslint.config.js ├── .releaserc ├── wxt.config.ts └── package.json /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/services/interception/libarchive-wasm/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.wxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /icons/NZBDonkey.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey.ico -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "lokalise.i18n-ally"] 3 | } 4 | -------------------------------------------------------------------------------- /icons/NZBDonkey_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_16.png -------------------------------------------------------------------------------- /icons/NZBDonkey_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_32.png -------------------------------------------------------------------------------- /icons/NZBDonkey_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_48.png -------------------------------------------------------------------------------- /icons/NZBDonkey_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_96.png -------------------------------------------------------------------------------- /src/public/icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/16.png -------------------------------------------------------------------------------- /src/public/icon/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/32.png -------------------------------------------------------------------------------- /src/public/icon/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/48.png -------------------------------------------------------------------------------- /src/public/icon/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/96.png -------------------------------------------------------------------------------- /icons/NZBDonkey_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_128.png -------------------------------------------------------------------------------- /icons/NZBDonkey_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_256.png -------------------------------------------------------------------------------- /icons/NZBDonkey_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/icons/NZBDonkey_512.png -------------------------------------------------------------------------------- /src/public/icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/128.png -------------------------------------------------------------------------------- /src/public/icon/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/256.png -------------------------------------------------------------------------------- /src/public/icon/warn_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/warn_16.png -------------------------------------------------------------------------------- /src/public/icon/warn_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/warn_32.png -------------------------------------------------------------------------------- /src/public/icon/warn_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/warn_48.png -------------------------------------------------------------------------------- /src/public/icon/warn_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/warn_96.png -------------------------------------------------------------------------------- /src/public/img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/download.png -------------------------------------------------------------------------------- /src/public/img/nzbget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/nzbget.png -------------------------------------------------------------------------------- /src/public/img/sabnzbd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/sabnzbd.png -------------------------------------------------------------------------------- /src/public/img/synology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/synology.png -------------------------------------------------------------------------------- /src/public/img/torbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/torbox.png -------------------------------------------------------------------------------- /src/public/icon/error_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/error_128.png -------------------------------------------------------------------------------- /src/public/icon/error_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/error_16.png -------------------------------------------------------------------------------- /src/public/icon/error_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/error_256.png -------------------------------------------------------------------------------- /src/public/icon/error_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/error_32.png -------------------------------------------------------------------------------- /src/public/icon/error_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/error_48.png -------------------------------------------------------------------------------- /src/public/icon/error_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/error_96.png -------------------------------------------------------------------------------- /src/public/icon/warn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/warn_128.png -------------------------------------------------------------------------------- /src/public/icon/warn_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/warn_256.png -------------------------------------------------------------------------------- /src/public/img/premiumize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/premiumize.png -------------------------------------------------------------------------------- /src/public/icon/success_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/success_128.png -------------------------------------------------------------------------------- /src/public/icon/success_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/success_16.png -------------------------------------------------------------------------------- /src/public/icon/success_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/success_256.png -------------------------------------------------------------------------------- /src/public/icon/success_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/success_32.png -------------------------------------------------------------------------------- /src/public/icon/success_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/success_48.png -------------------------------------------------------------------------------- /src/public/icon/success_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/icon/success_96.png -------------------------------------------------------------------------------- /src/public/img/jdownloader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/jdownloader.png -------------------------------------------------------------------------------- /src/public/img/searchengine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/public/img/searchengine.png -------------------------------------------------------------------------------- /KNOWN_ISSUES.md: -------------------------------------------------------------------------------- 1 | # Known issues 2 | 3 | - Filters in the NZB file and debug log do not work in the popup window on Firefox. 4 | -------------------------------------------------------------------------------- /src/services/searchengines/defaultEngine/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export { defaultSettings, type Settings } from './settings' 3 | -------------------------------------------------------------------------------- /src/services/searchengines/easyNewsEngine/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export { defaultSettings, type Settings } from './settings' 3 | -------------------------------------------------------------------------------- /postcss.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss': {}, 5 | 'autoprefixer': {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/services/lists/index.ts: -------------------------------------------------------------------------------- 1 | export * from './categoriesRegexpList' 2 | export * from './interceptionDoaminsList' 3 | export * from './searchEnginesList' 4 | -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/libarchive.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tensai75/NZBDonkey/HEAD/src/services/interception/libarchive-wasm/libarchive.wasm -------------------------------------------------------------------------------- /src/assets/primevue/inputotp.css: -------------------------------------------------------------------------------- 1 | @import './inputtext'; 2 | 3 | .p-inputotp { 4 | @apply flex items-center gap-2 5 | } 6 | 7 | .p-inputotp-input { 8 | @apply text-center w-10 9 | } -------------------------------------------------------------------------------- /src/services/categories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export { 3 | categoriesDefaultSettings, 4 | categoryDefaultSettings, 5 | type CategoriesSettings, 6 | type CategorySettings, 7 | } from './settings' 8 | -------------------------------------------------------------------------------- /src/assets/primevue/orderlist.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | @import './listbox'; 3 | 4 | .p-orderlist { 5 | @apply flex gap-[1.125rem] 6 | } 7 | 8 | .p-orderlist-controls { 9 | @apply flex flex-col justify-center gap-2 10 | } -------------------------------------------------------------------------------- /src/services/general/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export { 3 | defaultSettings, 4 | get as getSettings, 5 | set as saveSettings, 6 | use as useSettings, 7 | watch as watchSettings, 8 | type Settings, 9 | } from './settings' 10 | -------------------------------------------------------------------------------- /src/assets/primevue/ripple.css: -------------------------------------------------------------------------------- 1 | .p-ink { 2 | @apply block absolute bg-black/10 dark:bg-white/30 scale-0 rounded-[100%] pointer-events-none 3 | } 4 | 5 | .p-ink-active { 6 | @apply transition-[opacity,transform] duration-500 scale-[2.5] opacity-0 ease-linear 7 | } -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ArchiveReader'; 2 | export type { LibarchiveModule } from './libarchive'; 3 | export { default as libarchive } from './libarchive'; 4 | export * from './libarchiveWasm'; 5 | export * from './wrapLibarchiveWasm'; 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import tailwindcss from 'tailwindcss-primeui' 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: ['./src/**/*.{html,ts,js,vue}'], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [tailwindcss], 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/primevue/confirmdialog.css: -------------------------------------------------------------------------------- 1 | @import './dialog'; 2 | @import './button'; 3 | 4 | .p-confirmdialog .p-dialog-content { 5 | @apply flex items-center gap-4 6 | } 7 | 8 | .p-confirmdialog-icon { 9 | @apply text-surface-700 dark:text-surface-0 text-[2rem] h-8 w-8 10 | } -------------------------------------------------------------------------------- /src/assets/primevue/blockui.css: -------------------------------------------------------------------------------- 1 | .p-blockui { 2 | @apply relative 3 | } 4 | 5 | .p-blockui-mask { 6 | @apply rounded-md 7 | } 8 | 9 | .p-blockui-mask.p-overlay-mask { 10 | @apply absolute 11 | } 12 | 13 | .p-blockui-mask-document.p-overlay-mask { 14 | @apply fixed 15 | } -------------------------------------------------------------------------------- /src/assets/primevue/skeleton.css: -------------------------------------------------------------------------------- 1 | .p-skeleton { 2 | @apply overflow-hidden bg-surface-200 dark:bg-surface-700 animate-pulse rounded-md 3 | } 4 | 5 | .p-skeleton-circle { 6 | @apply rounded-full 7 | } 8 | 9 | .p-skeleton-animation-none::after { 10 | @apply animate-none 11 | } 12 | -------------------------------------------------------------------------------- /src/services/interception/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export { 3 | defaultDomainSettings, 4 | defaultSettings, 5 | get as getSettings, 6 | set as saveSettings, 7 | use as useSettings, 8 | watch as watchSettings, 9 | type DomainSettings, 10 | type Settings, 11 | } from './settings' 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 120, 7 | "useTabs": false, 8 | "quoteProps": "consistent", 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "arrowParens": "always", 12 | "endOfLine": "auto" 13 | } 14 | -------------------------------------------------------------------------------- /src/services/nzbfile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export { NZBFileObject, type NZBFileTarget } from './nzbFile.class' 3 | export { 4 | defaultSettings, 5 | get as getSettings, 6 | set as saveSettings, 7 | use as useSettings, 8 | watch as watchSettings, 9 | type Settings, 10 | } from './settings' 11 | -------------------------------------------------------------------------------- /src/services/notifications/index.ts: -------------------------------------------------------------------------------- 1 | import { notification } from './functions' 2 | 3 | export default { 4 | error: (message: string, id: string = '') => notification(2, message, id), 5 | success: (message: string, id: string = '') => notification(1, message, id), 6 | info: (message: string, id: string = '') => notification(0, message, id), 7 | } 8 | -------------------------------------------------------------------------------- /lists/categoriesRegExpList.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "data": [ 4 | { 5 | "name": "Movies", 6 | "regexp": "([hx].?26[45]|xvid|hevc|(dvd|bd)rip|bluray|(720|1080|2160)p|uhd|hdr)" 7 | }, 8 | { 9 | "name": "TV", 10 | "regexp": "((staffel|season|folge|episode).{0,3}\\d|[e|s]\\d+)" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/primevue/picklist.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | @import './listbox'; 3 | 4 | .p-picklist { 5 | @apply flex gap-[1.125rem] 6 | } 7 | 8 | .p-picklist-controls { 9 | @apply flex flex-col justify-center gap-2 10 | } 11 | 12 | .p-picklist-list-container { 13 | @apply flex-grow flex-shrink basis-1/2 14 | } 15 | 16 | .p-picklist .p-listbox { 17 | @apply h-full 18 | } -------------------------------------------------------------------------------- /src/assets/primevue/toolbar.css: -------------------------------------------------------------------------------- 1 | .p-toolbar { 2 | @apply flex items-center justify-between flex-wrap p-3 gap-2 3 | bg-surface-0 dark:bg-surface-900 4 | text-surface-700 dark:text-surface-0 5 | border border-surface-200 dark:border-surface-700 rounded-md 6 | } 7 | 8 | .p-toolbar-start, 9 | .p-toolbar-center, 10 | .p-toolbar-end { 11 | @apply flex items-center 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .env 11 | node_modules 12 | dist 13 | stats.html 14 | stats-*.json 15 | .wxt 16 | web-ext.config.ts 17 | 18 | # Editor directories and files 19 | .VSCodeCounter/* 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | // global.d.ts 2 | /// 3 | 4 | // Type definitions for the 'psl' module 5 | declare module 'psl' { 6 | export function parse(domain: string): { input: string; tld: string; sld: string; domain: string; subdomain: string } 7 | export function isValid(domain: string): boolean 8 | export function get(domain: string): string | null 9 | } 10 | -------------------------------------------------------------------------------- /src/entrypoints/background/general/connectionTestHandler.ts: -------------------------------------------------------------------------------- 1 | import { onMessage } from '@/services/messengers/extensionMessenger' 2 | import * as targets from '@/services/targets' 3 | 4 | export default function (): void { 5 | onMessage('connectionTest', (message) => { 6 | const targetSettings = message.data 7 | return targets[targetSettings.type].testConnection(targetSettings) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/primevue/overlaybadge.css: -------------------------------------------------------------------------------- 1 | @import './badge'; 2 | 3 | .p-overlaybadge { 4 | @apply relative 5 | } 6 | 7 | .p-overlaybadge .p-badge { 8 | @apply absolute top-0 end-0 translate-x-[50%] translate-y-[-50%] origin-[100%_0] m-0 outline outline-2 outline-surface-0 dark:outline-surface-900 9 | } 10 | 11 | .p-overlaybadge .p-badge:dir(rtl) { 12 | @apply -translate-x-1/2 -translate-y-1/2 13 | } 14 | -------------------------------------------------------------------------------- /src/entrypoints/background/interception/index.ts: -------------------------------------------------------------------------------- 1 | import contentScriptRegistrationHandler from './contentScriptRegistrationHandler' 2 | import declarativeNetRequestHandler from './declarativeNetRequestHandler' 3 | import interceptionDomainsUpdate from './interceptionDomainsUpdate' 4 | 5 | export default function (): void { 6 | contentScriptRegistrationHandler() 7 | declarativeNetRequestHandler() 8 | interceptionDomainsUpdate() 9 | } 10 | -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/libarchiveWasm.ts: -------------------------------------------------------------------------------- 1 | import libarchive from './libarchive'; 2 | import { wrapLibarchiveWasm } from './wrapLibarchiveWasm'; 3 | 4 | export type LibarchiveWasm = ReturnType; 5 | 6 | export async function libarchiveWasm( 7 | ...args: Parameters 8 | ): Promise { 9 | return libarchive(...args).then((mod) => wrapLibarchiveWasm(mod)); 10 | } 11 | -------------------------------------------------------------------------------- /src/services/targets/functions.ts: -------------------------------------------------------------------------------- 1 | import { get as getTargetSettings, Settings, TargetSettings } from './settings' 2 | 3 | export const getTargets = async (): Promise => { 4 | const settings = await getTargetSettings() 5 | return settings.targets 6 | } 7 | 8 | export const getActiveTargets = async (settings: Settings): Promise => { 9 | return settings.targets.filter((target) => target.isActive) 10 | } 11 | -------------------------------------------------------------------------------- /src/entrypoints/background/general/index.ts: -------------------------------------------------------------------------------- 1 | import registerConnectionTestHandler from './connectionTestHandler' 2 | import registerNzblnkHandler from './nzblnkHandler' 3 | import registerContextMenus from './registerContextMenus' 4 | import searchEnginesUpdate from './searchEnginesUpdate' 5 | 6 | export default function (): void { 7 | registerContextMenus() 8 | registerNzblnkHandler() 9 | registerConnectionTestHandler() 10 | searchEnginesUpdate() 11 | } 12 | -------------------------------------------------------------------------------- /src/services/targets/nzbget/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'nzbget' 4 | export const name = 'NZBGet' 5 | export const description = i18n.t('targets.nzbget.description') 6 | export const canHaveCategories = true 7 | export const hasTargetCategories = true 8 | export const hasConnectionTest = true 9 | export const hasAdvancedSettings = true 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/services/targets/sabnzbd/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'sabnzbd' 4 | export const name = 'SABnzbd' 5 | export const description = i18n.t('targets.sabnzbd.description') 6 | export const canHaveCategories = true 7 | export const hasTargetCategories = true 8 | export const hasConnectionTest = true 9 | export const hasAdvancedSettings = true 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/services/targets/torbox/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'torbox' 4 | export const name = 'Torbox.app' 5 | export const description = i18n.t('targets.torbox.description') 6 | export const canHaveCategories = false 7 | export const hasTargetCategories = false 8 | export const hasConnectionTest = true 9 | export const hasAdvancedSettings = false 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/entrypoints/background/settingsUpdate/v1_2_0.ts: -------------------------------------------------------------------------------- 1 | import * as nzbfile from '@/services/nzbfile' 2 | 3 | export default async function (): Promise { 4 | await migrateNzbfileSettings() 5 | } 6 | 7 | async function migrateNzbfileSettings() { 8 | const settings: nzbfile.Settings = await nzbfile.getSettings() 9 | settings.filesToBeRemoved = settings.filesToBeRemoved ?? [] 10 | settings.addCategory = !!settings.addCategory 11 | await nzbfile.saveSettings(settings) 12 | } 13 | -------------------------------------------------------------------------------- /src/services/targets/download/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'download' 4 | export const name = 'Download' 5 | export const description = i18n.t('targets.download.description') 6 | export const canHaveCategories = true 7 | export const hasTargetCategories = false 8 | export const hasConnectionTest = false 9 | export const hasAdvancedSettings = false 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/services/targets/jdownloader/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'jdownloader' 4 | export const name = 'JDownloader' 5 | export const description = i18n.t('targets.jdownloader.description') 6 | export const canHaveCategories = false 7 | export const hasTargetCategories = false 8 | export const hasConnectionTest = true 9 | export const hasAdvancedSettings = false 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/services/targets/premiumize/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'premiumize' 4 | export const name = 'Premiumize.me' 5 | export const description = i18n.t('targets.premiumize.description') 6 | export const canHaveCategories = false 7 | export const hasTargetCategories = false 8 | export const hasConnectionTest = true 9 | export const hasAdvancedSettings = false 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/services/targets/synology/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | 3 | export const type = 'synology' 4 | export const name = 'Synology Downloadstation' 5 | export const description = i18n.t('targets.synology.description') 6 | export const canHaveCategories = false 7 | export const hasTargetCategories = false 8 | export const hasConnectionTest = true 9 | export const hasAdvancedSettings = true 10 | export * from './functions' 11 | export { defaultSettings, type Settings } from './settings' 12 | -------------------------------------------------------------------------------- /src/assets/primevue/iconfield.css: -------------------------------------------------------------------------------- 1 | .p-iconfield { 2 | @apply relative 3 | } 4 | 5 | .p-inputicon { 6 | @apply absolute top-1/2 -mt-2 text-surface-400 leading-none 7 | } 8 | 9 | .p-iconfield .p-inputicon:first-child { 10 | @apply start-3 11 | } 12 | 13 | .p-iconfield .p-inputicon:last-child { 14 | @apply end-3 15 | } 16 | 17 | .p-iconfield .p-inputtext:not(:first-child) { 18 | @apply ps-10 19 | } 20 | 21 | .p-iconfield .p-inputtext:not(:last-child) { 22 | @apply pe-10 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/primevue/inplace.css: -------------------------------------------------------------------------------- 1 | .p-inplace-display { 2 | @apply inline cursor-pointer border border-transparent px-3 py-2 rounded-md 3 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 4 | transition-colors duration-200 5 | } 6 | 7 | .p-inplace-display:not(.p-disabled):hover { 8 | @apply bg-surface-100 dark:bg-surface-800 text-surface-800 dark:text-surface-0 9 | } 10 | 11 | .p-inplace-content { 12 | @apply block 13 | } -------------------------------------------------------------------------------- /src/entrypoints/nzbdialog/main.ts: -------------------------------------------------------------------------------- 1 | import PrimeVue from 'primevue/config' 2 | import { createApp } from 'vue' 3 | 4 | import App from './App.vue' 5 | 6 | import { MyPreset } from '@/assets/presets' 7 | import log from '@/services/logger/debugLogger' 8 | 9 | log.initDebugLog('nzbdialog') 10 | 11 | const primeVueTheme = { 12 | theme: { 13 | preset: MyPreset, 14 | }, 15 | } 16 | 17 | const app = createApp(App).use(PrimeVue, primeVueTheme) 18 | 19 | app.mount('#app') 20 | 21 | export default app 22 | -------------------------------------------------------------------------------- /src/entrypoints/nzbdonkey/main.ts: -------------------------------------------------------------------------------- 1 | import PrimeVue from 'primevue/config' 2 | import { createApp } from 'vue' 3 | 4 | import App from './App.vue' 5 | 6 | import { MyPreset } from '@/assets/presets' 7 | import log from '@/services/logger/debugLogger' 8 | 9 | log.initDebugLog('nzbdonkeypage') 10 | 11 | const primeVueTheme = { 12 | theme: { 13 | preset: MyPreset, 14 | }, 15 | } 16 | 17 | const app = createApp(App).use(PrimeVue, primeVueTheme) 18 | 19 | app.mount('#app') 20 | 21 | export default app 22 | -------------------------------------------------------------------------------- /src/services/targets/premiumize/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { categoriesDefaultSettings } from '@/services/categories' 4 | 5 | export const defaultSettings: TargetSettings = { 6 | type: 'premiumize', 7 | name: 'Premiumize.me', 8 | isActive: false, 9 | settings: { 10 | apiKey: '', 11 | timeout: 30000, 12 | }, 13 | categories: categoriesDefaultSettings, 14 | } 15 | 16 | export type Settings = { 17 | apiKey: string 18 | timeout: number 19 | } 20 | -------------------------------------------------------------------------------- /src/services/targets/download/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { categoriesDefaultSettings } from '@/services/categories' 4 | 5 | export const defaultSettings: TargetSettings = { 6 | type: 'download', 7 | name: 'Download', 8 | isActive: false, 9 | settings: { 10 | defaultPath: 'nzbfiles', 11 | saveAs: false, 12 | }, 13 | categories: categoriesDefaultSettings, 14 | } 15 | 16 | export type Settings = { 17 | defaultPath: string 18 | saveAs: boolean 19 | } 20 | -------------------------------------------------------------------------------- /src/entrypoints/categoryselection/main.ts: -------------------------------------------------------------------------------- 1 | import PrimeVue from 'primevue/config' 2 | import { createApp } from 'vue' 3 | 4 | import App from './App.vue' 5 | 6 | import { MyPreset } from '@/assets/presets' 7 | import log from '@/services/logger/debugLogger' 8 | 9 | log.initDebugLog('categoryselection') 10 | 11 | const primeVueTheme = { 12 | theme: { 13 | preset: MyPreset, 14 | }, 15 | } 16 | 17 | const app = createApp(App).use(PrimeVue, primeVueTheme) 18 | 19 | app.mount('#app') 20 | 21 | export default app 22 | -------------------------------------------------------------------------------- /src/assets/primevue/buttongroup.css: -------------------------------------------------------------------------------- 1 | .p-buttongroup .p-button { 2 | @apply m-0 focus-visible:relative focus-visible:z-10 3 | } 4 | 5 | .p-buttongroup .p-button:not(:last-child) { 6 | @apply border-r-0 7 | } 8 | 9 | .p-buttongroup .p-button:not(:first-of-type):not(:last-of-type) { 10 | @apply rounded-none 11 | } 12 | 13 | .p-buttongroup .p-button:first-of-type:not(:only-of-type) { 14 | @apply rounded-e-none 15 | } 16 | 17 | .p-buttongroup .p-button:last-of-type:not(:only-of-type) { 18 | @apply rounded-s-none 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/primevue/scrolltop.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | 3 | .p-scrolltop.p-button { 4 | @apply fixed bottom-5 end-5 5 | } 6 | 7 | .p-scrolltop-sticky.p-button { 8 | @apply sticky flex ms-auto 9 | } 10 | 11 | .p-scrolltop-enter-from { 12 | @apply opacity-0 13 | } 14 | 15 | .p-scrolltop-enter-active { 16 | @apply transition-opacity duration-150 17 | } 18 | 19 | .p-scrolltop.p-scrolltop-leave-to { 20 | @apply opacity-0 21 | } 22 | 23 | .p-scrolltop-leave-active { 24 | @apply transition-opacity duration-150 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/primevue/card.css: -------------------------------------------------------------------------------- 1 | .p-card { 2 | @apply flex flex-col rounded-xl 3 | bg-surface-0 dark:bg-surface-900 4 | text-surface-700 dark:text-surface-0 5 | shadow-[0_1px_3px_0_rgba(0,0,0,0.1),0_1px_2px_-1px_rgba(0,0,0,0.1)] 6 | } 7 | 8 | .p-card-caption { 9 | @apply flex flex-col gap-2 10 | } 11 | 12 | .p-card-body { 13 | @apply p-5 flex flex-col gap-2 14 | } 15 | 16 | .p-card-title { 17 | @apply font-medium text-xl 18 | } 19 | 20 | .p-card-subtitle { 21 | @apply text-surface-500 dark:text-surface-400 22 | } -------------------------------------------------------------------------------- /src/services/targets/torbox/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { categoriesDefaultSettings } from '@/services/categories' 4 | 5 | export const defaultSettings: TargetSettings = { 6 | type: 'torbox', 7 | name: 'Torbox.app', 8 | isActive: false, 9 | settings: { 10 | apiKey: '', 11 | as_queued: false, 12 | timeout: 30000, 13 | }, 14 | categories: categoriesDefaultSettings, 15 | } 16 | 17 | export type Settings = { 18 | apiKey: string 19 | as_queued: boolean 20 | timeout: number 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/permissionUtilities.ts: -------------------------------------------------------------------------------- 1 | import { browser, Browser } from '#imports' 2 | 3 | /** 4 | * Requests a browser permission. 5 | * @param {Browser.permissions.Permissions} permissions - The permissions to request. 6 | * @return {Promise} Resolves to `true` if the permission is granted, otherwise `false`. 7 | * @throws {Error} Throws an error if the permission request fails. 8 | */ 9 | export async function requestPermission(permissions: Browser.permissions.Permissions): Promise { 10 | return browser.permissions.request(permissions) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/pages/licensePage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/services/searchengines/index.ts: -------------------------------------------------------------------------------- 1 | export * as defaultEngine from './defaultEngine' 2 | export * as easyNewsEngine from './easyNewsEngine' 3 | 4 | export { type Settings as DefaultSearchEngineSettings } from './defaultEngine/settings' 5 | export { type Settings as EasyNewSearchsEngineSettings } from './easyNewsEngine/settings' 6 | 7 | export { 8 | defaultSettings, 9 | get as getSettings, 10 | set as saveSettings, 11 | use as useSettings, 12 | watch as watchSettings, 13 | type EngineType, 14 | type SearchEngine, 15 | type Settings, 16 | } from './settings' 17 | -------------------------------------------------------------------------------- /src/entrypoints/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NZBDonkey 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/primevue/terminal.css: -------------------------------------------------------------------------------- 1 | .p-terminal { 2 | @apply h-72 overflow-auto px-3 py-2 rounded-md 3 | border border-surface-300 dark:border-surface-700 4 | bg-surface-0 dark:bg-surface-950 5 | text-surface-700 dark:text-surface-0 6 | } 7 | 8 | .p-terminal-prompt { 9 | @apply flex items-center 10 | } 11 | 12 | .p-terminal-prompt-value { 13 | @apply flex-auto border-none bg-transparent text-inherit p-0 outline-none text-base 14 | } 15 | 16 | .p-terminal-prompt-label { 17 | @apply me-1 18 | } 19 | 20 | .p-terminal-input::-ms-clear { 21 | @apply hidden 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/libarchive-wasm.ts: -------------------------------------------------------------------------------- 1 | import 'wxt' 2 | 3 | import { resolve } from 'node:path' 4 | 5 | import { defineWxtModule } from 'wxt/modules' 6 | 7 | export default defineWxtModule({ 8 | name: 'libarchive-wasm', 9 | setup(wxt) { 10 | wxt.logger.log(`Copying ${resolve('src/services/interception/libarchive-wasm/libarchive.wasm')}`) 11 | wxt.hook('build:publicAssets', (_, assets) => { 12 | assets.push({ 13 | absoluteSrc: resolve('src/services/interception/libarchive-wasm/libarchive.wasm'), 14 | relativeDest: 'libarchive.wasm', 15 | }) 16 | }) 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["src/locales"], 3 | "i18n-ally.keystyle": "nested", 4 | "i18n-ally.extract.autoDetect": false, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "eslint.format.enable": true, 8 | "eslint.workingDirectories": [{ "mode": "auto" }], 9 | "eslint.validate": ["javascript", "typescript", "vue"], 10 | "eslint.options": { 11 | "ignorePatterns": ["src/services/interception/libarchive-wasm/**"] 12 | }, 13 | "editor.codeActionsOnSave": { 14 | "source.organizeImports": "explicit" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/pages/changelogPage.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/pages/privacypolicyPage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/entrypoints/background/general/nzblnkHandler.ts: -------------------------------------------------------------------------------- 1 | import { getSettings as getGeneralSettings } from '@/services/general' 2 | import { onMessage } from '@/services/messengers/extensionMessenger' 3 | import { NZBFileObject } from '@/services/nzbfile' 4 | 5 | export default function (): void { 6 | onMessage('getGeneralSettings', async () => { 7 | const settings = await getGeneralSettings() 8 | return settings 9 | }) 10 | onMessage('searchNzbFile', async (message) => { 11 | const nzbfile = await new NZBFileObject().init() 12 | nzbfile.processNzblnk(message.data.nzblnk, message.data.source) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/entrypoints/background/general/searchEnginesUpdate.ts: -------------------------------------------------------------------------------- 1 | import { updateSearchEnginesList } from '@/services/lists/searchEnginesList' 2 | import * as searchengines from '@/services/searchengines' 3 | 4 | export default function (): void { 5 | // Delay the execution for 5 seconds to allow other startup tasks to complete 6 | setTimeout(() => { 7 | searchengines.getSettings().then(async (settings) => { 8 | if (settings.updateOnStartup) { 9 | settings.engines = await updateSearchEnginesList(settings.engines) 10 | searchengines.saveSettings(settings) 11 | } 12 | }) 13 | }, 5000) 14 | } 15 | -------------------------------------------------------------------------------- /src/entrypoints/background/interception/interceptionDomainsUpdate.ts: -------------------------------------------------------------------------------- 1 | import * as interception from '@/services/interception/' 2 | import { updateInterceptionDomainsList } from '@/services/lists' 3 | 4 | export default function (): void { 5 | // Delay the execution for 5 seconds to allow other startup tasks to complete 6 | setTimeout(() => { 7 | interception.getSettings().then(async (settings) => { 8 | if (settings.updateOnStartup) { 9 | settings.domains = await updateInterceptionDomainsList(settings.domains) 10 | interception.saveSettings(settings) 11 | } 12 | }) 13 | }, 5000) 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/primevue/rating.css: -------------------------------------------------------------------------------- 1 | .p-rating { 2 | @apply relative flex items-center gap-1 3 | } 4 | 5 | .p-rating-option { 6 | @apply inline-flex items-center cursor-pointer rounded-full 7 | } 8 | 9 | .p-rating-option.p-focus-visible { 10 | @apply outline outline-1 outline-offset-2 outline-primary 11 | } 12 | 13 | .p-rating-icon { 14 | @apply text-surface-500 dark:text-surface-400 text-base w-4 h-4 transition-colors duration-200 15 | } 16 | 17 | .p-rating:not(.p-disabled):not(.p-readonly) .p-rating-option:hover .p-rating-icon { 18 | @apply text-primary 19 | } 20 | 21 | .p-rating-option-active .p-rating-icon { 22 | @apply text-primary 23 | } -------------------------------------------------------------------------------- /src/assets/primevue/selectbutton.css: -------------------------------------------------------------------------------- 1 | @import './togglebutton'; 2 | 3 | .p-selectbutton { 4 | @apply inline-flex select-none rounded-md 5 | } 6 | 7 | .p-selectbutton .p-togglebutton { 8 | @apply rounded-none border-y border-r border-s-0 9 | } 10 | 11 | .p-selectbutton .p-togglebutton:focus-visible { 12 | @apply relative z-10 13 | } 14 | 15 | .p-selectbutton .p-togglebutton:first-child { 16 | @apply border-s rounded-s-md 17 | } 18 | 19 | .p-selectbutton .p-togglebutton:last-child { 20 | @apply rounded-e-md 21 | } 22 | 23 | .p-selectbutton.p-invalid { 24 | @apply outline outline-offset-0 outline-red-400 dark:outline-red-300 25 | } 26 | -------------------------------------------------------------------------------- /src/entrypoints/nzbdialog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NZBDonkey 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/entrypoints/nzbdonkey/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NZBDonkey 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/primevue/panel.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | 3 | .p-panel { 4 | @apply border border-surface-200 dark:border-surface-700 rounded-md 5 | bg-surface-0 dark:bg-surface-900 6 | text-surface-700 dark:text-surface-0 7 | } 8 | 9 | .p-panel-header { 10 | @apply flex justify-between items-center p-[1.125rem] 11 | } 12 | 13 | .p-panel-toggleable .p-panel-header { 14 | @apply py-[0.375rem] px-[1.125rem] 15 | } 16 | 17 | .p-panel-title { 18 | @apply leading-none font-semibold 19 | } 20 | 21 | .p-panel-content { 22 | @apply pt-0 pb-[1.125rem] px-[1.125rem] 23 | } 24 | 25 | .p-panel-footer { 26 | @apply pt-0 pb-[1.125rem] px-[1.125rem] 27 | } -------------------------------------------------------------------------------- /src/services/searchengines/easyNewsEngine/settings.ts: -------------------------------------------------------------------------------- 1 | import { SearchEngine } from '../settings' 2 | 3 | export const defaultSettings: SearchEngine = { 4 | type: 'easyNewsEngine', 5 | isActive: true, 6 | name: '', 7 | isDefault: false, 8 | settings: { 9 | username: '', 10 | password: '', 11 | removeUnderscore: false, 12 | removeHyphen: false, 13 | setIntoQuotes: false, 14 | searchURL: '', 15 | downloadURL: '', 16 | }, 17 | } 18 | 19 | export type Settings = { 20 | username: string 21 | password: string 22 | removeUnderscore: boolean 23 | removeHyphen: boolean 24 | setIntoQuotes: boolean 25 | searchURL: string 26 | downloadURL: string 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/services/targets/synology/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { categoriesDefaultSettings } from '@/services/categories' 4 | 5 | export const defaultSettings: TargetSettings = { 6 | type: 'synology', 7 | name: 'Synology Downloadstation', 8 | isActive: false, 9 | settings: { 10 | basepath: '', 11 | host: 'localhost', 12 | password: '', 13 | port: '5000', 14 | scheme: 'http', 15 | username: '', 16 | timeout: 30000, 17 | }, 18 | categories: categoriesDefaultSettings, 19 | } 20 | 21 | export type Settings = { 22 | basepath: string 23 | host: string 24 | password: string 25 | port: string 26 | scheme: 'http' | 'https' 27 | username: string 28 | timeout: number 29 | } 30 | -------------------------------------------------------------------------------- /src/services/categories/settings.ts: -------------------------------------------------------------------------------- 1 | export const categoriesDefaultSettings: CategoriesSettings = { 2 | useCategories: false, 3 | type: 'manual', 4 | fallback: 'none', 5 | categories: [], 6 | } 7 | 8 | export type CategoriesSettings = { 9 | useCategories: boolean 10 | type: 'automatic' | 'manual' | 'fixed' 11 | fallback: 'none' | 'manual' | 'fixed' 12 | categories: CategorySettings[] 13 | } 14 | 15 | export const categoryDefaultSettings: CategorySettings = { 16 | active: true, 17 | isDefault: false, 18 | name: '', 19 | regexp: '', 20 | isTargetCategory: false, 21 | } 22 | 23 | export type CategorySettings = { 24 | active: boolean 25 | isDefault: boolean 26 | name: string 27 | regexp: string 28 | isTargetCategory: boolean 29 | } 30 | -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/libarchiveWasm.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | import { libarchiveWasm } from './libarchiveWasm'; 4 | 5 | describe('libarchiveWasm', () => { 6 | test('version_number', async () => { 7 | const mod = await libarchiveWasm(); 8 | expect(mod.version_number()).toBeGreaterThanOrEqual(0); 9 | }); 10 | 11 | test('version_string', async () => { 12 | const mod = await libarchiveWasm(); 13 | expect(mod.version_string()).toMatch(/^libarchive \d+(?:.\d+)*$/); 14 | }); 15 | 16 | test('version_details', async () => { 17 | const mod = await libarchiveWasm(); 18 | expect(mod.version_details()).toMatch(/^libarchive \d+(?:.\d+)*(?: \w+\/\d+(?:.\d+)*)*$/); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/services/targets/jdownloader/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { Device } from './myJDownloader' 4 | 5 | import { categoriesDefaultSettings } from '@/services/categories' 6 | 7 | export const defaultSettings: TargetSettings = { 8 | type: 'jdownloader', 9 | name: 'JDownloader', 10 | isActive: false, 11 | settings: { 12 | addPaused: false, 13 | device: '', 14 | devices: new Array(), 15 | password: '', 16 | username: '', 17 | timeout: 30000, 18 | }, 19 | categories: categoriesDefaultSettings, 20 | } 21 | 22 | export type Settings = { 23 | addPaused: boolean 24 | device: string 25 | devices: Device[] 26 | password: string 27 | username: string 28 | timeout: number 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/primevue/chip.css: -------------------------------------------------------------------------------- 1 | .p-chip { 2 | @apply inline-flex items-center rounded-2xl gap-2 px-3 py-2 3 | bg-surface-100 dark:bg-surface-800 4 | text-surface-800 dark:text-surface-0 5 | } 6 | 7 | .p-chip-icon { 8 | @apply text-surface-800 dark:bg-surface-0 text-base w-4 h-4 9 | } 10 | 11 | .p-chip-image { 12 | @apply rounded-full w-8 h-8 -ms-2 13 | } 14 | 15 | .p-chip:has(.p-chip-remove-icon) { 16 | @apply pe-2 17 | } 18 | 19 | .p-chip:has(.p-chip-image) { 20 | @apply pt-1 pb-1 21 | } 22 | 23 | .p-chip-remove-icon { 24 | @apply cursor-pointer text-base w-4 h-4 rounded-full 25 | text-surface-800 dark:text-surface-0 26 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 27 | } 28 | -------------------------------------------------------------------------------- /src/services/notifications/functions.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '#i18n' 2 | import { browser } from '#imports' 3 | import { get as getGeneralSettings } from '@/services/general/settings' 4 | 5 | export function notification(level: number, message: string, id: string = '') { 6 | getGeneralSettings().then((settings) => { 7 | if (level >= settings.notifications) { 8 | const title = i18n.t('extension.name') 9 | const notificationID = id != '' ? 'NZBDonkey_#' + id : 'NZBDonkey_#' + Date.now().toString() 10 | const iconURL = ['icon/128.png', 'icon/success_128.png', 'icon/error_128.png'] 11 | browser.notifications.create(notificationID, { 12 | type: 'basic', 13 | iconUrl: iconURL[level], 14 | title: title, 15 | message: message, 16 | }) 17 | } 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /src/services/targets/nzbget/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { categoriesDefaultSettings } from '@/services/categories' 4 | 5 | export const defaultSettings: TargetSettings = { 6 | type: 'nzbget', 7 | name: 'NZBGet', 8 | isActive: false, 9 | settings: { 10 | addPaused: false, 11 | dupeMode: 'Force', 12 | basepath: '', 13 | host: 'localhost', 14 | password: '', 15 | port: '6789', 16 | scheme: 'http', 17 | username: '', 18 | timeout: 30000, 19 | }, 20 | categories: categoriesDefaultSettings, 21 | } 22 | 23 | export type Settings = { 24 | addPaused: boolean 25 | dupeMode: 'Force' | 'Score' | 'All' 26 | basepath: string 27 | host: string 28 | password: string 29 | port: string 30 | scheme: 'http' | 'https' 31 | username: string 32 | timeout: number 33 | } 34 | -------------------------------------------------------------------------------- /src/services/targets/sabnzbd/settings.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings } from '../settings' 2 | 3 | import { categoriesDefaultSettings } from '@/services/categories' 4 | 5 | export const defaultSettings: TargetSettings = { 6 | type: 'sabnzbd', 7 | name: 'SABnzbd', 8 | isActive: false, 9 | settings: { 10 | addPaused: false, 11 | apiKey: '', 12 | basepath: '', 13 | basicAuthPassword: '', 14 | basicAuthUsername: '', 15 | host: 'localhost', 16 | port: '8080', 17 | scheme: 'http', 18 | timeout: 30000, 19 | }, 20 | categories: categoriesDefaultSettings, 21 | } 22 | 23 | export type Settings = { 24 | addPaused: boolean 25 | apiKey: string 26 | basepath: string 27 | basicAuthPassword: string 28 | basicAuthUsername: string 29 | host: string 30 | port: string 31 | scheme: 'http' | 'https' 32 | timeout: number 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/primevue/dataview.css: -------------------------------------------------------------------------------- 1 | @import './paginator'; 2 | 3 | .p-dataview { 4 | @apply border-none 5 | } 6 | 7 | .p-dataview-header { 8 | @apply py-3 px-4 border-b border-surface-200 dark:border-surface-700 9 | bg-surface-0 dark:bg-surface-900 10 | text-surface-700 dark:text-surface-0 11 | } 12 | 13 | .p-dataview-content { 14 | @apply bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 15 | } 16 | 17 | .p-dataview-footer { 18 | @apply py-3 px-4 border-t border-surface-200 dark:border-surface-700 19 | bg-surface-0 dark:bg-surface-900 20 | text-surface-700 dark:text-surface-0 21 | } 22 | 23 | .p-dataview-paginator-top { 24 | @apply border-b border-surface-200 dark:border-surface-700 25 | } 26 | 27 | .p-dataview-paginator-bottom { 28 | @apply border-t border-surface-200 dark:border-surface-700 29 | } -------------------------------------------------------------------------------- /src/assets/primevue/splitbutton.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | @import './tieredmenu'; 3 | 4 | .p-splitbutton { 5 | @apply inline-flex relative rounded-md 6 | } 7 | 8 | .p-splitbutton-button { 9 | @apply rounded-e-none border-r-0 enabled:hover:border-r-0 enabled:active:border-r-0 focus-visible:z-10 10 | } 11 | 12 | .p-splitbutton-dropdown { 13 | @apply rounded-s-none focus-visible:z-10 14 | } 15 | 16 | .p-splitbutton .p-menu { 17 | @apply min-w-full 18 | } 19 | 20 | .p-splitbutton-fluid { 21 | @apply w-full 22 | } 23 | 24 | .p-splitbutton-rounded .p-splitbutton-dropdown { 25 | @apply rounded-e-[2rem] 26 | } 27 | 28 | .p-splitbutton-rounded .p-splitbutton-button { 29 | @apply rounded-s-[2rem] 30 | } 31 | 32 | .p-splitbutton-raised { 33 | @apply shadow-[0_3px_1px_-2px_rgba(0,0,0,0.2),0_2px_2px_0_rgba(0,0,0,0.14),0_1px_5px_0_rgba(0,0,0,0.12)] 34 | } 35 | -------------------------------------------------------------------------------- /SOURCE_CODE_REVIEW_README.md: -------------------------------------------------------------------------------- 1 | # Build instruction 2 | 3 | ## Operating systems 4 | 5 | - Windows 10 22H2 or higher 6 | - Ubuntu latest version 7 | 8 | ## Requirements 9 | 10 | - Node.js v22.x LTS (latest) 11 | - npm (latest version) 12 | 13 | ## Installation of dependencies 14 | 15 | ### Install command 16 | 17 | ``` 18 | npm ci 19 | ``` 20 | 21 | ## Build steps 22 | 23 | ### Chrome 24 | 25 | #### Build command 26 | 27 | ``` 28 | npm run zip 29 | ``` 30 | 31 | #### Resulting files 32 | 33 | ``` 34 | ./dist/chrome-mv3 [contains unpacked extension] 35 | ./dist/nzbdonky-v{{version}}-chrome.zip 36 | ``` 37 | 38 | ### FireFox 39 | 40 | #### Build command 41 | 42 | ``` 43 | npm run zip:firefox 44 | ``` 45 | 46 | #### Resulting files 47 | 48 | ``` 49 | ./dist/firefox-mv3 [contains unpacked extension] 50 | ./dist/nzbdonky-v{{version}}-firefox.zip 51 | ./dist/nzbdonky-v{{version}}-sources.zip 52 | ``` 53 | -------------------------------------------------------------------------------- /src/assets/primevue/knob.css: -------------------------------------------------------------------------------- 1 | .p-knob-range { 2 | @apply fill-none transition-[stroke] duration-100 ease-in 3 | } 4 | 5 | .p-knob-text { 6 | @apply text-xl text-center 7 | } 8 | 9 | .p-knob svg { 10 | @apply rounded-full 11 | transition-colors duration-200 12 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 13 | } 14 | 15 | .p-knob svg path:first-child { 16 | @apply stroke-surface-200 dark:stroke-surface-700 17 | } 18 | 19 | .p-knob svg path + path { 20 | @apply stroke-primary 21 | } 22 | 23 | .p-knob svg text { 24 | @apply fill-surface-500 dark:fill-surface-400 25 | } 26 | 27 | .p-knob-value { 28 | animation-name: p-knob-dash-frame; 29 | animation-fill-mode: forwards; 30 | fill: none; 31 | } 32 | 33 | @keyframes p-knob-dash-frame { 34 | 100% { 35 | stroke-dashoffset: 0; 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/targets/targetChooseDialog.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 24 | 25 | {{ item.name }} 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/entrypoints/categoryselection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NZBDonkey 10 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/entrypoints/options/main.ts: -------------------------------------------------------------------------------- 1 | import 'floating-vue/dist/style.css' 2 | 3 | import { vTooltip } from 'floating-vue' 4 | import PrimeVue from 'primevue/config' 5 | import ConfirmationService from 'primevue/confirmationservice' 6 | import { createApp } from 'vue' 7 | 8 | import App from './App.vue' 9 | 10 | import { i18n } from '#i18n' 11 | import { MyPreset } from '@/assets/presets' 12 | import log from '@/services/logger/debugLogger' 13 | 14 | log.initDebugLog('options') 15 | 16 | const primeVueTheme = { 17 | theme: { 18 | preset: MyPreset, 19 | }, 20 | locale: { 21 | apply: i18n.t('common.apply'), 22 | clear: i18n.t('common.clear'), 23 | emptyMessage: i18n.t('common.noAvailableOptions'), 24 | }, 25 | } 26 | 27 | const app = createApp(App).use(PrimeVue, primeVueTheme).use(ConfirmationService).directive('tooltip', vTooltip) 28 | 29 | app.mount('#app') 30 | 31 | export default app 32 | -------------------------------------------------------------------------------- /src/entrypoints/popup/main.ts: -------------------------------------------------------------------------------- 1 | import 'floating-vue/dist/style.css' 2 | 3 | import { vTooltip } from 'floating-vue' 4 | import PrimeVue from 'primevue/config' 5 | import ConfirmationService from 'primevue/confirmationservice' 6 | import { createApp } from 'vue' 7 | 8 | import App from './App.vue' 9 | 10 | import { i18n } from '#i18n' 11 | import { MyPreset } from '@/assets/presets' 12 | import log from '@/services/logger/debugLogger' 13 | 14 | log.initDebugLog('popup') 15 | 16 | const primeVueTheme = { 17 | theme: { 18 | preset: MyPreset, 19 | }, 20 | locale: { 21 | apply: i18n.t('common.apply'), 22 | clear: i18n.t('common.clear'), 23 | emptyMessage: i18n.t('common.noAvailableOptions'), 24 | }, 25 | } 26 | 27 | const app = createApp(App).use(PrimeVue, primeVueTheme).use(ConfirmationService).directive('tooltip', vTooltip) 28 | 29 | app.mount('#app') 30 | 31 | export default app 32 | -------------------------------------------------------------------------------- /src/services/searchengines/defaultEngine/settings.ts: -------------------------------------------------------------------------------- 1 | import { SearchEngine } from '../settings' 2 | 3 | export const defaultSettings: SearchEngine = { 4 | type: 'defaultEngine', 5 | name: '', 6 | isActive: true, 7 | isDefault: false, 8 | settings: { 9 | searchURL: '', 10 | responseType: 'html', 11 | searchPattern: '', 12 | posterPattern: '', 13 | searchGroup: 1, 14 | posterGroup: 1, 15 | downloadURL: '', 16 | removeUnderscore: false, 17 | removeHyphen: false, 18 | setIntoQuotes: false, 19 | groupByPoster: false, 20 | resultSelector: '', 21 | }, 22 | } 23 | 24 | export type Settings = { 25 | searchURL: string 26 | responseType: 'json' | 'html' 27 | searchPattern: string 28 | posterPattern: string 29 | searchGroup: number 30 | posterGroup: number 31 | downloadURL: string 32 | removeUnderscore: boolean 33 | removeHyphen: boolean 34 | setIntoQuotes: boolean 35 | groupByPoster: boolean 36 | resultSelector: string 37 | } 38 | -------------------------------------------------------------------------------- /src/entrypoints/background/settingsUpdate/v1_4_0.ts: -------------------------------------------------------------------------------- 1 | import { updateSearchEnginesList } from '@/services/lists' 2 | import * as searchengines from '@/services/searchengines' 3 | import * as targets from '@/services/targets' 4 | 5 | export default async function (): Promise { 6 | await migrateSearchEnginesSettings() 7 | await migrateTargetsSettings() 8 | } 9 | 10 | async function migrateSearchEnginesSettings() { 11 | const settings: searchengines.Settings = await searchengines.getSettings() 12 | settings.engines = await updateSearchEnginesList(settings.engines) 13 | await searchengines.saveSettings(settings) 14 | } 15 | 16 | async function migrateTargetsSettings() { 17 | const settings: targets.TargetsSettings = await targets.getSettings() 18 | for (const target of settings.targets) { 19 | if (target.type === 'jdownloader') { 20 | ;(target.settings as targets.JdownloaderTargetSettings).timeout = 30000 21 | } 22 | } 23 | await targets.saveSettings(settings) 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/primevue/avatar.css: -------------------------------------------------------------------------------- 1 | .p-avatar { 2 | @apply inline-flex items-center justify-center 3 | w-8 h-8 text-base rounded-md 4 | bg-surface-200 dark:bg-surface-700 5 | } 6 | 7 | .p-avatar-image { 8 | @apply bg-transparent 9 | } 10 | 11 | .p-avatar-circle, 12 | .p-avatar-circle img { 13 | @apply rounded-full 14 | } 15 | 16 | .p-avatar-icon { 17 | @apply text-base 18 | } 19 | 20 | .p-avatar img { 21 | @apply w-full h-full 22 | } 23 | 24 | .p-avatar-lg { 25 | @apply w-12 h-12 text-2xl 26 | } 27 | 28 | .p-avatar-lg .p-avatar-icon { 29 | @apply text-2xl 30 | } 31 | 32 | .p-avatar-xl { 33 | @apply w-16 h-16 text-[2rem] 34 | } 35 | 36 | .p-avatar-xl .p-avatar-icon { 37 | @apply text-[2rem] 38 | } 39 | 40 | .p-avatar-group { 41 | @apply flex items-center 42 | } 43 | 44 | .p-avatar-group .p-avatar + .p-avatar { 45 | @apply -ms-4 46 | } 47 | 48 | .p-avatar-group .p-avatar { 49 | @apply border-2 border-surface-200 dark:border-surface-700 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/primevue/iftalabel.css: -------------------------------------------------------------------------------- 1 | .p-iftalabel { 2 | @apply block relative 3 | } 4 | 5 | .p-iftalabel label { 6 | @apply absolute pointer-events-none top-2 transition-all ease-out duration-200 leading-none text-xs font-medium start-3 text-surface-500 dark:text-surface-400 7 | } 8 | 9 | .p-iftalabel .p-inputtext, 10 | .p-iftalabel .p-textarea, 11 | .p-iftalabel .p-select-label, 12 | .p-iftalabel .p-multiselect-label, 13 | .p-iftalabel .p-autocomplete-input-multiple, 14 | .p-iftalabel .p-cascadeselect-label, 15 | .p-iftalabel .p-treeselect-label { 16 | @apply pt-6 pb-2 17 | } 18 | 19 | .p-iftalabel:has(.p-invalid) label { 20 | @apply text-red-400 dark:text-red-300 21 | } 22 | 23 | .p-iftalabel:has(input:focus) label , 24 | .p-iftalabel:has(input:-webkit-autofill) label, 25 | .p-iftalabel:has(textarea:focus) label , 26 | .p-iftalabel:has(.p-inputwrapper-focus) label { 27 | @apply text-primary 28 | } 29 | 30 | .p-iftalabel .p-inputicon { 31 | @apply top-6 translate-y-1/4 mt-0 32 | } 33 | -------------------------------------------------------------------------------- /scripts/vite-plugin-to-utf8.ts: -------------------------------------------------------------------------------- 1 | import { OutputBundle, OutputOptions } from 'rollup' 2 | import { PluginOption } from 'vite' 3 | 4 | function strToUtf8(str: string) { 5 | return str 6 | .split('') 7 | .map((ch) => (ch.charCodeAt(0) <= 0x7f ? ch : '\\u' + ('0000' + ch.charCodeAt(0).toString(16)).slice(-4))) 8 | .join('') 9 | } 10 | 11 | export default function toUtf8(): PluginOption { 12 | return { 13 | name: 'to-utf8', 14 | generateBundle(options: OutputOptions, bundle: OutputBundle) { 15 | // Iterate through each asset in the bundle 16 | for (const fileName in bundle) { 17 | if (bundle[fileName].type === 'chunk') { 18 | // Assuming you want to convert the chunk's code 19 | const originalCode = bundle[fileName].code 20 | const modifiedCode = strToUtf8(originalCode) 21 | 22 | // Update the chunk's code with the modified version 23 | bundle[fileName].code = modifiedCode 24 | } 25 | } 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/primevue/tag.css: -------------------------------------------------------------------------------- 1 | .p-tag { 2 | @apply inline-flex items-center justify-center 3 | bg-primary-100 dark:bg-primary-500/15 4 | text-primary-700 dark:text-primary-300 5 | text-sm font-bold py-1 px-2 rounded-md gap-1 6 | } 7 | 8 | .p-tag-icon { 9 | @apply text-xs w-3 h-3 10 | } 11 | 12 | .p-tag-rounded { 13 | @apply rounded-2xl 14 | } 15 | 16 | .p-tag-success { 17 | @apply bg-green-100 dark:bg-green-500/15 text-green-700 dark:text-green-300 18 | } 19 | 20 | .p-tag-info { 21 | @apply bg-sky-100 dark:bg-sky-500/15 text-sky-700 dark:text-sky-300 22 | } 23 | 24 | .p-tag-warn { 25 | @apply bg-orange-100 dark:bg-orange-500/15 text-orange-700 dark:text-orange-300 26 | } 27 | 28 | .p-tag-danger { 29 | @apply bg-red-100 dark:bg-red-500/15 text-red-700 dark:text-red-300 30 | } 31 | 32 | .p-tag-secondary { 33 | @apply bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 34 | } 35 | 36 | .p-tag-contrast { 37 | @apply bg-surface-950 dark:bg-surface-0 text-surface-0 dark:text-surface-950 38 | } -------------------------------------------------------------------------------- /src/services/messengers/extensionMessenger.ts: -------------------------------------------------------------------------------- 1 | import { defineExtensionMessaging, RemoveListenerCallback as RemoveListenerCallbackType } from '@webext-core/messaging' 2 | 3 | import { Settings as GeneralSettings } from '@/services/general' 4 | import { NZBFileObject } from '@/services/nzbfile' 5 | import { TargetSettings } from '@/services/targets' 6 | import { SerializedRequest, SerializedResponse } from '@/utils/fetchUtilities' 7 | 8 | interface ProtocolMap { 9 | analyseTextSelection(data: { tabId: number }): void 10 | getGeneralSettings(data: boolean): Promise 11 | searchNzbFile(data: { nzblnk: string; source: string }): void 12 | nzbFileDialog(data: { windowID: number }): NZBFileObject | NZBFileObject[] 13 | connectionTest(data: TargetSettings): Promise 14 | fetchRequest(data: SerializedRequest): Promise 15 | heartbeat(data: null): void 16 | } 17 | export const { sendMessage, onMessage } = defineExtensionMessaging() 18 | export type RemoveListenerCallback = RemoveListenerCallbackType 19 | -------------------------------------------------------------------------------- /src/assets/primevue/tooltip.css: -------------------------------------------------------------------------------- 1 | .p-tooltip { 2 | @apply absolute hidden max-w-48 3 | } 4 | 5 | .p-tooltip-right, 6 | .p-tooltip-left { 7 | @apply py-0 px-1 8 | } 9 | 10 | .p-tooltip-top, 11 | .p-tooltip-bottom { 12 | @apply py-1 px-0 13 | } 14 | 15 | .p-tooltip-text { 16 | @apply whitespace-pre-line break-words bg-surface-700 text-surface-0 py-2 px-3 17 | rounded-md shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)] 18 | } 19 | 20 | .p-tooltip-arrow { 21 | @apply absolute w-0 h-0 border-solid border-transparent 22 | } 23 | 24 | .p-tooltip-right .p-tooltip-arrow { 25 | @apply -mt-1 border-[.25rem] border-s-0 border-e-surface-700 rtl:rotate-180 26 | } 27 | 28 | .p-tooltip-left .p-tooltip-arrow { 29 | @apply -mt-1 border-[.25rem] border-e-0 border-s-surface-700 rtl:rotate-180 30 | } 31 | 32 | .p-tooltip-top .p-tooltip-arrow { 33 | @apply -ms-1 border-[.25rem] border-b-0 border-t-surface-700 34 | } 35 | 36 | .p-tooltip-bottom .p-tooltip-arrow { 37 | @apply -ms-1 border-[.25rem] border-t-0 border-b-surface-700 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright (c) 2025 Tensai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/entrypoints/background/index.ts: -------------------------------------------------------------------------------- 1 | import { PublicPath } from 'wxt/browser' 2 | 3 | import { browser, defineBackground } from '#imports' 4 | import generalBackground from '@/entrypoints/background/general' 5 | import interceptionBackground from '@/entrypoints/background/interception' 6 | import settingsUpdate from '@/entrypoints/background/settingsUpdate' 7 | import log from '@/services/logger/debugLogger' 8 | 9 | export default defineBackground(() => { 10 | log.initDebugLog('background') 11 | log.initMessageListener() 12 | settingsUpdate() 13 | generalBackground() 14 | interceptionBackground() 15 | browser.runtime.onInstalled.addListener((details) => { 16 | if (details.reason === 'install') { 17 | log.info('NZBDonkey has been freshly installed') 18 | // save the current version to storage 19 | browser.storage.sync.set({ version: browser.runtime.getManifest().version }) 20 | log.info('opening info page') 21 | browser.tabs.create({ url: browser.runtime.getURL('/nzbdonkey.html' as PublicPath) }) 22 | } 23 | }) 24 | log.info('background script loaded successfully') 25 | }) 26 | -------------------------------------------------------------------------------- /src/entrypoints/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NZBDonkey 10 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/entrypoints/background/general/registerContextMenus.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerAnalyseSelectionContextMenu, 3 | registerAnalyseSelectionContextMenuListener, 4 | } from './analyseSelectionHandler' 5 | import { registerSendToContextMenu, registerSendToContextMenuListener } from './sendToHandler' 6 | 7 | import log from '@/services/logger/debugLogger' 8 | import { watchSettings as watchTargetSettings } from '@/services/targets' 9 | 10 | export default function (): void { 11 | registerContextMenus() 12 | registerContextMenuListeners() 13 | watchTargetSettings(() => { 14 | log.info('target settings have changed: updating context menus') 15 | registerContextMenus() 16 | }) 17 | } 18 | 19 | async function registerContextMenus(): Promise { 20 | // remove all existing context menus 21 | log.info('removing all existing context menus') 22 | await browser.contextMenus.removeAll() 23 | registerAnalyseSelectionContextMenu() 24 | registerSendToContextMenu() 25 | } 26 | 27 | function registerContextMenuListeners(): void { 28 | registerAnalyseSelectionContextMenuListener() 29 | registerSendToContextMenuListener() 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/primevue/scrollpanel.css: -------------------------------------------------------------------------------- 1 | .p-scrollpanel-content-container { 2 | @apply overflow-hidden w-full h-full relative z-10 float-left 3 | } 4 | 5 | .p-scrollpanel-content { 6 | @apply relative overflow-auto 7 | h-[calc(100%+18px)] w-[calc(100%+18px)] 8 | pt-0 ps-0 pr-[18px] pb-[18px] [scrollbar-width:none] 9 | } 10 | 11 | .p-scrollpanel-content::-webkit-scrollbar { 12 | @apply hidden 13 | } 14 | 15 | .p-scrollpanel-bar { 16 | @apply relative rounded-sm z-20 cursor-pointer opacity-0 17 | bg-surface-100 dark:bg-surface-800 18 | transition-opacity duration-200 border-none 19 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 20 | } 21 | 22 | .p-scrollpanel-bar-y { 23 | @apply w-[9px] top-0 24 | } 25 | 26 | .p-scrollpanel-bar-x { 27 | @apply h-[9px] bottom-0 28 | } 29 | 30 | .p-scrollpanel-hidden { 31 | @apply invisible 32 | } 33 | 34 | .p-scrollpanel:hover .p-scrollpanel-bar, 35 | .p-scrollpanel:active .p-scrollpanel-bar { 36 | @apply opacity-100 37 | } 38 | 39 | .p-scrollpanel-grabbed { 40 | @apply select-none 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/primevue/inputtext.css: -------------------------------------------------------------------------------- 1 | .p-inputtext { 2 | @apply appearance-none rounded-md 3 | border border-surface-300 dark:border-surface-700 4 | enabled:hover:border-surface-400 dark:enabled:hover:border-surface-600 5 | enabled:focus:border-primary dark:enabled:focus:border-primary 6 | bg-surface-0 dark:bg-surface-950 7 | text-surface-700 dark:text-surface-0 8 | disabled:bg-surface-200 disabled:text-surface-500 dark:disabled:bg-surface-700 dark:disabled:text-surface-400 disabled:opacity-100 9 | placeholder:text-surface-500 dark:placeholder:text-surface-400 10 | px-3 py-2 11 | transition-colors duration-200 12 | shadow-[0_1px_2px_0_rgba(18,18,23,0.05)] 13 | outline-none 14 | } 15 | 16 | .p-inputtext.p-invalid { 17 | @apply border-red-400 dark:border-red-300 18 | } 19 | 20 | .p-inputtext.p-variant-filled { 21 | @apply bg-surface-50 dark:bg-surface-800 22 | } 23 | 24 | .p-inputtext-sm { 25 | @apply text-sm px-[0.625rem] py-[0.375rem] 26 | } 27 | 28 | .p-inputtext-lg { 29 | @apply text-lg px-[0.875rem] py-[0.625rem] 30 | } 31 | 32 | .p-inputtext-fluid { 33 | @apply w-full 34 | } -------------------------------------------------------------------------------- /src/services/general/settings.ts: -------------------------------------------------------------------------------- 1 | import { getSettings, setSettings, useSettings, watchSettings } from '@/utils/settingsUtilities' 2 | 3 | export const name = 'generalSettings' 4 | 5 | export const defaultSettings: Settings = { 6 | catchLinks: true, 7 | catchLinksShowDialog: false, 8 | debug: true, 9 | textSelection: { 10 | title: [], 11 | header: [], 12 | password: [], 13 | }, 14 | notifications: 0, 15 | nzbLog: true, 16 | } 17 | 18 | export type Settings = { 19 | catchLinks: boolean 20 | catchLinksShowDialog: boolean 21 | notifications: 0 | 1 | 2 | 3 22 | textSelection: { 23 | title: string[] 24 | header: string[] 25 | password: string[] 26 | } 27 | nzbLog: boolean 28 | debug: boolean 29 | } 30 | 31 | export const use = async () => useSettings({ name, defaults: defaultSettings }) 32 | export const get = async () => getSettings({ name, defaults: defaultSettings }) 33 | export const set = async (newSettings: Settings) => 34 | setSettings({ name, defaults: defaultSettings }, newSettings) 35 | export const watch = (callback: (settings: Settings) => void) => 36 | watchSettings({ name, defaults: defaultSettings }, callback) 37 | -------------------------------------------------------------------------------- /src/services/lists/categoriesRegexpList.ts: -------------------------------------------------------------------------------- 1 | import categoriesRegexpDefaultList from '@@/lists/categoriesRegExpList.json' 2 | 3 | import { fetchAndValidateList } from './functions' 4 | 5 | import { CategorySettings as CategoriesRegExpListItem, categoryDefaultSettings } from '@/services/categories' 6 | 7 | const categoriesRegexpList = { 8 | url: `https://raw.githubusercontent.com/${import.meta.env.WXT_REPOSITORY_NAME}/${import.meta.env.WXT_BRANCH_NAME}/lists/categoriesRegExpList.json`, 9 | expectedVersion: 1, 10 | sortkey: 'name', 11 | defaultList: categoriesRegexpDefaultList, 12 | defaultKeys: [...Object.keys(categoryDefaultSettings)] as (keyof CategoriesRegExpListItem)[], 13 | } 14 | 15 | export function getCategoriesRegexpList(): Promise { 16 | return fetchAndValidateList( 17 | 'categories regexp list', 18 | categoriesRegexpList.url, 19 | categoriesRegexpList.expectedVersion, 20 | categoriesRegexpList.sortkey as keyof CategoriesRegExpListItem, 21 | categoriesRegexpList.defaultList as { version: number; data: CategoriesRegExpListItem[] }, 22 | categoriesRegexpList.defaultKeys as (keyof CategoriesRegExpListItem)[] 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/primevue/badge.css: -------------------------------------------------------------------------------- 1 | .p-badge { 2 | @apply inline-flex items-center justify-center rounded-md 3 | py-0 px-2 text-xs font-bold min-w-6 h-6 4 | bg-primary text-primary-contrast 5 | } 6 | 7 | .p-badge-dot { 8 | @apply min-w-2 h-2 rounded-full p-0 9 | } 10 | 11 | .p-badge-circle { 12 | @apply p-0 rounded-full 13 | } 14 | 15 | .p-badge-secondary { 16 | @apply bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 17 | } 18 | 19 | .p-badge-success { 20 | @apply bg-green-500 dark:bg-green-400 text-white dark:text-green-950 21 | } 22 | 23 | .p-badge-info { 24 | @apply bg-sky-500 dark:bg-sky-400 text-white dark:text-sky-950 25 | } 26 | 27 | .p-badge-warn { 28 | @apply bg-orange-500 dark:bg-orange-400 text-white dark:text-orange-950 29 | } 30 | 31 | .p-badge-danger { 32 | @apply bg-red-500 dark:bg-red-400 text-white dark:text-red-950 33 | } 34 | 35 | .p-badge-contrast { 36 | @apply bg-surface-950 dark:bg-white text-white dark:text-surface-950 37 | } 38 | 39 | .p-badge-sm { 40 | @apply text-[0.625rem] min-w-5 h-5 41 | } 42 | 43 | .p-badge-lg { 44 | @apply text-sm min-w-7 h-7 45 | } 46 | 47 | .p-badge-xl { 48 | @apply text-base min-w-8 h-8 49 | } -------------------------------------------------------------------------------- /src/assets/primevue/textarea.css: -------------------------------------------------------------------------------- 1 | .p-textarea { 2 | @apply appearance-none rounded-md 3 | border border-surface-300 dark:border-surface-700 4 | enabled:hover:border-surface-400 dark:enabled:hover:border-surface-600 5 | enabled:focus:border-primary 6 | bg-surface-0 dark:bg-surface-950 7 | text-surface-700 dark:text-surface-0 8 | disabled:bg-surface-200 disabled:text-surface-500 disabled:opacity-100 dark:disabled:bg-surface-700 dark:disabled:text-surface-400 9 | placeholder:text-surface-500 dark:placeholder:text-surface-400 10 | px-3 py-2 11 | transition-colors duration-200 12 | shadow-[0_1px_2px_0_rgba(18,18,23,0.05)] 13 | outline-none 14 | } 15 | 16 | .p-textarea.p-invalid { 17 | @apply border-red-400 dark:border-red-300 18 | } 19 | 20 | .p-textarea.p-variant-filled { 21 | @apply bg-surface-50 dark:bg-surface-800 22 | } 23 | 24 | .p-textarea-fluid { 25 | @apply w-full 26 | } 27 | 28 | .p-textarea-resizable { 29 | @apply overflow-hidden resize-none 30 | } 31 | 32 | .p-textarea-sm { 33 | @apply text-sm px-[0.625rem] py-[0.375rem] 34 | } 35 | 36 | .p-textarea-lg { 37 | @apply text-lg px-[0.875rem] py-[0.625rem] 38 | } 39 | -------------------------------------------------------------------------------- /src/components/inputs/timeoutInput.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/semantic_release.yaml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - beta 7 | - rc 8 | - master 9 | paths: 10 | - 'src/**' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 # fetch all commits for changelog 23 | persist-credentials: false 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 22 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Run semantic-release 34 | uses: cycjimmy/semantic-release-action@v4 35 | with: 36 | extra_plugins: | 37 | @semantic-release/changelog 38 | @semantic-release/npm 39 | @semantic-release/git 40 | @semantic-release/exec 41 | @semantic-release/github 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.PAT }} 44 | WXT_REPOSITORY_NAME: ${{ github.repository }} 45 | WXT_BRANCH_NAME: ${{ github.ref_name }} 46 | -------------------------------------------------------------------------------- /src/assets/primevue/breadcrumb.css: -------------------------------------------------------------------------------- 1 | .p-breadcrumb { 2 | @apply bg-surface-0 dark:bg-surface-900 p-4 overflow-x-auto 3 | } 4 | 5 | .p-breadcrumb-list { 6 | @apply m-0 p-0 list-none flex items-center flex-nowrap gap-2 7 | } 8 | 9 | .p-breadcrumb-separator { 10 | @apply flex items-center text-surface-400 dark:text-surface-500 11 | } 12 | 13 | .p-breadcrumb-separator-icon:dir(rtl) { 14 | @apply rotate-180 15 | } 16 | 17 | .p-breadcrumb::-webkit-scrollbar { 18 | @apply hidden 19 | } 20 | 21 | .p-breadcrumb-item-link { 22 | @apply no-underline flex items-center gap-2 transition-colors duration-200 rounded-md 23 | text-surface-500 dark:text-surface-400 24 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 25 | } 26 | 27 | .p-breadcrumb-item-link:hover .p-breadcrumb-item-label { 28 | @apply text-surface-700 dark:text-surface-0 29 | } 30 | 31 | .p-breadcrumb-item-label { 32 | @apply transition-colors duration-200 33 | } 34 | 35 | .p-breadcrumb-item-icon { 36 | @apply text-surface-400 dark:text-surface-500 transition-colors duration-200 37 | } 38 | 39 | .p-breadcrumb-item-link:hover .p-breadcrumb-item-icon { 40 | @apply text-surface-500 dark:text-surface-400 41 | } 42 | -------------------------------------------------------------------------------- /src/components/logger/logDialog.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /LICENSE.DE.md: -------------------------------------------------------------------------------- 1 | ## MIT-Lizenz 2 | 3 | Copyright (c) 2025 by Tensai 4 | 5 | Jedem, der eine Kopie dieser Software und der zugehörigen Dokumentationsdateien (die „Software“) erhält, wird hiermit kostenlos die Erlaubnis erteilt, ohne Einschränkung mit der Software zu handeln, einschliesslich und ohne Einschränkung der Rechte zur Nutzung, zum Kopieren, Ändern, Zusammenführen, Veröffentlichen, Verteilen, Unterlizenzieren und/oder Verkaufen von Kopien der Software, und Personen, denen die Software zur Verfügung gestellt wird, dies unter den folgenden Bedingungen zu gestatten: 6 | 7 | Der obige Urheberrechtshinweis und dieser Genehmigungshinweis müssen in allen Kopien oder wesentlichen Teilen der Software enthalten sein. 8 | 9 | DIE SOFTWARE WIRD OHNE MÄNGELGEWÄHR UND OHNE JEGLICHE AUSDRÜCKLICHE ODER STILLSCHWEIGENDE GEWÄHRLEISTUNG, EINSCHLIESSLICH, ABER NICHT BESCHRÄNKT AUF DIE GEWÄHRLEISTUNG DER MARKTGÄNGIGKEIT, DER EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND DER NICHTVERLETZUNG VON RECHTEN DRITTER, ZUR VERFÜGUNG GESTELLT. DIE AUTOREN ODER URHEBERRECHTSINHABER SIND IN KEINEM FALL HAFTBAR FÜR ANSPRÜCHE, SCHÄDEN ODER ANDERE VERPFLICHTUNGEN, OB IN EINER VERTRAGS- ODER HAFTUNGSKLAGE, EINER UNERLAUBTEN HANDLUNG ODER ANDERWEITIG, DIE SICH AUS ODER IN VERBINDUNG MIT DER SOFTWARE ODER DER NUTZUNG ODER ANDEREN GESCHÄFTEN MIT DER SOFTWARE ERGEBEN. 10 | -------------------------------------------------------------------------------- /src/assets/primevue/slider.css: -------------------------------------------------------------------------------- 1 | .p-slider { 2 | @apply relative bg-surface-200 dark:bg-surface-700 rounded-sm 3 | } 4 | 5 | .p-slider-handle { 6 | @apply cursor-grab touch-none flex items-center justify-center h-[20px] w-[20px] 7 | bg-surface-200 dark:bg-surface-700 rounded-full 8 | transition-colors duration-200 9 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 10 | before:w-[16px] before:h-[16px] before:block before:rounded-full 11 | before:bg-surface-0 dark:before:bg-surface-950 12 | before:shadow-[0px_0.5px_0px_0px_rgba(0,0,0,0.08),0px_1px_1px_0px_rgba(0,0,0,0.14)] 13 | before:transition-colors before:duration-200 14 | } 15 | 16 | .p-slider-range { 17 | @apply block bg-primary rounded-sm 18 | } 19 | 20 | .p-slider.p-slider-horizontal { 21 | @apply h-[3px] 22 | } 23 | 24 | .p-slider-horizontal .p-slider-range { 25 | @apply top-0 start-0 h-full 26 | } 27 | 28 | .p-slider-horizontal .p-slider-handle { 29 | @apply top-1/2 -mt-[10px] -ms-[10px] 30 | } 31 | 32 | .p-slider-vertical { 33 | @apply min-h-[100px] w-[3px] 34 | } 35 | 36 | .p-slider-vertical .p-slider-handle { 37 | @apply start-1/2 -mb-[10px] -ms-[10px] 38 | } 39 | 40 | .p-slider-vertical .p-slider-range { 41 | @apply bottom-0 start-0 w-full 42 | } 43 | -------------------------------------------------------------------------------- /src/entrypoints/selection.content/index.ts: -------------------------------------------------------------------------------- 1 | import PrimeVue from 'primevue/config' 2 | import { createApp } from 'vue' 3 | 4 | import App from './App.vue' 5 | 6 | import { createShadowRootUi, defineContentScript } from '#imports' 7 | import { MyPreset } from '@/assets/presets' 8 | import log from '@/services/logger/debugLogger' 9 | 10 | export default defineContentScript({ 11 | registration: 'runtime', 12 | matches: [''], 13 | main(ctx) { 14 | log.initDebugLog('selection-content') 15 | const primeVueTheme = { 16 | theme: { 17 | preset: MyPreset, 18 | options: { 19 | prefix: 'nzbdonkey', 20 | }, 21 | }, 22 | } 23 | // Define the UI 24 | createShadowRootUi(ctx, { 25 | name: 'nzbdonkey-selection-dialog', 26 | position: 'inline', 27 | anchor: 'body', 28 | append: 'last', 29 | inheritStyles: true, 30 | onMount: (container) => { 31 | // Define how the UI will be mounted inside the container 32 | const app = createApp(App).use(PrimeVue, primeVueTheme) 33 | app.mount(container) 34 | return app 35 | }, 36 | onRemove: (app) => { 37 | // Unmount the app when the UI is removed 38 | app?.unmount() 39 | }, 40 | }).then((ui) => { 41 | // Mount the UI 42 | ui.mount() 43 | }) 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /src/services/logger/debugLoggerContent.ts: -------------------------------------------------------------------------------- 1 | import { defineExtensionMessaging } from '@webext-core/messaging' 2 | 3 | import { DebugLogProtocolMap, DebugLogQuery, IDebugLog } from './loggerDB' 4 | 5 | const extensionMessenger = defineExtensionMessaging() 6 | 7 | const log = (message: IDebugLog) => { 8 | console[message.type]( 9 | `[NZBDonkey] ${message.text.charAt(0).toUpperCase() + message.text.slice(1)}`, 10 | message.error ?? '' 11 | ) 12 | extensionMessenger.sendMessage('debbugLoggerLog', message) 13 | } 14 | const clear = () => extensionMessenger.sendMessage('debbugLoggerClear', undefined) 15 | const get = () => { 16 | return extensionMessenger.sendMessage('debbugLoggerGet', undefined) 17 | } 18 | const getLazy = (debugLogQuery: DebugLogQuery) => { 19 | return extensionMessenger.sendMessage('debbugLoggerGetLazy', JSON.parse(JSON.stringify(debugLogQuery))) 20 | } 21 | const count = (debugLogQuery: DebugLogQuery) => { 22 | return extensionMessenger.sendMessage('debbugLoggerCount', JSON.parse(JSON.stringify(debugLogQuery))) 23 | } 24 | const download = () => { 25 | return extensionMessenger.sendMessage('debbugLoggerDownload', undefined) 26 | } 27 | const getSources = () => { 28 | return extensionMessenger.sendMessage('debbugLoggerGetSources', undefined) 29 | } 30 | 31 | export { clear, count, download, get, getLazy, getSources, log } 32 | -------------------------------------------------------------------------------- /src/services/nzbfile/settings.ts: -------------------------------------------------------------------------------- 1 | import { getSettings, setSettings, useSettings, watchSettings } from '@/utils/settingsUtilities' 2 | 3 | export const name = 'nzbfileSettings' 4 | 5 | export const defaultSettings: Settings = { 6 | addPassword: true, 7 | addTitle: true, 8 | addCategory: true, 9 | addPasswordToFilename: true, 10 | processTitle: true, 11 | processTitleType: 'spaces', 12 | fileCheck: true, 13 | fileCheckThreshold: 1, 14 | segmentCheck: true, 15 | segmentCheckThreshold: 2, 16 | filesToBeRemoved: [], 17 | } 18 | 19 | export type Settings = { 20 | addPassword: boolean 21 | addTitle: boolean 22 | addCategory: boolean 23 | addPasswordToFilename: boolean 24 | processTitle: boolean 25 | processTitleType: 'spaces' | 'dots' 26 | fileCheck: boolean 27 | fileCheckThreshold: number 28 | segmentCheck: boolean 29 | segmentCheckThreshold: number 30 | filesToBeRemoved: string[] 31 | } 32 | 33 | export const use = async () => useSettings({ name, defaults: defaultSettings }) 34 | export const get = async () => getSettings({ name, defaults: defaultSettings }) 35 | export const set = async (newSettings: Settings) => 36 | setSettings({ name, defaults: defaultSettings }, newSettings) 37 | export const watch = (callback: (settings: Settings) => void) => 38 | watchSettings({ name, defaults: defaultSettings }, callback) 39 | -------------------------------------------------------------------------------- /src/services/searchengines/settings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as DefaultEngineSettings } from './defaultEngine/settings' 2 | import { Settings as EasyNewsEngineSettings } from './easyNewsEngine/settings' 3 | 4 | import { getSettings, setSettings, useSettings, watchSettings } from '@/utils/settingsUtilities' 5 | 6 | export const name = 'serachenginesSettings' 7 | 8 | export const defaultSettings: Settings = { 9 | searchOrder: 'parallel', 10 | engines: [], 11 | updateOnStartup: true, 12 | } 13 | 14 | export type Settings = { 15 | searchOrder: 'parallel' | 'sequential' 16 | engines: SearchEngine[] 17 | updateOnStartup: boolean 18 | } 19 | 20 | export type SearchEngine = { 21 | type: EngineType 22 | name: string 23 | isActive: boolean 24 | isDefault: boolean 25 | settings: DefaultEngineSettings | EasyNewsEngineSettings 26 | icon?: string 27 | } 28 | 29 | export type EngineType = 'defaultEngine' | 'easyNewsEngine' 30 | 31 | export const use = async () => useSettings({ name, defaults: defaultSettings }) 32 | export const get = async () => getSettings({ name, defaults: defaultSettings }) 33 | export const set = async (newSettings: Settings) => 34 | setSettings({ name, defaults: defaultSettings }, newSettings) 35 | export const watch = (callback: (settings: Settings) => void) => 36 | watchSettings({ name, defaults: defaultSettings }, callback) 37 | -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/libarchive.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-default-export */ 2 | 3 | // cf. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts 4 | 5 | interface JSTypes { 6 | number: number; 7 | string: string; 8 | } 9 | 10 | type JSType = keyof JSTypes; 11 | 12 | type ReturnToType = R extends null ? null : R extends JSType ? JSTypes[R] : never; 13 | 14 | type ArgsToType = SA extends readonly [infer S, ...infer R] 15 | ? readonly [ReturnToType, ...ArgsToType] 16 | : readonly []; 17 | 18 | // eslint-disable-next-line import/exports-last 19 | export interface LibarchiveModule { 20 | _free: (ptr: number) => void; 21 | 22 | _malloc: (size: number) => number; 23 | 24 | cwrap: < 25 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters 26 | RT extends ReturnToType, 27 | IA = undefined, 28 | R extends JSType = JSType, 29 | I extends readonly JSType[] = readonly JSType[], 30 | >( 31 | ident: string, 32 | returnType: R, 33 | argTypes: I, 34 | ) => (...args: IA extends unknown[] ? IA : ArgsToType) => RT; 35 | 36 | HEAP8: Int8Array; 37 | 38 | locateFile: (url: string, scriptDirectory: string) => string; 39 | } 40 | 41 | declare function libarchive(options?: Partial): Promise; 42 | 43 | export default libarchive; 44 | -------------------------------------------------------------------------------- /src/assets/primevue/imagecompare.css: -------------------------------------------------------------------------------- 1 | .p-imagecompare { 2 | @apply relative overflow-hidden w-full aspect-video 3 | } 4 | 5 | .p-imagecompare img { 6 | @apply w-full h-full absolute 7 | } 8 | 9 | .p-imagecompare img + img { 10 | @apply [clip-path:polygon(0_0,50%_0,50%_100%,0_100%)] 11 | } 12 | 13 | .p-imagecompare:dir(rtl) img + img { 14 | @apply [clip-path:polygon(50%_0,100%_0,100%_100%,50%_100%)] 15 | } 16 | 17 | .p-imagecompare-slider { 18 | @apply relative appearance-none w-[calc(100%+1rem)] h-full -ms-2 bg-transparent outline-none transition-all duration-200 19 | hover:w-[calc(100%+2rem)] hover:-ms-4 20 | } 21 | 22 | .p-imagecompare-slider::-webkit-slider-thumb { 23 | @apply appearance-none h-4 w-4 bg-white/30 rounded-full bg-contain cursor-ew-resize transition-all duration-200 24 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 25 | } 26 | 27 | .p-imagecompare-slider::-moz-range-thumb { 28 | @apply appearance-none h-4 w-4 bg-white/30 rounded-full bg-contain cursor-ew-resize transition-all duration-200 29 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 30 | } 31 | 32 | .p-imagecompare-slider:hover::-webkit-slider-thumb { 33 | @apply bg-white/40 h-8 w-8 34 | } 35 | 36 | .p-imagecompare-slider:hover::-moz-range-thumb { 37 | @apply bg-white/40 h-8 w-8 38 | } 39 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginJs from '@eslint/js' 2 | import eslintConfigPrettier from 'eslint-config-prettier' 3 | import pluginImport from 'eslint-plugin-import' 4 | import pluginPrettier from 'eslint-plugin-prettier' 5 | import pluginVue from 'eslint-plugin-vue' 6 | import globals from 'globals' 7 | import tseslint from 'typescript-eslint' 8 | 9 | /** @type {import('eslint').Linter.Config[]} */ 10 | export default [ 11 | { files: ['**/*.{js,mjs,cjs,ts,vue}'] }, 12 | { languageOptions: { globals: globals.browser } }, 13 | pluginJs.configs.recommended, 14 | ...tseslint.configs.recommended, 15 | ...pluginVue.configs['flat/recommended'], 16 | { 17 | plugins: { 18 | import: pluginImport, 19 | prettier: pluginPrettier, // Add Prettier as a plugin 20 | }, 21 | rules: { 22 | 'import/order': [ 23 | 'error', 24 | { 25 | 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], 26 | 'newlines-between': 'always', 27 | 'alphabetize': { order: 'asc', caseInsensitive: true }, 28 | }, 29 | ], 30 | 'prettier/prettier': 'error', // Run Prettier as an ESLint rule 31 | }, 32 | }, 33 | { 34 | files: ['**/*.vue'], 35 | languageOptions: { parserOptions: { parser: tseslint.parser } }, 36 | }, 37 | { ignores: ['src/services/interception/libarchive-wasm/**'] }, 38 | eslintConfigPrettier, // Ensure Prettier config is applied last 39 | ] 40 | -------------------------------------------------------------------------------- /src/assets/primevue/fieldset.css: -------------------------------------------------------------------------------- 1 | .p-fieldset { 2 | @apply border border-surface-200 dark:border-surface-700 rounded-md 3 | bg-surface-0 dark:bg-surface-900 4 | text-surface-700 dark:text-surface-0 5 | px-[1.125rem] pt-0 pb-[1.125rem] 6 | } 7 | 8 | .p-fieldset-legend { 9 | @apply border border-transparent rounded-md px-3 py-2 10 | transition-colors duration-200 11 | } 12 | 13 | .p-fieldset-toggleable > .p-fieldset-legend { 14 | @apply p-0 15 | } 16 | 17 | .p-fieldset-toggle-button { 18 | @apply select-none overflow-hidden relative 19 | flex items-center justify-center gap-2 20 | px-3 py-2 21 | border-none rounded-md 22 | bg-surface-0 dark:bg-surface-900 23 | hover:bg-surface-100 dark:hover:bg-surface-800 24 | text-surface-700 dark:text-surface-0 25 | hover:text-surface-800 dark:hover:text-surface-0 26 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 27 | transition-colors duration-200 28 | } 29 | 30 | .p-fieldset-legend-label { 31 | @apply font-semibold; 32 | } 33 | 34 | .p-fieldset-toggle-icon { 35 | @apply text-surface-500 dark:text-surface-400 36 | transition-colors duration-200 37 | } 38 | 39 | .p-fieldset-toggle-button:hover .p-fieldset-toggle-icon { 40 | @apply text-surface-600 dark:text-surface-300 41 | } 42 | 43 | .p-fieldset .p-fieldset-content { 44 | @apply p-0 45 | } -------------------------------------------------------------------------------- /src/assets/primevue/speeddial.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | 3 | .p-speeddial { 4 | @apply static flex gap-2 5 | } 6 | 7 | .p-speeddial-button { 8 | @apply z-10 9 | } 10 | 11 | .p-speeddial-button.p-speeddial-rotate { 12 | @apply [transition:transform_250ms_cubic-bezier(0.4,0,0.2,1)_0ms,background_200ms,color_200ms,border-color_200ms] will-change-transform 13 | } 14 | 15 | .p-speeddial-list { 16 | @apply m-0 p-0 list-none flex items-center justify-center pointer-events-none outline-none z-20 gap-2 17 | transition-[top] ease-linear duration-200 18 | } 19 | 20 | .p-speeddial-item { 21 | @apply scale-0 opacity-0 [transition:transform_200ms_cubic-bezier(0.4,0,0.2,1)_0ms,opacity_0.8s] will-change-transform 22 | } 23 | 24 | .p-speeddial-circle .p-speeddial-item, 25 | .p-speeddial-semi-circle .p-speeddial-item, 26 | .p-speeddial-quarter-circle .p-speeddial-item { 27 | @apply absolute 28 | } 29 | 30 | .p-speeddial-mask { 31 | @apply absolute start-0 top-0 w-full h-full opacity-0 bg-white/40 dark:bg-white/60 rounded-md transition-opacity duration-150 32 | } 33 | 34 | .p-speeddial-mask-visible { 35 | @apply pointer-events-none opacity-100 transition-opacity duration-150 36 | } 37 | 38 | .p-speeddial-open .p-speeddial-list { 39 | @apply pointer-events-auto 40 | } 41 | 42 | .p-speeddial-open .p-speeddial-item { 43 | @apply scale-100 opacity-100 44 | } 45 | 46 | .p-speeddial-open .p-speeddial-rotate { 47 | @apply rotate-45 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/primevue/fileupload.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | @import './message'; 3 | @import './progressbar'; 4 | 5 | .p-fileupload input[type="file"] { 6 | @apply hidden 7 | } 8 | 9 | .p-fileupload-advanced { 10 | @apply border border-surface-200 dark:border-surface-700 rounded-md 11 | bg-surface-0 dark:bg-surface-900 12 | text-surface-700 dark:text-surface-0 13 | } 14 | 15 | .p-fileupload-header { 16 | @apply flex items-center p-[1.125rem] gap-2 text-surface-700 dark:text-surface-0 17 | } 18 | 19 | .p-fileupload-content { 20 | @apply border border-transparent relative transition-colors duration-200 pt-0 px-[1.125rem] pb-[1.125rem] 21 | } 22 | 23 | .p-fileupload-content .p-progressbar { 24 | @apply w-full absolute top-0 start-0 h-1 25 | } 26 | 27 | .p-fileupload-file-list { 28 | @apply flex flex-col gap-2 mt-[1.125rem] 29 | } 30 | 31 | .p-fileupload-file { 32 | @apply flex flex-wrap items-center p-4 border-b border-surface-200 dark:border-surface-700 gap-2 last:border-b-0 33 | } 34 | 35 | .p-fileupload-file-info { 36 | @apply flex flex-col gap-2 37 | } 38 | 39 | .p-fileupload-file-thumbnail { 40 | @apply flex-shrink-0 41 | } 42 | 43 | .p-fileupload-file-actions { 44 | @apply ms-auto 45 | } 46 | 47 | .p-fileupload-highlight { 48 | @apply border border-dashed border-primary 49 | } 50 | 51 | .p-fileupload-advanced .p-message { 52 | @apply mt-0 53 | } 54 | 55 | .p-fileupload-basic { 56 | @apply flex flex-wrap items-center justify-center gap-2 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/primevue/password.css: -------------------------------------------------------------------------------- 1 | @import './inputtext'; 2 | 3 | .p-password { 4 | @apply inline-flex relative 5 | } 6 | 7 | .p-password .p-password-overlay { 8 | @apply min-w-full 9 | } 10 | 11 | .p-password-meter { 12 | @apply h-3 bg-surface-200 dark:bg-surface-700 rounded-md 13 | } 14 | 15 | .p-password-meter-label { 16 | @apply h-full w-0 transition-[width] duration-1000 ease-in-out rounded-md 17 | } 18 | 19 | .p-password-meter-weak { 20 | @apply bg-red-500 dark:bg-red-400 21 | } 22 | 23 | .p-password-meter-medium { 24 | @apply bg-amber-500 dark:bg-amber-400 25 | } 26 | 27 | .p-password-meter-strong { 28 | @apply bg-green-500 dark:bg-green-400 29 | } 30 | 31 | .p-password-fluid { 32 | @apply flex 33 | } 34 | 35 | .p-password-fluid .p-password-input { 36 | @apply w-full 37 | } 38 | 39 | .p-password-input::-ms-reveal, 40 | .p-password-input::-ms-clear { 41 | @apply hidden 42 | } 43 | 44 | .p-password-overlay { 45 | @apply p-3 rounded-md bg-surface-0 dark:bg-surface-900 46 | border border-surface-200 dark:border-surface-700 47 | text-surface-700 dark:text-surface-0 48 | shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)] 49 | } 50 | 51 | .p-password-content { 52 | @apply flex flex-col gap-2 53 | } 54 | 55 | .p-password-toggle-mask-icon { 56 | @apply end-3 text-surface-500 dark:text-surface-400 absolute top-1/2 -mt-2 w-4 h-4 57 | } 58 | 59 | .p-password:has(.p-password-toggle-mask-icon) .p-password-input { 60 | @apply pe-10 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/primevue/divider.css: -------------------------------------------------------------------------------- 1 | .p-divider-horizontal { 2 | @apply flex w-full relative items-center my-4 mx-0 py-0 px-4 3 | before:absolute before:block before:top-1/2 before:start-0 before:w-full 4 | before:border-t before:border-surface-200 dark:before:border-surface-700 5 | } 6 | 7 | .p-divider-horizontal .p-divider-content { 8 | @apply py-0 px-2 9 | } 10 | 11 | .p-divider-vertical { 12 | @apply min-h-full flex relative justify-center my-0 mx-4 py-2 px-0 13 | before:absolute before:block before:top-0 before:start-1/2 before:h-full 14 | before:border-s before:border-surface-200 before:dark:border-surface-700 15 | } 16 | 17 | .p-divider.p-divider-vertical .p-divider-content { 18 | @apply py-2 px-0 19 | } 20 | 21 | .p-divider-content { 22 | @apply z-10 bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 23 | } 24 | 25 | .p-divider-solid.p-divider-horizontal:before { 26 | @apply border-solid 27 | } 28 | 29 | .p-divider-solid.p-divider-vertical:before { 30 | @apply border-solid 31 | } 32 | 33 | .p-divider-dashed.p-divider-horizontal:before { 34 | @apply border-dashed 35 | } 36 | 37 | .p-divider-dashed.p-divider-vertical:before { 38 | @apply border-dashed 39 | } 40 | 41 | .p-divider-dotted.p-divider-horizontal:before { 42 | @apply border-dotted 43 | } 44 | 45 | .p-divider-dotted.p-divider-vertical:before { 46 | @apply border-dotted 47 | } 48 | 49 | .p-divider-left:dir(rtl), 50 | .p-divider-right:dir(rtl) { 51 | @apply flex-row-reverse 52 | } 53 | -------------------------------------------------------------------------------- /src/assets/primevue/metergroup.css: -------------------------------------------------------------------------------- 1 | .p-metergroup { 2 | @apply flex gap-4 3 | } 4 | 5 | .p-metergroup-meters { 6 | @apply flex bg-surface-200 dark:bg-surface-700 rounded-md 7 | } 8 | 9 | .p-metergroup-label-list { 10 | @apply flex flex-wrap m-0 p-0 list-none 11 | } 12 | 13 | .p-metergroup-label { 14 | @apply inline-flex items-center gap-2 15 | } 16 | 17 | .p-metergroup-label-marker { 18 | @apply inline-flex w-2 h-2 rounded-full 19 | } 20 | 21 | .p-metergroup-label-icon { 22 | @apply text-base w-4 h-4 23 | } 24 | 25 | .p-metergroup-horizontal { 26 | @apply flex-col 27 | } 28 | 29 | .p-metergroup-label-list-horizontal { 30 | @apply gap-4 31 | } 32 | 33 | .p-metergroup-horizontal .p-metergroup-meters { 34 | @apply h-2 35 | } 36 | 37 | .p-metergroup-horizontal .p-metergroup-meter:first-of-type { 38 | @apply rounded-s-md 39 | } 40 | 41 | .p-metergroup-horizontal .p-metergroup-meter:last-of-type { 42 | @apply rounded-e-md 43 | } 44 | 45 | .p-metergroup-vertical { 46 | @apply flex-row 47 | } 48 | 49 | .p-metergroup-label-list-vertical { 50 | @apply flex-col gap-2 51 | } 52 | 53 | .p-metergroup-vertical .p-metergroup-meters { 54 | @apply flex-col w-2 h-full 55 | } 56 | 57 | .p-metergroup-vertical .p-metergroup-label-list { 58 | @apply items-start 59 | } 60 | 61 | .p-metergroup-vertical .p-metergroup-meter:first-of-type { 62 | @apply rounded-ss-md rounded-se-md 63 | } 64 | 65 | .p-metergroup-vertical .p-metergroup-meter:last-of-type { 66 | @apply rounded-ee-md rounded-es-md 67 | } 68 | -------------------------------------------------------------------------------- /src/assets/primevue/progressspinner.css: -------------------------------------------------------------------------------- 1 | .p-progressspinner { 2 | @apply relative my-0 mx-auto w-[100px] h-[100px] inline-block 3 | before:block before:pt-[100%] 4 | } 5 | 6 | .p-progressspinner-spin { 7 | @apply h-full origin-center w-full absolute top-0 bottom-0 start-0 end-0 m-auto; 8 | } 9 | 10 | .p-progressspinner-spin { 11 | animation: p-progressspinner-rotate 2s linear infinite; 12 | } 13 | 14 | .p-progressspinner-circle { 15 | stroke-dasharray: 89, 200; 16 | stroke-dashoffset: 0; 17 | stroke: theme(colors.red.500); 18 | stroke-linecap: round; 19 | animation: p-progressspinner-dash 1.5s ease-in-out infinite, p-progressspinner-color 6s ease-in-out infinite; 20 | } 21 | 22 | @keyframes p-progressspinner-rotate { 23 | 100% { 24 | transform: rotate(360deg); 25 | } 26 | } 27 | 28 | @keyframes p-progressspinner-dash { 29 | 0% { 30 | stroke-dasharray: 1, 200; 31 | stroke-dashoffset: 0; 32 | } 33 | 50% { 34 | stroke-dasharray: 89, 200; 35 | stroke-dashoffset: -35px; 36 | } 37 | 100% { 38 | stroke-dasharray: 89, 200; 39 | stroke-dashoffset: -124px; 40 | } 41 | } 42 | 43 | @keyframes p-progressspinner-color { 44 | 100%, 45 | 0% { 46 | stroke: theme(colors.red.500); 47 | } 48 | 40% { 49 | stroke: theme(colors.blue.500); 50 | } 51 | 66% { 52 | stroke: theme(colors.green.500); 53 | } 54 | 80%, 55 | 90% { 56 | stroke: theme(colors.yellow.500); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/assets/primevue/splitter.css: -------------------------------------------------------------------------------- 1 | .p-splitter { 2 | @apply flex flex-wrap 3 | border border-surface-200 dark:border-surface-700 rounded-md 4 | bg-surface-0 dark:bg-surface-900 5 | text-surface-700 dark:text-surface-0 6 | } 7 | 8 | .p-splitter-vertical { 9 | @apply flex-col 10 | } 11 | 12 | .p-splitter-gutter { 13 | @apply flex-grow-0 flex-shrink-0 flex items-center justify-center z-10 bg-surface-200 dark:bg-surface-700 14 | } 15 | 16 | .p-splitter-gutter-handle { 17 | @apply rounded-md bg-transparent 18 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 19 | transition-colors duration-200 20 | } 21 | 22 | .p-splitter-horizontal.p-splitter-resizing { 23 | @apply cursor-col-resize select-none 24 | } 25 | 26 | .p-splitter-vertical.p-splitter-resizing { 27 | @apply cursor-row-resize select-none 28 | } 29 | 30 | .p-splitter-horizontal > .p-splitter-gutter > .p-splitter-gutter-handle { 31 | @apply h-[24px] w-full 32 | } 33 | 34 | .p-splitter-vertical > .p-splitter-gutter > .p-splitter-gutter-handle { 35 | @apply w-[24px] h-full 36 | } 37 | 38 | .p-splitter-horizontal > .p-splitter-gutter { 39 | @apply cursor-col-resize 40 | } 41 | 42 | .p-splitter-vertical > .p-splitter-gutter { 43 | @apply cursor-row-resize 44 | } 45 | 46 | .p-splitterpanel { 47 | @apply flex-grow overflow-hidden 48 | } 49 | 50 | .p-splitterpanel-nested { 51 | @apply flex 52 | } 53 | 54 | .p-splitterpanel .p-splitter { 55 | @apply flex-grow border-none 56 | } -------------------------------------------------------------------------------- /src/services/interception/settings.ts: -------------------------------------------------------------------------------- 1 | import { getSettings, setSettings, useSettings, watchSettings } from '@/utils/settingsUtilities' 2 | 3 | export const name = 'interceptionSettings' 4 | 5 | export const defaultSettings: Settings = { 6 | enabled: true, 7 | domains: [], 8 | updateOnStartup: true, 9 | } 10 | 11 | export type Settings = { 12 | enabled: boolean 13 | domains: DomainSettings[] 14 | updateOnStartup: boolean 15 | } 16 | 17 | export const defaultDomainSettings: DomainSettings = { 18 | id: '', 19 | isActive: true, 20 | domain: '', 21 | pathRegExp: '', 22 | isDefault: false, 23 | showNzbDialog: true, 24 | postDataHandling: 'sendAsURLSearchParams', 25 | fetchOrigin: 'background', 26 | archiveFileExtensions: [], 27 | } 28 | 29 | export type DomainSettings = { 30 | id?: string 31 | isActive: boolean 32 | domain: string 33 | pathRegExp: string 34 | isDefault: boolean 35 | showNzbDialog: boolean 36 | postDataHandling: 'sendAsFormData' | 'sendAsURLSearchParams' 37 | fetchOrigin: 'injection' | 'background' 38 | archiveFileExtensions: string[] 39 | icon?: string 40 | } 41 | 42 | export const use = async () => useSettings({ name, defaults: defaultSettings }) 43 | export const get = async () => getSettings({ name, defaults: defaultSettings }) 44 | export const set = async (newSettings: Settings) => 45 | setSettings({ name, defaults: defaultSettings }, newSettings) 46 | export const watch = (callback: (settings: Settings) => void) => 47 | watchSettings({ name, defaults: defaultSettings }, callback) 48 | -------------------------------------------------------------------------------- /src/entrypoints/interception.content/index.ts: -------------------------------------------------------------------------------- 1 | import { defineContentScript } from '#imports' 2 | import log from '@/services/logger/debugLogger' 3 | import { onMessage, sendMessage } from '@/services/messengers/extensionMessenger' 4 | import { deserializeRequest, getHttpStatusText, serializeResponse } from '@/utils/fetchUtilities' 5 | 6 | export default defineContentScript({ 7 | registration: 'runtime', 8 | main() { 9 | log.initDebugLog('interception-content') 10 | log.info('interception content script loaded successfully') 11 | 12 | setInterval(() => { 13 | log.info('sending heartbeat message to background script') 14 | sendMessage('heartbeat', null) 15 | }, 25000) // every 25 seconds 16 | 17 | onMessage('fetchRequest', async (message) => { 18 | log.info(`fetch request message received`) 19 | try { 20 | const deserializedRequest = deserializeRequest(message.data) 21 | log.info(`fetching ${deserializedRequest.url}`) 22 | const response = await fetch(deserializedRequest) 23 | if (!response.ok) { 24 | throw new Error(`${response.status} - ${getHttpStatusText(response.status)}`) 25 | } 26 | const serializedResponse = await serializeResponse(response) 27 | log.info(`sending serialized response back to background script`) 28 | return serializedResponse 29 | } catch (e) { 30 | const error = e instanceof Error ? e : new Error(String(e)) 31 | log.error('error fetching intercepted request', error) 32 | return error 33 | } 34 | }) 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/assets/primevue/menu.css: -------------------------------------------------------------------------------- 1 | .p-menu { 2 | @apply bg-surface-0 dark:bg-surface-900 3 | text-surface-700 dark:text-surface-0 4 | border border-surface-200 dark:border-surface-700 5 | rounded-md min-w-52 6 | } 7 | 8 | .p-menu-list { 9 | @apply m-0 p-1 list-none outline-none flex flex-col gap-[2px] 10 | } 11 | 12 | .p-menu-item-content { 13 | @apply transition-colors duration-200 rounded-sm text-surface-700 dark:text-surface-0 14 | } 15 | 16 | .p-menu-item-link { 17 | @apply cursor-pointer flex items-center no-underline overflow-hidden relative text-inherit 18 | px-3 py-2 gap-2 select-none outline-none 19 | } 20 | 21 | .p-menu-item-icon { 22 | @apply text-surface-400 dark:text-surface-500 23 | } 24 | 25 | .p-menu-item.p-focus .p-menu-item-content { 26 | @apply bg-surface-100 dark:bg-surface-800 text-surface-800 dark:text-surface-0 27 | } 28 | 29 | .p-menu-item.p-focus .p-menu-item-icon { 30 | @apply text-surface-500 dark:text-surface-400 31 | } 32 | 33 | .p-menu-item:not(.p-disabled) .p-menu-item-content:hover { 34 | @apply bg-surface-100 dark:bg-surface-800 text-surface-800 dark:text-surface-0 35 | } 36 | 37 | .p-menu-item:not(.p-disabled) .p-menu-item-content:hover .p-menu-item-icon { 38 | @apply text-surface-500 dark:text-surface-400 39 | } 40 | 41 | .p-menu-overlay { 42 | @apply shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)] 43 | } 44 | 45 | .p-menu-submenu-label { 46 | @apply bg-transparent px-3 py-2 text-surface-500 dark:text-surface-400 font-semibold 47 | } 48 | 49 | .p-menu-separator { 50 | @apply border-t border-surface-200 dark:border-surface-700 51 | } -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/ArchiveReaderEntry.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | import { describe, expect, test } from 'vitest'; 3 | 4 | import { ArchiveReader } from './ArchiveReader'; 5 | import { libarchiveWasm } from './libarchiveWasm'; 6 | 7 | function verifyArchiveEntries(a: ArchiveReader): void { 8 | const entries: Record[] = []; 9 | a.forEach((entry) => { 10 | const pathname = entry.getPathname(); 11 | const size = entry.getSize(); 12 | // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression 13 | const data = pathname.includes('.md') ? entry.readData() : (entry.skipData() as undefined); 14 | entries.push({ 15 | data: new TextDecoder().decode(data), 16 | encrypted: entry.isEncrypted(), 17 | filetype: entry.getFiletype(), 18 | pathname, 19 | size, 20 | }); 21 | 22 | const atime = entry.getAccessTime(); 23 | const btime = entry.getBirthTime(); 24 | const ctime = entry.getCreationTime(); 25 | const mtime = entry.getModificationTime(); 26 | expect(atime).toBeGreaterThanOrEqual(0); 27 | expect(btime).toBe(0); 28 | expect(ctime).toBe(0); 29 | expect(mtime).toBeGreaterThan(new Date('2020-01-01').getTime()); 30 | }); 31 | expect(entries).toMatchSnapshot(); 32 | } 33 | 34 | describe('ArchiveReaderEntry', () => { 35 | test('verification all methods', async () => { 36 | const data = await readFile('./archives/deflate.zip'); 37 | const mod = await libarchiveWasm(); 38 | const a = new ArchiveReader(mod, new Int8Array(data)); 39 | verifyArchiveEntries(a); 40 | a.free(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/assets/primevue/popover.css: -------------------------------------------------------------------------------- 1 | .p-popover { 2 | @apply mt-[10px] bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 3 | border border-surface-200 dark:border-surface-700 4 | rounded-md shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)] 5 | before:bottom-full before:left-5 before:h-0 before:w-0 before:absolute before:pointer-events-none 6 | before:border-[10px] before:-ms-[10px] before:border-transparent before:border-b-surface-200 dark:before:border-b-surface-700 7 | after:bottom-full after:left-5 after:h-0 after:w-0 after:absolute after:pointer-events-none 8 | after:border-[8px] after:-ms-[8px] after:border-transparent after:border-b-surface-0 dark:after:border-b-surface-900 9 | } 10 | 11 | .p-popover-content { 12 | @apply p-3 13 | } 14 | 15 | .p-popover-flipped { 16 | @apply -mt-[10px] mb-[10px] 17 | } 18 | 19 | .p-popover-enter-from { 20 | @apply opacity-0 scale-y-75 21 | } 22 | 23 | .p-popover-leave-to { 24 | @apply opacity-0 25 | } 26 | 27 | .p-popover-enter-active { 28 | @apply [transition:transform_120ms_cubic-bezier(0,0,0.2,1),opacity_120ms_cubic-bezier(0,0,0.2,1)] 29 | } 30 | 31 | .p-popover-leave-active { 32 | @apply transition-opacity duration-100 ease-linear 33 | } 34 | 35 | .p-popover-flipped:after, 36 | .p-popover-flipped:before { 37 | @apply bottom-auto top-full 38 | } 39 | 40 | .p-popover.p-popover-flipped:after { 41 | @apply border-b-transparent border-t-surface-0 dark:border-t-surface-900 42 | } 43 | 44 | .p-popover.p-popover-flipped:before { 45 | @apply border-b-transparent border-t-surface-200 dark:border-t-surface-700 46 | } 47 | -------------------------------------------------------------------------------- /src/assets/primevue/carousel.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | 3 | .p-carousel { 4 | @apply flex flex-col 5 | } 6 | 7 | .p-carousel-content-container { 8 | @apply flex flex-col overflow-auto 9 | } 10 | 11 | .p-carousel-content { 12 | @apply flex flex-row gap-1 13 | } 14 | 15 | .p-carousel-content:dir(rtl) { 16 | @apply flex-row-reverse 17 | } 18 | 19 | .p-carousel-viewport { 20 | @apply overflow-hidden w-full 21 | } 22 | 23 | .p-carousel-item-list { 24 | @apply flex flex-row 25 | } 26 | 27 | .p-carousel-prev-button, 28 | .p-carousel-next-button { 29 | @apply self-center flex-shrink-0 30 | } 31 | 32 | .p-carousel-indicator-list { 33 | @apply flex flex-row justify-center flex-wrap p-4 gap-2 m-0 list-none 34 | } 35 | 36 | .p-carousel-indicator-list:dir(rtl) { 37 | @apply rtl:flex-row-reverse 38 | } 39 | 40 | .p-carousel-indicator-button { 41 | @apply flex items-center justify-center w-8 h-2 border-none rounded-md p-0 m-0 select-none cursor-pointer transition-colors duration-200 42 | bg-surface-200 hover:bg-surface-300 dark:bg-surface-700 dark:hover:bg-surface-600 43 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 44 | } 45 | 46 | .p-carousel-indicator-active .p-carousel-indicator-button { 47 | @apply bg-primary 48 | } 49 | 50 | .p-carousel-vertical .p-carousel-content { 51 | @apply flex-col 52 | } 53 | 54 | .p-carousel-vertical .p-carousel-item-list { 55 | @apply flex-col h-full 56 | } 57 | 58 | .p-items-hidden .p-carousel-item { 59 | @apply invisible 60 | } 61 | 62 | .p-items-hidden .p-carousel-item.p-carousel-item-active { 63 | @apply visible 64 | } 65 | -------------------------------------------------------------------------------- /src/entrypoints/nzblnk.content/index.ts: -------------------------------------------------------------------------------- 1 | import { defineContentScript } from '#imports' 2 | import log from '@/services/logger/debugLogger' 3 | import { sendMessage } from '@/services/messengers/extensionMessenger' 4 | import { debounceVoid } from '@/utils/generalUtilities' 5 | 6 | export default defineContentScript({ 7 | registration: 'manifest', 8 | matches: [''], 9 | runAt: 'document_idle', 10 | allFrames: true, 11 | main() { 12 | sendMessage('getGeneralSettings', true).then(async (settings) => { 13 | if (!(await settings).catchLinks) return 14 | log.initDebugLog('nzblnk-content') 15 | 16 | const handleNzblnkClick = debounceVoid( 17 | (href: string) => { 18 | log.info(`click on nzblnk link detected: ${href}`) 19 | void sendMessage('searchNzbFile', { 20 | nzblnk: href, 21 | source: document.URL, 22 | }) 23 | }, 24 | 500, 25 | true 26 | ) // 500ms debounce window 27 | 28 | document.addEventListener('click', (event: MouseEvent) => { 29 | try { 30 | const anchor = (event.target as HTMLElement)?.closest('a[href^="nzblnk"]') as HTMLAnchorElement | null 31 | if (anchor) { 32 | const href = anchor.getAttribute('href') 33 | if (href) { 34 | event.preventDefault() 35 | handleNzblnkClick(href) 36 | } 37 | } 38 | } catch (e) { 39 | const error = e instanceof Error ? e : new Error(String(e)) 40 | log.error('error while processing click event', error) 41 | } 42 | }) 43 | log.info('nzblnk content script loaded successfully') 44 | }) 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /src/assets/primevue/colorpicker.css: -------------------------------------------------------------------------------- 1 | .p-colorpicker { 2 | @apply inline-block relative; 3 | } 4 | 5 | .p-colorpicker-dragging { 6 | @apply cursor-pointer 7 | } 8 | 9 | .p-colorpicker-preview { 10 | @apply w-6 h-6 p-0 border-none rounded-md transition-colors duration-200 cursor-pointer disabled:cursor-auto 11 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 12 | } 13 | 14 | .p-colorpicker-panel { 15 | @apply bg-surface-800 dark:bg-surface-900 16 | border border-surface-900 dark:border-surface-700 17 | rounded-md shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)] 18 | w-[193px] h-[166px] absolute top-0 start-0 19 | } 20 | 21 | .p-colorpicker-panel-inline { 22 | @apply static shadow-none 23 | } 24 | 25 | .p-colorpicker-content { 26 | @apply relative; 27 | } 28 | 29 | .p-colorpicker-color-selector { 30 | @apply w-[150px] h-[150px] top-[8px] start-[8px] absolute 31 | } 32 | 33 | .p-colorpicker-color-background { 34 | @apply w-full h-full bg-[linear-gradient(to_top,#000_0%,rgba(0,0,0,0)_100%),linear-gradient(to_right,#fff_0%,rgba(255,255,255,0)_100%)]; 35 | } 36 | 37 | .p-colorpicker-color-handle { 38 | @apply absolute top-0 start-[150px] rounded-full w-[10px] h-[10px] border border-surface-0 -mt-[5px] me-0 mb-0 -ms-[5px] cursor-pointer opacity-85 39 | } 40 | 41 | .p-colorpicker-hue { 42 | @apply w-[17px] h-[150px] top-[8px] start-[167px] absolute opacity-85 bg-[linear-gradient(0deg,red_0,#ff0_17%,#0f0_33%,#0ff_50%,#00f_67%,#f0f_83%,red)]; 43 | } 44 | 45 | .p-colorpicker-hue-handle { 46 | @apply absolute top-[150px] start-0 w-[21px] -ms-[2px] -mt-[5px] h-[10px] border-2 opacity-85 border-surface-0 cursor-pointer 47 | } 48 | -------------------------------------------------------------------------------- /src/services/lists/functions.ts: -------------------------------------------------------------------------------- 1 | import log from '@/services/logger/debugLogger' 2 | import { useFetch } from '@/utils/fetchUtilities' 3 | 4 | export const fetchAndValidateList = async ( 5 | listname: string, 6 | url: string, 7 | expectedVersion: number, 8 | sortKey: keyof T, 9 | defaultList: { version: number; data: T[] }, 10 | defaultKeys: (keyof T)[] = [] 11 | ): Promise => { 12 | const fetchList = async (url: string): Promise<{ version: number; data: T[] }> => { 13 | const response = await useFetch(url) 14 | return await response.json() 15 | } 16 | 17 | const filterKeys = (list: T[]): T[] => { 18 | if (defaultKeys.length === 0) { 19 | return list 20 | } 21 | return list.map((item: T) => { 22 | const filteredItem = {} as T 23 | for (const key of defaultKeys) { 24 | if (item[key] !== undefined) { 25 | filteredItem[key] = item[key] 26 | } 27 | } 28 | return filteredItem 29 | }) 30 | } 31 | 32 | const sortList = (data: T[], key: keyof T): T[] => { 33 | return data.sort((a, b) => { 34 | const aKey = a[key] 35 | const bKey = b[key] 36 | return typeof aKey === 'string' && typeof bKey === 'string' ? aKey.localeCompare(bKey) : 0 37 | }) 38 | } 39 | 40 | try { 41 | const json = await fetchList(url) 42 | if (json.version !== expectedVersion) { 43 | throw new Error(`Version mismatch: expected ${expectedVersion}, got ${json.version}`) 44 | } 45 | return sortList(filterKeys(json.data), sortKey) 46 | } catch (e) { 47 | const error = e instanceof Error ? e : new Error(String(e)) 48 | log.warn(`Error loading the ${listname} from URL: ${error.message}`) 49 | return sortList(filterKeys(defaultList.data), sortKey) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master", 4 | { 5 | "name": "beta", 6 | "prerelease": true 7 | }, 8 | { 9 | "name": "rc", 10 | "prerelease": true 11 | } 12 | ], 13 | "plugins": [ 14 | "@semantic-release/commit-analyzer", 15 | "@semantic-release/release-notes-generator", 16 | [ 17 | "@semantic-release/changelog", 18 | { 19 | "changelogFile": "CHANGELOG.md", 20 | "branches": [ 21 | "master" 22 | ] 23 | } 24 | ], 25 | [ 26 | "@semantic-release/npm", 27 | { 28 | "npmPublish": false 29 | } 30 | ], 31 | [ 32 | "@semantic-release/git", 33 | { 34 | "assets": [ 35 | "CHANGELOG.md", 36 | "package.json", 37 | "package-lock.json" 38 | ], 39 | "message": "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}", 40 | "branches": [ 41 | "master" 42 | ] 43 | } 44 | ], 45 | [ 46 | "@semantic-release/git", 47 | { 48 | "assets": [ 49 | "package.json", 50 | "package-lock.json" 51 | ], 52 | "message": "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}", 53 | "branches": [ 54 | "beta", 55 | "rc" 56 | ] 57 | } 58 | ], 59 | [ 60 | "@semantic-release/exec", 61 | { 62 | "prepareCmd": "npm run zip && npm run zip:firefox" 63 | } 64 | ], 65 | [ 66 | "@semantic-release/github", 67 | { 68 | "releaseNameTemplate": "NZBDonkey v${nextRelease.version}", 69 | "assets": [ 70 | { 71 | "path": "dist/nzbdonkey-*.zip" 72 | } 73 | ] 74 | } 75 | ] 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /src/assets/primevue/image.css: -------------------------------------------------------------------------------- 1 | .p-image-mask { 2 | @apply flex items-center justify-center 3 | } 4 | 5 | .p-image-preview { 6 | @apply relative inline-flex leading-none 7 | } 8 | 9 | .p-image-preview-mask { 10 | @apply absolute start-0 top-0 w-full h-full flex items-center justify-center opacity-0 11 | border-none p-0 cursor-pointer bg-transparent text-surface-200 transition-all duration-200 12 | } 13 | 14 | .p-image-preview:hover > .p-image-preview-mask { 15 | @apply opacity-100 cursor-pointer bg-black/40 dark:bg-black/60 16 | } 17 | 18 | .p-image-preview-icon { 19 | @apply text-2xl w-6 h-6 20 | } 21 | 22 | .p-image-toolbar { 23 | @apply absolute top-4 end-4 start-auto bottom-auto flex z-10 p-2 bg-white/10 border border-white/20 rounded-3xl gap-2; 24 | } 25 | 26 | .p-image-action { 27 | @apply inline-flex justify-center items-center text-surface-50 bg-transparent w-12 h-12 m-0 p-0 28 | border-none cursor-pointer select-none rounded-full transition-colors duration-200 29 | hover:text-surface-0 hover:bg-white/10 30 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 31 | } 32 | 33 | .p-image-action .p-icon { 34 | @apply text-2xl h-6 w-6 35 | } 36 | 37 | .p-image-action.p-disabled { 38 | @apply pointer-events-auto 39 | } 40 | 41 | .p-image-original { 42 | @apply transition-transform duration-150 max-w-[100vw] max-h-[100vh] 43 | } 44 | 45 | .p-image-original-enter-active { 46 | @apply transition-all duration-150 ease-[cubic-bezier(0,0,0.2,1)] 47 | } 48 | 49 | .p-image-original-leave-active { 50 | @apply transition-all duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] 51 | } 52 | 53 | .p-image-original-enter-from, 54 | .p-image-original-leave-to { 55 | @apply opacity-0 scale-75 56 | } 57 | -------------------------------------------------------------------------------- /src/assets/primevue/paginator.css: -------------------------------------------------------------------------------- 1 | .p-paginator { 2 | @apply flex items-center justify-center flex-wrap py-2 px-4 rounded-md gap-1 3 | bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 4 | } 5 | 6 | .p-paginator-content { 7 | @apply flex items-center justify-center flex-wrap gap-1 8 | } 9 | 10 | .p-paginator-content-start { 11 | @apply ms-auto 12 | } 13 | 14 | .p-paginator-content-end { 15 | @apply me-auto 16 | } 17 | 18 | .p-paginator-page, 19 | .p-paginator-next, 20 | .p-paginator-last, 21 | .p-paginator-first, 22 | .p-paginator-prev { 23 | @apply cursor-pointer inline-flex items-center justify-center leading-none overflow-hidden relative 24 | bg-transparent border-none min-w-10 h-10 transition-colors duration-200 rounded-full p-0 m-0 25 | text-surface-500 dark:text-surface-400 26 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 27 | } 28 | 29 | .p-paginator-page:not(.p-disabled):not(.p-paginator-page-selected):hover, 30 | .p-paginator-first:not(.p-disabled):hover, 31 | .p-paginator-prev:not(.p-disabled):hover, 32 | .p-paginator-next:not(.p-disabled):hover, 33 | .p-paginator-last:not(.p-disabled):hover { 34 | @apply bg-surface-100 text-surface-600 dark:bg-surface-800 dark:text-surface-300 35 | } 36 | 37 | .p-paginator-page.p-paginator-page-selected { 38 | @apply bg-highlight 39 | } 40 | 41 | .p-paginator-current { 42 | @apply text-surface-500 dark:text-surface-400 43 | } 44 | 45 | .p-paginator-pages { 46 | @apply flex items-center gap-1 47 | } 48 | 49 | .p-paginator-jtp-input .p-inputtext { 50 | @apply max-w-10 51 | } 52 | 53 | .p-paginator-first:dir(rtl), 54 | .p-paginator-prev:dir(rtl), 55 | .p-paginator-next:dir(rtl), 56 | .p-paginator-last:dir(rtl) { 57 | @apply rotate-180 58 | } 59 | -------------------------------------------------------------------------------- /src/services/targets/settings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as DownloadSettings } from './download' 2 | import { Settings as JDownloaderSettings } from './jdownloader' 3 | import { Settings as NzbgetSettings } from './nzbget' 4 | import { Settings as PremiumizeSettings } from './premiumize' 5 | import { Settings as SabnzbdSettings } from './sabnzbd' 6 | import { Settings as SynologySettings } from './synology' 7 | import { Settings as TorboxSettings } from './torbox' 8 | 9 | import { CategoriesSettings } from '@/services/categories' 10 | import { getSettings, setSettings, useSettings, watchSettings } from '@/utils/settingsUtilities' 11 | 12 | const name = 'targetSettings' 13 | 14 | export const defaultSettings: Settings = { 15 | allowMultipleTargets: false, 16 | targets: [], 17 | } 18 | 19 | export type Settings = { 20 | allowMultipleTargets: boolean 21 | targets: TargetSettings[] 22 | } 23 | 24 | export type TargetSettings = { 25 | type: TargetType 26 | name: string 27 | isActive: boolean 28 | settings: 29 | | DownloadSettings 30 | | JDownloaderSettings 31 | | NzbgetSettings 32 | | PremiumizeSettings 33 | | SabnzbdSettings 34 | | SynologySettings 35 | | TorboxSettings 36 | categories: CategoriesSettings 37 | } 38 | 39 | export type TargetType = 'download' | 'jdownloader' | 'nzbget' | 'premiumize' | 'sabnzbd' | 'synology' | 'torbox' 40 | 41 | export const use = async () => useSettings({ name, defaults: defaultSettings }) 42 | export const get = async () => getSettings({ name, defaults: defaultSettings }) 43 | export const set = async (newSettings: Settings) => 44 | setSettings({ name, defaults: defaultSettings }, newSettings) 45 | export const watch = (callback: (settings: Settings) => void) => 46 | watchSettings({ name, defaults: defaultSettings }, callback) 47 | -------------------------------------------------------------------------------- /src/components/categories/categoryRegExpDialog.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 37 | {{ item.name }} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/entrypoints/background/settingsUpdate/v1_3_0.ts: -------------------------------------------------------------------------------- 1 | import * as interception from '@/services/interception' 2 | import { updateInterceptionDomainsList } from '@/services/lists' 3 | import * as searchengines from '@/services/searchengines' 4 | 5 | export default async function (): Promise { 6 | await migrateInterceptionSettings() 7 | await migrateSearchEnginesSettings() 8 | } 9 | 10 | async function migrateInterceptionSettings() { 11 | const settings: interception.Settings = await interception.getSettings() 12 | let preSettings = JSON.parse(JSON.stringify(settings)) 13 | console.log('settings pre-update to v1.3.0', preSettings) 14 | settings.domains.forEach((domain) => { 15 | // @ts-expect-error Property does not exist on type 'DomainSettings' 16 | delete domain.allowDownloadInterception 17 | // @ts-expect-error Property does not exist on type 'DomainSettings' 18 | delete domain.dontShowDoubleCountWarning 19 | // @ts-expect-error Property does not exist on type 'DomainSettings' 20 | delete domain.requiresPostDataHandling 21 | // @ts-expect-error Property does not exist on type 'DomainSettings' 22 | delete domain.postDataHandling 23 | domain.id = domain.id || domain.domain 24 | }) 25 | settings.updateOnStartup = settings.updateOnStartup === undefined ? true : settings.updateOnStartup 26 | preSettings = JSON.parse(JSON.stringify(settings)) 27 | console.log('settings mid-update to v1.3.0 (should have Id now)', preSettings) 28 | settings.domains = await updateInterceptionDomainsList(settings.domains) 29 | console.log('settings post-update to v1.3.0', settings) 30 | await interception.saveSettings(settings) 31 | } 32 | 33 | async function migrateSearchEnginesSettings() { 34 | const settings: searchengines.Settings = await searchengines.getSettings() 35 | settings.updateOnStartup = settings.updateOnStartup === undefined ? true : settings.updateOnStartup 36 | await searchengines.saveSettings(settings) 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/primevue/common.css: -------------------------------------------------------------------------------- 1 | .p-connected-overlay-enter-from { 2 | @apply opacity-0 scale-y-75 3 | } 4 | 5 | .p-connected-overlay-leave-to { 6 | @apply opacity-0 7 | } 8 | 9 | .p-connected-overlay-enter-active { 10 | @apply transition-[opacity,transform] duration-150 ease-[cubic-bezier(0,0,0.2,1)] 11 | } 12 | 13 | .p-connected-overlay-leave-active { 14 | @apply transition-opacity duration-100 ease-linear 15 | } 16 | 17 | .p-toggleable-content-enter-from, 18 | .p-toggleable-content-leave-to { 19 | @apply max-h-0 20 | } 21 | 22 | .p-toggleable-content-enter-to, 23 | .p-toggleable-content-leave-from { 24 | @apply max-h-[1000px] 25 | } 26 | 27 | .p-toggleable-content-leave-active { 28 | @apply overflow-hidden transition-[max-height] animate-duration-[450ms] ease-[cubic-bezier(0,1,0,1)]; 29 | } 30 | 31 | .p-toggleable-content-enter-active { 32 | @apply overflow-hidden transition-[max-height] duration-1000 ease-in-out 33 | } 34 | 35 | .p-disabled, 36 | .p-disabled * { 37 | @apply cursor-default pointer-events-none select-none 38 | } 39 | 40 | .p-disabled, 41 | .p-component:disabled { 42 | @apply opacity-60 43 | } 44 | 45 | .pi { 46 | @apply text-base 47 | } 48 | 49 | .p-icon { 50 | @apply w-4 h-4 51 | } 52 | 53 | .p-overlay-mask { 54 | @apply bg-black/50 text-surface-200 transition-colors duration-150 fixed top-0 start-0 w-full h-full 55 | } 56 | 57 | .p-overlay-mask-enter { 58 | animation: p-overlay-mask-enter-animation 150ms forwards; 59 | } 60 | 61 | .p-overlay-mask-leave { 62 | animation: p-overlay-mask-leave-animation 150ms forwards; 63 | } 64 | 65 | @keyframes p-overlay-mask-enter-animation { 66 | from { 67 | background: transparent; 68 | } 69 | to { 70 | background: rgba(0,0,0,0.5); 71 | } 72 | } 73 | 74 | @keyframes p-overlay-mask-leave-animation { 75 | from { 76 | background: rgba(0,0,0,0.5); 77 | } 78 | to { 79 | background: transparent; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /wxt.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePluginMdToHTML } from 'vite-plugin-md-to-html' 2 | import { defineConfig, type ConfigEnv } from 'wxt' 3 | 4 | import toUtf8 from './scripts/vite-plugin-to-utf8' 5 | 6 | import type { Browser } from '#imports' 7 | 8 | const getManifest: (env: ConfigEnv) => Partial = (env) => { 9 | const manifest: Partial = { 10 | name: '__MSG_extension_name__', 11 | description: '__MSG_extension_description__', 12 | default_locale: 'en', 13 | permissions: [ 14 | 'storage', 15 | 'scripting', 16 | 'notifications', 17 | 'downloads', 18 | 'contextMenus', 19 | 'clipboardWrite', 20 | 'webRequest', 21 | 'declarativeNetRequestWithHostAccess', 22 | ], 23 | host_permissions: [''], 24 | web_accessible_resources: [ 25 | { 26 | resources: ['/libarchive.wasm', '/icon/*', '/img/*', '/assets/style.css'], 27 | matches: [''], 28 | }, 29 | ], 30 | content_security_policy: { 31 | extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';", 32 | }, 33 | } 34 | if (env.browser === 'firefox') { 35 | manifest.browser_specific_settings = { 36 | gecko: { 37 | id: '{dd77cf0b-b93f-4e9f-8006-b642c02219db}', 38 | }, 39 | } 40 | } 41 | return manifest 42 | } 43 | 44 | // See https://wxt.dev/api/config.html 45 | export default defineConfig({ 46 | srcDir: 'src', 47 | outDir: 'dist', 48 | publicDir: 'src/public', 49 | modulesDir: 'src/modules', 50 | modules: ['@wxt-dev/module-vue', '@wxt-dev/i18n/module'], 51 | imports: false, 52 | manifest: getManifest, 53 | vite: () => ({ 54 | plugins: [vitePluginMdToHTML(), toUtf8()], 55 | }), 56 | zip: { 57 | excludeSources: ['icons/**', 'screenshots/**', '*.yml'], 58 | artifactTemplate: '{{name}}-v{{packageVersion}}-{{browser}}.zip', 59 | sourcesTemplate: '{{name}}-v{{packageVersion}}-sources.zip', 60 | }, 61 | }) 62 | -------------------------------------------------------------------------------- /src/entrypoints/background/settingsUpdate/v1_4_3.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DomainSettings, 3 | getSettings as getInterceptionSettings, 4 | saveSettings as saveInterceptionSettings, 5 | } from '@/services/interception' 6 | 7 | export default async function (): Promise { 8 | await migrateInterceptionSettings() 9 | } 10 | 11 | async function migrateInterceptionSettings() { 12 | const settings = await getInterceptionSettings() 13 | const preSettings = JSON.parse(JSON.stringify(settings)) 14 | console.log('settings pre-update to v1.4.3', preSettings) 15 | 16 | // Fast check: if all domain names are unique, skip processing 17 | const domainNames = settings.domains.map((d) => d.domain) 18 | const uniqueDomainNames = new Set(domainNames) 19 | if (domainNames.length === uniqueDomainNames.size) { 20 | // No duplicates found 21 | return 22 | } 23 | 24 | // Group domains by domain name 25 | const domainMap = new Map() 26 | 27 | // Iterate through domains and keep the appropriate one 28 | for (const domain of settings.domains) { 29 | const existing = domainMap.get(domain.domain) 30 | 31 | if (!existing) { 32 | // No duplicate yet, add it 33 | domainMap.set(domain.domain, domain) 34 | } else { 35 | // Duplicate found - keep active over inactive, or the later one if same status 36 | if (domain.isActive && !existing.isActive) { 37 | // New one is active, existing is not - replace 38 | domainMap.set(domain.domain, domain) 39 | } else if (domain.isActive === existing.isActive) { 40 | // Same active status - keep the later one (current iteration) 41 | domainMap.set(domain.domain, domain) 42 | } 43 | // If existing is active and new one is not, keep existing (do nothing) 44 | } 45 | } 46 | 47 | // Update settings with deduplicated domains 48 | settings.domains = Array.from(domainMap.values()) 49 | console.log('settings post-update to v1.4.3', settings) 50 | await saveInterceptionSettings(settings) 51 | } 52 | -------------------------------------------------------------------------------- /src/services/targets/index.ts: -------------------------------------------------------------------------------- 1 | import { TargetSettings, Settings as TargetsSettings, TargetType } from './settings' 2 | 3 | import { NZBFileObject } from '@/services/nzbfile' 4 | 5 | export type Target = { 6 | type: string 7 | name: string 8 | description: string 9 | canHaveCategories: boolean 10 | hasTargetCategories: boolean 11 | hasConnectionTest: boolean 12 | hasAdvancedSettings: boolean 13 | settings: TargetsSettings 14 | functions: Functions 15 | } 16 | 17 | type Functions = { 18 | push: (nzbFile: NZBFileObject, targetsettings: TargetSettings) => Promise 19 | testConnection?: (targetsettings: TargetSettings) => Promise 20 | getCategories?: (targetsettings: TargetSettings) => Promise 21 | } 22 | 23 | export { type Settings as DownloadTargetSettings } from './download' 24 | export { type Settings as JdownloaderTargetSettings } from './jdownloader' 25 | export { type Settings as NzbgetTargetSettings } from './nzbget' 26 | export { type Settings as PremiumizeTargetSettings } from './premiumize' 27 | export { type Settings as SabnzbdTargetSettings } from './sabnzbd' 28 | export { type Settings as SynologyTargetSettings } from './synology' 29 | export { type Settings as TorboxTargetSettings } from './torbox' 30 | 31 | export const targetList: TargetType[] = [ 32 | 'download', 33 | 'jdownloader', 34 | 'nzbget', 35 | 'premiumize', 36 | 'sabnzbd', 37 | 'synology', 38 | 'torbox', 39 | ] 40 | 41 | export * as download from './download' 42 | export * as jdownloader from './jdownloader' 43 | export * as nzbget from './nzbget' 44 | export * as premiumize from './premiumize' 45 | export * as sabnzbd from './sabnzbd' 46 | export * as synology from './synology' 47 | export * as torbox from './torbox' 48 | 49 | export * from './functions' 50 | export { 51 | defaultSettings, 52 | get as getSettings, 53 | set as saveSettings, 54 | use as useSettings, 55 | watch as watchSettings, 56 | type TargetSettings, 57 | type Settings as TargetsSettings, 58 | type TargetType, 59 | } from './settings' 60 | -------------------------------------------------------------------------------- /src/assets/primevue/progressbar.css: -------------------------------------------------------------------------------- 1 | .p-progressbar { 2 | @apply relative overflow-hidden h-5 bg-surface-200 dark:bg-surface-700 rounded-md 3 | } 4 | 5 | .p-progressbar-value { 6 | @apply m-0 bg-primary 7 | } 8 | 9 | .p-progressbar-label { 10 | @apply text-primary-contrast text-xs font-semibold 11 | } 12 | 13 | .p-progressbar-determinate .p-progressbar-value { 14 | @apply h-full w-0 absolute flex items-center justify-center overflow-hidden transition-[width] duration-1000 ease-in-out 15 | } 16 | 17 | .p-progressbar-determinate .p-progressbar-label { 18 | @apply inline-flex 19 | } 20 | 21 | .p-progressbar-indeterminate .p-progressbar-value::before { 22 | @apply content-[''] absolute bg-inherit top-0 start-0 bottom-0 will-change-[inset-inline-start,inset-inline-end]; 23 | } 24 | 25 | .p-progressbar-indeterminate .p-progressbar-value::before { 26 | animation: p-progressbar-indeterminate-anim 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 27 | } 28 | 29 | .p-progressbar-indeterminate .p-progressbar-value::after { 30 | @apply content-[''] absolute bg-inherit top-0 start-0 bottom-0 will-change-[inset-inline-start,inset-inline-end]; 31 | 32 | } 33 | 34 | .p-progressbar-indeterminate .p-progressbar-value::after { 35 | animation: p-progressbar-indeterminate-anim-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 36 | animation-delay: 1.15s; 37 | } 38 | 39 | @keyframes p-progressbar-indeterminate-anim { 40 | 0% { 41 | inset-inline-start: -35%; 42 | inset-inline-end: 100%; 43 | } 44 | 60% { 45 | inset-inline-start: 100%; 46 | inset-inline-end: -90%; 47 | } 48 | 100% { 49 | inset-inline-start: 100%; 50 | inset-inline-end: -90%; 51 | } 52 | } 53 | 54 | @keyframes p-progressbar-indeterminate-anim-short { 55 | 0% { 56 | inset-inline-start: -200%; 57 | inset-inline-end: 100%; 58 | } 59 | 60% { 60 | inset-inline-start: 107%; 61 | inset-inline-end: -8%; 62 | } 63 | 100% { 64 | inset-inline-start: 107%; 65 | inset-inline-end: -8%; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/assets/primevue/confirmpopup.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | 3 | .p-confirmpopup { 4 | @apply absolute mt-[10px] top-0 left-0 5 | border border-surface-200 dark:border-surface-700 rounded-md 6 | bg-surface-0 dark:bg-surface-900 7 | text-surface-700 dark:text-surface-0 8 | shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-2px_rgba(0,0,0,0.1)] 9 | before:bottom-full before:left-5 before:h-0 before:w-0 before:absolute before:pointer-events-none 10 | before:border-[10px] before:-ms-[10px] before:border-transparent before:border-b-surface-200 dark:before:border-b-surface-700 11 | after:bottom-full after:left-5 after:h-0 after:w-0 after:absolute after:pointer-events-none 12 | after:border-[8px] after:-ms-[8px] after:border-transparent after:border-b-surface-0 dark:after:border-b-surface-900 13 | } 14 | 15 | .p-confirmpopup-content { 16 | @apply flex items-center p-3 gap-4 17 | } 18 | 19 | .p-confirmpopup-icon { 20 | @apply text-2xl w-6 h-6 text-surface-700 dark:text-surface-0 21 | } 22 | 23 | .p-confirmpopup-footer { 24 | @apply flex justify-end gap-2 pt-0 px-3 pb-3 25 | } 26 | 27 | .p-confirmpopup-footer button { 28 | @apply w-auto 29 | } 30 | 31 | .p-confirmpopup-footer button:last-child { 32 | @apply m-0 33 | } 34 | 35 | .p-confirmpopup-flipped { 36 | @apply -mt-[10px] mb-[10px] 37 | } 38 | 39 | .p-confirmpopup-enter-from { 40 | @apply opacity-0 scale-y-75 41 | } 42 | 43 | .p-confirmpopup-leave-to { 44 | @apply opacity-0 45 | } 46 | 47 | .p-confirmpopup-enter-active { 48 | @apply [transition:transform_120ms_cubic-bezier(0,0,0.2,1),opacity_120ms_cubic-bezier(0,0,0.2,1)] 49 | } 50 | 51 | .p-confirmpopup-leave-active { 52 | @apply transition-opacity duration-100 ease-linear 53 | } 54 | 55 | .p-confirmpopup-flipped:after, 56 | .p-confirmpopup-flipped:before { 57 | @apply bottom-auto top-full 58 | } 59 | 60 | .p-confirmpopup-flipped:after { 61 | @apply border-b-transparent border-t-surface-0 dark:border-t-surface-900 62 | } 63 | 64 | .p-confirmpopup-flipped:before { 65 | @apply border-b-transparent border-t-surface-200 dark:border-t-surface-700 66 | } 67 | -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/ArchiveReaderEntry.ts: -------------------------------------------------------------------------------- 1 | import type { ArchiveReader } from './ArchiveReader'; 2 | 3 | export class ArchiveReaderEntry { 4 | // eslint-disable-next-line @typescript-eslint/parameter-properties 5 | reader: ArchiveReader; 6 | 7 | pointer: number; 8 | 9 | readCalled: boolean; 10 | 11 | constructor(reader: ArchiveReader, ptr: number) { 12 | this.reader = reader; 13 | this.pointer = ptr; 14 | this.readCalled = false; 15 | } 16 | 17 | free(): void { 18 | this.skipData(); 19 | this.reader = null as unknown as ArchiveReader; 20 | this.pointer = null as unknown as number; 21 | } 22 | 23 | readData(): Int8Array | undefined { 24 | if (this.readCalled) throw new Error('It has already been called.'); 25 | 26 | const size = this.getSize(); 27 | if (!size) { 28 | this.skipData(); 29 | return undefined; 30 | } 31 | 32 | this.readCalled = true; 33 | return this.reader.readData(size); 34 | } 35 | 36 | skipData(): void { 37 | if (this.readCalled) return; 38 | this.readCalled = true; 39 | this.reader.skipData(); 40 | } 41 | 42 | getFiletype(): string { 43 | return this.reader.getEntryFiletype(this.pointer); 44 | } 45 | 46 | getPathname(): string { 47 | return this.reader.getEntryPathname(this.pointer); 48 | } 49 | 50 | getSize(): number { 51 | return this.reader.getEntrySize(this.pointer); 52 | } 53 | 54 | getAccessTime(): number { 55 | return this.reader.getEntryAccessTime(this.pointer); 56 | } 57 | 58 | getBirthTime(): number { 59 | return this.reader.getEntryBirthTime(this.pointer); 60 | } 61 | 62 | getCreationTime(): number { 63 | return this.reader.getEntryCreationTime(this.pointer); 64 | } 65 | 66 | getModificationTime(): number { 67 | return this.reader.getEntryModificationTime(this.pointer); 68 | } 69 | 70 | isEncrypted(): boolean { 71 | return this.reader.isEntryEncrypted(this.pointer); 72 | } 73 | 74 | getSymlinkTarget(): string { 75 | return this.reader.getSymlinkTarget(this.pointer); 76 | } 77 | 78 | getHardlinkTarget(): string { 79 | return this.reader.getHardlinkTarget(this.pointer); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/assets/primevue/dock.css: -------------------------------------------------------------------------------- 1 | .p-dock { 2 | @apply absolute z-10 flex justify-center items-center pointer-events-none 3 | } 4 | 5 | .p-dock-list-container { 6 | @apply flex pointer-events-auto bg-white/10 border border-white/10 p-2 rounded-xl 7 | } 8 | 9 | .p-dock-list { 10 | @apply m-0 p-0 list-none flex items-center justify-center outline-none 11 | } 12 | 13 | .p-dock-item { 14 | @apply transition-all duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform p-2 rounded-md 15 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 16 | } 17 | 18 | .p-dock-item-link { 19 | @apply flex flex-col items-center justify-center relative cursor-default w-12 h-12 20 | } 21 | 22 | .p-dock-top { 23 | @apply start-0 top-0 w-full 24 | } 25 | 26 | .p-dock-top .p-dock-item { 27 | @apply origin-[center_top] 28 | } 29 | 30 | .p-dock-bottom { 31 | @apply start-0 bottom-0 w-full 32 | } 33 | 34 | .p-dock-bottom .p-dock-item { 35 | @apply origin-[center_bottom] 36 | } 37 | 38 | .p-dock-right { 39 | @apply end-0 top-0 h-full 40 | } 41 | 42 | .p-dock-right .p-dock-item { 43 | @apply origin-[center_right] 44 | } 45 | 46 | .p-dock-right .p-dock-list { 47 | @apply flex-col 48 | } 49 | 50 | .p-dock-left { 51 | @apply start-0 top-0 h-full 52 | } 53 | 54 | .p-dock-left .p-dock-item { 55 | @apply origin-[center_left] 56 | } 57 | 58 | .p-dock-left .p-dock-list { 59 | @apply flex-col 60 | } 61 | 62 | .p-dock-mobile.p-dock-top .p-dock-list-container, 63 | .p-dock-mobile.p-dock-bottom .p-dock-list-container { 64 | @apply overflow-x-auto w-full 65 | } 66 | 67 | .p-dock-mobile.p-dock-top .p-dock-list-container .p-dock-list, 68 | .p-dock-mobile.p-dock-bottom .p-dock-list-container .p-dock-list { 69 | @apply mt-0 mx-auto 70 | } 71 | 72 | .p-dock-mobile.p-dock-left .p-dock-list-container, 73 | .p-dock-mobile.p-dock-right .p-dock-list-container { 74 | @apply overflow-y-auto h-full 75 | } 76 | 77 | .p-dock-mobile.p-dock-left .p-dock-list-container .p-dock-list, 78 | .p-dock-mobile.p-dock-right .p-dock-list-container .p-dock-list { 79 | @apply mt-auto mx-0 80 | } 81 | 82 | .p-dock-mobile .p-dock-list .p-dock-item { 83 | @apply transform-none m-0 84 | } 85 | -------------------------------------------------------------------------------- /src/components/searchengines/defaultSearchEnginesDialog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 43 | 47 | {{ item.name }} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/primevue/accordion.css: -------------------------------------------------------------------------------- 1 | .p-accordionpanel { 2 | @apply flex flex-col border-b border-surface-200 dark:border-surface-700 3 | } 4 | 5 | .p-accordionheader { 6 | @apply cursor-pointer disabled:cursor-auto flex items-center justify-between p-[1.125rem] font-semibold 7 | bg-surface-0 dark:bg-surface-900 8 | text-surface-500 dark:text-surface-400 9 | transition-colors duration-200 10 | } 11 | 12 | .p-accordionpanel:first-child > .p-accordionheader { 13 | @apply rounded-ss-md rounded-se-md 14 | } 15 | 16 | .p-accordionpanel:last-child > .p-accordionheader { 17 | @apply rounded-ee-md rounded-es-md 18 | } 19 | 20 | .p-accordionpanel:last-child.p-accordionpanel-active > .p-accordionheader { 21 | @apply rounded-ee-md rounded-es-md 22 | } 23 | 24 | .p-accordionheader-toggle-icon { 25 | @apply text-surface-500 dark:text-surface-400 26 | } 27 | 28 | .p-accordionpanel:not(.p-disabled) .p-accordionheader:focus-visible { 29 | @apply outline outline-1 outline-offset-[-1px] outline-primary 30 | } 31 | 32 | .p-accordionpanel:not(.p-accordionpanel-active):not(.p-disabled) > .p-accordionheader:hover { 33 | @apply bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 34 | } 35 | 36 | .p-accordionpanel:not(.p-accordionpanel-active):not(.p-disabled) .p-accordionheader:hover .p-accordionheader-toggle-icon { 37 | @apply text-surface-700 dark:text-surface-0 38 | } 39 | 40 | .p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader { 41 | @apply bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 42 | } 43 | 44 | .p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader .p-accordionheader-toggle-icon { 45 | @apply text-surface-700 dark:text-surface-0; 46 | } 47 | 48 | .p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader:hover { 49 | @apply bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 50 | } 51 | 52 | .p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader:hover .p-accordionheader-toggle-icon { 53 | @apply text-surface-700 dark:text-surface-0; 54 | } 55 | 56 | .p-accordioncontent-content { 57 | @apply bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 pt-0 px-[1.125rem] pb-[1.125rem] 58 | } 59 | -------------------------------------------------------------------------------- /src/components/interception/defaultDomainsDialog.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{ i18n.t('settings.interception.defaultDomainsDialog.noDomains') }} 30 | 31 | 32 | 40 | 44 | {{ item.domain }} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/assets/primevue/inputgroup.css: -------------------------------------------------------------------------------- 1 | .p-inputgroup, 2 | .p-inputgroup .p-floatlabel, 3 | .p-inputgroup .p-iftalabel { 4 | @apply flex items-stretch w-full 5 | } 6 | 7 | .p-inputgroup .p-inputtext, 8 | .p-inputgroup .p-inputwrapper { 9 | @apply flex-auto w-[1%] 10 | } 11 | 12 | .p-inputgroupaddon { 13 | @apply flex items-center justify-center p-2 min-w-10 14 | border-y border-surface-300 dark:border-surface-700 15 | bg-surface-0 dark:bg-surface-950 text-surface-400 16 | } 17 | 18 | .p-inputgroupaddon:first-child, 19 | .p-inputgroupaddon + .p-inputgroupaddon { 20 | @apply border-s 21 | } 22 | 23 | .p-inputgroupaddon:last-child { 24 | @apply border-e 25 | } 26 | 27 | .p-inputgroup > .p-component, 28 | .p-inputgroup > .p-inputwrapper > .p-component, 29 | .p-inputgroup > .p-floatlabel > .p-component, 30 | .p-inputgroup > .p-floatlabel > .p-inputwrapper > .p-component, 31 | .p-inputgroup > .p-iftalabel > .p-component, 32 | .p-inputgroup > .p-iftalabel > .p-inputwrapper > .p-component { 33 | @apply rounded-none m-0 34 | } 35 | 36 | .p-inputgroupaddon:first-child, 37 | .p-inputgroup > .p-component:first-child, 38 | .p-inputgroup > .p-inputwrapper:first-child > .p-component, 39 | .p-inputgroup > .p-floatlabel:first-child > .p-component, 40 | .p-inputgroup > .p-floatlabel:first-child > .p-inputwrapper > .p-component, 41 | .p-inputgroup > .p-iftalabel:first-child > .p-component, 42 | .p-inputgroup > .p-iftalabel:first-child > .p-inputwrapper > .p-component { 43 | @apply rounded-s-md 44 | } 45 | 46 | .p-inputgroupaddon:last-child, 47 | .p-inputgroup > .p-component:last-child, 48 | .p-inputgroup > .p-inputwrapper:last-child > .p-component, 49 | .p-inputgroup > .p-floatlabel:last-child > .p-component, 50 | .p-inputgroup > .p-floatlabel:last-child > .p-inputwrapper > .p-component, 51 | .p-inputgroup > .p-iftalabel:last-child > .p-component, 52 | .p-inputgroup > .p-iftalabel:last-child > .p-inputwrapper > .p-component { 53 | @apply rounded-e-md 54 | } 55 | 56 | .p-inputgroup .p-component:focus, 57 | .p-inputgroup .p-component.p-focus, 58 | .p-inputgroup .p-inputwrapper-focus, 59 | .p-inputgroup .p-component:focus ~ label, 60 | .p-inputgroup .p-component.p-focus ~ label, 61 | .p-inputgroup .p-inputwrapper-focus ~ label { 62 | @apply z-10 63 | } 64 | 65 | .p-inputgroup > .p-button:not(.p-button-icon-only) { 66 | @apply w-auto 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nzbdonkey", 3 | "description": "The ultimate NZB file downloader webextension.", 4 | "private": true, 5 | "version": "1.5.0", 6 | "type": "module", 7 | "eslintIgnore": [ 8 | "src/services/interception/libarchive-wasm/**" 9 | ], 10 | "prettierIgnore": [ 11 | "src/services/interception/libarchive-wasm/**" 12 | ], 13 | "scripts": { 14 | "dev": "wxt", 15 | "dev:firefox": "wxt -b firefox", 16 | "build": "wxt build", 17 | "build:firefox": "wxt build --mv3 -b firefox", 18 | "zip": "wxt zip", 19 | "zip:firefox": "wxt zip --mv3 -b firefox", 20 | "compile": "vue-tsc --noEmit", 21 | "postinstall": "wxt prepare", 22 | "lint": "eslint ./src/**/* ./*.{js,ts,vue} --ext .js,.ts,.vue --fix", 23 | "format": "prettier --write \"./src/**/*.{js,ts,vue,json}\" \"./*.{js,ts,vue,json}\"" 24 | }, 25 | "dependencies": { 26 | "@primeuix/themes": "^1.0.0", 27 | "@primevue/forms": "^4.2.5", 28 | "@vigneshpa/parse-tar": "^1.2.2", 29 | "@vueuse/core": "^12.5.0", 30 | "@webext-core/messaging": "^2.2.0", 31 | "@wxt-dev/i18n": "^0.2.3", 32 | "dexie": "^4.0.11", 33 | "fast-xml-parser": "^4.5.1", 34 | "floating-vue": "^5.2.2", 35 | "jsonpath": "^1.1.1", 36 | "libarchive-wasm": "^1.1.0", 37 | "linkedom": "^0.18.11", 38 | "primeicons": "^7.0.0", 39 | "primevue": "^4.2.5", 40 | "psl": "^1.15.0", 41 | "tailwindcss-primeui": "^0.4.0", 42 | "vue": "^3.5.12" 43 | }, 44 | "devDependencies": { 45 | "@eslint/js": "^9.20.0", 46 | "@types/chrome": "^0.0.280", 47 | "@types/firefox-webext-browser": "^120.0.4", 48 | "@types/jsonpath": "^0.2.4", 49 | "@wxt-dev/module-vue": "^1.0.1", 50 | "autoprefixer": "^10.4.20", 51 | "eslint": "^9.20.0", 52 | "eslint-config-prettier": "^10.1.2", 53 | "eslint-plugin-import": "^2.31.0", 54 | "eslint-plugin-prettier": "^5.2.6", 55 | "eslint-plugin-vue": "^9.32.0", 56 | "globals": "^15.14.0", 57 | "postcss": "^8.5.1", 58 | "postcss-import": "^16.1.0", 59 | "prettier": "^3.5.3", 60 | "sass-embedded": "^1.85.1", 61 | "tailwindcss": "^3.4.17", 62 | "typescript": "^5.8.3", 63 | "typescript-eslint": "^8.23.0", 64 | "vite-plugin-md-to-html": "^0.0.18", 65 | "vite-plugin-plain-text": "^1.4.2", 66 | "vue-tsc": "^2.1.10", 67 | "wxt": "^0.20.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/stringUtilities.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts the filename from a file path. 3 | * @param {string} path - The file path to extract the filename from. 4 | * @return {string} The extracted filename, or an empty string if none is found. 5 | */ 6 | export const getFileNameFromPath = (path: string): string => { 7 | return path.split('\\').pop()?.split('/').pop() ?? '' 8 | } 9 | 10 | /** 11 | * Extracts the basename (filename without extension) from a filename. 12 | * @param {string} filename - The filename to extract the basename from. 13 | * @return {string} The extracted basename, or the original filename if no extension is found. 14 | */ 15 | export const getBasenameFromFilename = (filename: string): string => { 16 | return filename.slice(0, filename.lastIndexOf('.')) 17 | } 18 | 19 | /** 20 | * Extracts the file extension from a filename. 21 | * @param {string} filename - The filename to extract the extension from. 22 | * @return {string} The extracted file extension, or an empty string if none is found. 23 | */ 24 | export const getExtensionFromFilename = (filename: string): string => { 25 | return filename.slice(filename.lastIndexOf('.') + 1) 26 | } 27 | 28 | /** 29 | * Formats a string as an error message by capitalizing the first letter 30 | * and appending an exclamation mark. 31 | * @param {string} input - The input string to format. 32 | * @return {string} The formatted error string, or an empty string if the input is empty. 33 | */ 34 | export const generateErrorString = (input: string): string => { 35 | if (!input) return '' 36 | // Remove punctuation marks at the end 37 | const sanitized = input.replace(/[.!]+$/g, '') 38 | // Capitalize the first character 39 | const capitalized = sanitized.charAt(0).toUpperCase() + sanitized.slice(1) 40 | return `${capitalized}!` 41 | } 42 | 43 | /** 44 | * Encodes a Unicode string into a Base64-encoded string. 45 | * @param {string} string - The input string to encode. 46 | * @return {string} The Base64-encoded string. 47 | * @throws {Error} Throws an error if encoding fails. 48 | */ 49 | export const b64EncodeUnicode = (string: string): string => { 50 | try { 51 | return btoa( 52 | encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, (_match, p1) => String.fromCharCode(parseInt(p1, 16))) 53 | ) 54 | } catch { 55 | throw new Error('failed to encode string to Base64') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/primevue/drawer.css: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | 3 | .p-drawer { 4 | @apply flex flex-col pointer-events-auto relative transition-transform duration-300 5 | border border-surface-200 dark:border-surface-700 6 | bg-surface-0 dark:bg-surface-900 7 | text-surface-700 dark:text-surface-0 8 | shadow-[0_20px_25px_-5px_rgba(0,0,0,0.1),0_8px_10px_-6px_rgba(0,0,0,0.1)] 9 | } 10 | 11 | .p-drawer { 12 | @apply [transform:translate3d(0,0,0)] 13 | } 14 | 15 | .p-drawer-content { 16 | @apply overflow-y-auto flex-grow pt-0 pb-5 px-5 17 | } 18 | 19 | .p-drawer-header { 20 | @apply flex items-center justify-between flex-shrink-0 p-5 21 | } 22 | 23 | .p-drawer-footer { 24 | @apply p-5 25 | } 26 | 27 | .p-drawer-title { 28 | @apply font-semibold text-2xl 29 | } 30 | 31 | .p-drawer-full .p-drawer { 32 | @apply transition-none transform-none w-screen h-screen max-h-full top-0 left-0 33 | } 34 | 35 | .p-drawer-left .p-drawer-enter-from, 36 | .p-drawer-left .p-drawer-leave-to { 37 | @apply -translate-x-full 38 | } 39 | 40 | .p-drawer-right .p-drawer-enter-from, 41 | .p-drawer-right .p-drawer-leave-to { 42 | @apply translate-x-full 43 | } 44 | 45 | .p-drawer-top .p-drawer-enter-from, 46 | .p-drawer-top .p-drawer-leave-to { 47 | @apply -translate-y-full 48 | } 49 | 50 | .p-drawer-bottom .p-drawer-enter-from, 51 | .p-drawer-bottom .p-drawer-leave-to { 52 | @apply translate-y-full 53 | } 54 | 55 | .p-drawer-full .p-drawer-enter-from, 56 | .p-drawer-full .p-drawer-leave-to { 57 | @apply opacity-0 58 | } 59 | 60 | .p-drawer-full .p-drawer-enter-active, 61 | .p-drawer-full .p-drawer-leave-active { 62 | @apply transition-opacity animate-duration-400 ease-[cubic-bezier(0.25,0.8,0.25,1)] 63 | } 64 | 65 | .p-drawer-left .p-drawer { 66 | @apply w-80 h-full border-r 67 | } 68 | 69 | .p-drawer-right .p-drawer { 70 | @apply w-80 h-full border-s 71 | } 72 | 73 | .p-drawer-top .p-drawer { 74 | @apply h-40 w-full border-b 75 | } 76 | 77 | .p-drawer-bottom .p-drawer { 78 | @apply h-40 w-full border-t 79 | } 80 | 81 | .p-drawer-left .p-drawer-content, 82 | .p-drawer-right .p-drawer-content, 83 | .p-drawer-top .p-drawer-content, 84 | .p-drawer-bottom .p-drawer-content { 85 | @apply w-full h-full 86 | } 87 | 88 | .p-drawer-open { 89 | @apply flex 90 | } 91 | 92 | .p-overlay-mask:dir(rtl) { 93 | @apply flex-row-reverse; 94 | } 95 | -------------------------------------------------------------------------------- /src/assets/primevue/toggleswitch.css: -------------------------------------------------------------------------------- 1 | .p-toggleswitch { 2 | @apply inline-block w-10 h-6 3 | } 4 | 5 | .p-toggleswitch-input { 6 | @apply cursor-pointer disabled:cursor-default appearance-none absolute top-0 start-0 w-full h-full m-0 p-0 opacity-0 z-10 rounded-[30px] 7 | } 8 | 9 | .p-toggleswitch-slider { 10 | @apply inline-block w-full h-full rounded-[30px] shadow-[0_1px_2px_0_rgba(18,18,23,0.05)] 11 | bg-surface-300 dark:bg-surface-700 12 | border border-transparent 13 | transition-colors duration-200 14 | } 15 | 16 | .p-toggleswitch-handle { 17 | @apply absolute top-1/2 flex justify-center items-center 18 | bg-surface-0 dark:bg-surface-400 19 | text-surface-500 dark:text-surface-900 20 | w-4 h-4 start-1 -mt-2 rounded-full 21 | transition-[background,color,left] duration-200 22 | } 23 | 24 | .p-toggleswitch.p-toggleswitch-checked .p-toggleswitch-slider { 25 | @apply bg-primary 26 | } 27 | 28 | .p-toggleswitch.p-toggleswitch-checked .p-toggleswitch-handle { 29 | @apply bg-surface-0 dark:bg-surface-900 text-primary start-5 30 | } 31 | 32 | .p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:hover) .p-toggleswitch-slider { 33 | @apply bg-surface-400 dark:bg-surface-600 34 | } 35 | 36 | .p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:hover) .p-toggleswitch-handle { 37 | @apply bg-surface-0 dark:bg-surface-300 text-surface-700 dark:text-surface-800 38 | } 39 | 40 | .p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:hover).p-toggleswitch-checked .p-toggleswitch-slider { 41 | @apply bg-primary-emphasis 42 | } 43 | 44 | .p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:hover).p-toggleswitch-checked .p-toggleswitch-handle { 45 | @apply bg-surface-0 dark:bg-surface-900 text-primary-emphasis 46 | } 47 | 48 | .p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:focus-visible) .p-toggleswitch-slider { 49 | @apply outline outline-1 outline-offset-2 outline-primary 50 | } 51 | 52 | .p-toggleswitch.p-invalid > .p-toggleswitch-slider { 53 | @apply border-red-400 dark:border-red-300 54 | } 55 | 56 | .p-toggleswitch.p-disabled { 57 | @apply opacity-100 58 | } 59 | 60 | .p-toggleswitch.p-disabled .p-toggleswitch-slider { 61 | @apply bg-surface-200 dark:bg-surface-600 62 | } 63 | 64 | .p-toggleswitch.p-disabled .p-toggleswitch-handle { 65 | @apply bg-surface-700 dark:bg-surface-900 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/primevue/listbox.css: -------------------------------------------------------------------------------- 1 | @import './inputtext'; 2 | @import './iconfield'; 3 | 4 | .p-listbox { 5 | @apply bg-surface-0 dark:bg-surface-950 text-surface-700 dark:text-surface-0 6 | border border-surface-300 dark:border-surface-700 rounded-md 7 | shadow-[0_1px_2px_0_rgba(18,18,23,0.05)] 8 | transition-colors duration-200 9 | } 10 | 11 | .p-listbox.p-focus { 12 | @apply border-primary 13 | } 14 | 15 | .p-listbox.p-disabled { 16 | @apply bg-surface-200 text-surface-500 dark:bg-surface-700 dark:text-surface-400 opacity-100 cursor-default 17 | } 18 | 19 | .p-listbox.p-disabled .p-listbox-option { 20 | @apply text-surface-500 dark:text-surface-400 cursor-default 21 | } 22 | 23 | .p-listbox.p-invalid { 24 | @apply border-red-400 dark:border-red-300 25 | } 26 | 27 | .p-listbox-header { 28 | @apply pt-2 pb-1 px-4 29 | } 30 | 31 | .p-listbox-filter { 32 | @apply w-full 33 | } 34 | 35 | .p-listbox-list-container { 36 | @apply overflow-auto 37 | } 38 | 39 | .p-listbox-list { 40 | @apply list-none m-0 p-1 outline-none flex flex-col gap-[2px] 41 | } 42 | 43 | .p-listbox-option { 44 | @apply flex items-center cursor-pointer relative overflow-hidden px-3 py-2 border-none rounded-sm 45 | text-surface-700 dark:text-surface-0 46 | transition-colors duration-200 47 | } 48 | 49 | .p-listbox-striped li:nth-child(even of .p-listbox-option) { 50 | @apply bg-surface-50 dark:bg-surface-900 51 | } 52 | 53 | .p-listbox .p-listbox-list .p-listbox-option.p-listbox-option-selected { 54 | @apply bg-highlight 55 | } 56 | 57 | .p-listbox:not(.p-disabled) .p-listbox-option.p-listbox-option-selected.p-focus { 58 | @apply bg-highlight-emphasis 59 | } 60 | 61 | .p-listbox:not(.p-disabled) .p-listbox-option:not(.p-listbox-option-selected):not(.p-disabled).p-focus { 62 | @apply bg-surface-100 dark:bg-surface-800 text-surface-800 dark:text-surface-0 63 | } 64 | 65 | .p-listbox:not(.p-disabled) .p-listbox-option:not(.p-listbox-option-selected):not(.p-disabled):hover { 66 | @apply bg-surface-100 dark:bg-surface-800 text-surface-800 dark:text-surface-0 67 | } 68 | 69 | .p-listbox-option-check-icon { 70 | @apply relative -ms-[0.375rem] me-[0.375rem] text-surface-700 dark:text-surface-0 71 | } 72 | 73 | .p-listbox-option-group { 74 | @apply m-0 px-3 py-2 text-surface-500 dark:text-surface-400 font-semibold 75 | } 76 | 77 | .p-listbox-empty-message { 78 | @apply px-3 py-2 79 | } -------------------------------------------------------------------------------- /src/components/targets/downloadSettings.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | {{ 21 | i18n.t('settings.nzbFileTargets.download.path') 22 | }} 23 | 33 | 41 | {{ 42 | $field.error?.message 43 | }} 44 | 45 | 46 | 47 | {{ 48 | i18n.t('settings.nzbFileTargets.download.saveAs.title') 49 | }} 50 | 55 | 56 | 57 | 58 | {{ i18n.t('settings.nzbFileTargets.download.saveAs.description') }} 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/services/interception/libarchive-wasm/ArchiveReader.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | import { describe, expect, test } from 'vitest'; 3 | 4 | import { ArchiveReader } from './ArchiveReader'; 5 | import { libarchiveWasm } from './libarchiveWasm'; 6 | 7 | function verifyArchiveEntries(a: ArchiveReader): void { 8 | const entries: Record[] = []; 9 | for (;;) { 10 | const entryPointer = a.nextEntryPointer(); 11 | if (entryPointer === 0) break; 12 | const pathname = a.getEntryPathname(entryPointer); 13 | const size = a.getEntrySize(entryPointer); 14 | // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression 15 | const data = pathname.includes('.md') ? a.readData(size) : (a.skipData() as undefined); 16 | entries.push({ 17 | data: new TextDecoder().decode(data), 18 | encrypted: a.isEntryEncrypted(entryPointer), 19 | filetype: a.getEntryFiletype(entryPointer), 20 | pathname, 21 | size, 22 | }); 23 | 24 | const atime = a.getEntryAccessTime(entryPointer); 25 | const btime = a.getEntryBirthTime(entryPointer); 26 | const ctime = a.getEntryCreationTime(entryPointer); 27 | const mtime = a.getEntryModificationTime(entryPointer); 28 | expect(atime).toBeGreaterThanOrEqual(0); 29 | expect(btime).toBe(0); 30 | expect(ctime).toBe(0); 31 | expect(mtime).toBeGreaterThan(new Date('2020-01-01').getTime()); 32 | } 33 | expect(entries).toMatchSnapshot(); 34 | } 35 | 36 | function testArchive(name: string, passphrase?: string): void { 37 | test(name, async () => { 38 | const data = await readFile(`./archives/${name}`); 39 | const mod = await libarchiveWasm(); 40 | const a = new ArchiveReader(mod, new Int8Array(data), passphrase); 41 | expect(a.hasEncryptedData()).toBe(null); 42 | verifyArchiveEntries(a); 43 | expect(!!a.hasEncryptedData()).toBe(passphrase != null); 44 | a.free(); 45 | }); 46 | } 47 | 48 | describe('ArchiveReader', () => { 49 | testArchive('deflate.zip'); 50 | testArchive('deflate-encrypted.zip', 'Passw0rd!'); 51 | testArchive('store.zip'); 52 | 53 | testArchive('a.tar'); 54 | testArchive('a.tar.bz2'); 55 | testArchive('a.tar.gz'); 56 | testArchive('a.tar.xz'); 57 | 58 | testArchive('bzip2.7z'); 59 | testArchive('lzma.7z'); 60 | testArchive('lzma2.7z'); 61 | 62 | testArchive('v4.rar'); 63 | testArchive('v4-encrypted.rar', 'Passw0rd!'); 64 | testArchive('v5.rar'); 65 | }); 66 | -------------------------------------------------------------------------------- /src/assets/primevue/checkbox.css: -------------------------------------------------------------------------------- 1 | .p-checkbox { 2 | @apply relative inline-flex select-none w-5 h-5 align-bottom 3 | } 4 | 5 | .p-checkbox-input { 6 | @apply cursor-pointer disabled:cursor-default appearance-none absolute start-0 top-0 w-full h-full m-0 p-0 opacity-0 z-10 7 | border border-transparent rounded-sm 8 | } 9 | 10 | .p-checkbox-box { 11 | @apply flex justify-center items-center rounded-sm w-5 h-5 12 | border border-surface-300 dark:border-surface-700 13 | bg-surface-0 dark:bg-surface-950 14 | transition-colors duration-200 15 | shadow-[0_1px_2px_0_rgba(18,18,23,0.05)] 16 | } 17 | 18 | .p-checkbox-icon { 19 | @apply text-surface-700 dark:text-surface-0 20 | text-sm w-[0.875rem] h-[0.875rem] 21 | transition-colors duration-200 22 | } 23 | 24 | .p-checkbox:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-box { 25 | @apply border-surface-400 dark:border-surface-600 26 | } 27 | 28 | .p-checkbox-checked .p-checkbox-box { 29 | @apply border-primary bg-primary 30 | } 31 | 32 | .p-checkbox-checked .p-checkbox-icon { 33 | @apply text-primary-contrast 34 | } 35 | 36 | .p-checkbox-checked:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-box { 37 | @apply bg-primary-emphasis border-primary-emphasis 38 | } 39 | 40 | .p-checkbox-checked:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-icon { 41 | @apply text-primary-contrast 42 | } 43 | 44 | .p-checkbox:not(.p-disabled):has(.p-checkbox-input:focus-visible) .p-checkbox-box { 45 | @apply outline outline-1 outline-offset-2 outline-primary 46 | } 47 | 48 | .p-checkbox.p-invalid > .p-checkbox-box { 49 | @apply border-red-400 dark:border-red-300 50 | } 51 | 52 | .p-checkbox.p-variant-filled .p-checkbox-box { 53 | @apply bg-surface-50 dark:bg-surface-800 54 | } 55 | 56 | .p-checkbox.p-disabled { 57 | @apply opacity-100 58 | } 59 | 60 | .p-checkbox.p-disabled .p-checkbox-box { 61 | @apply bg-surface-200 dark:bg-surface-400 border-surface-300 dark:border-surface-700 62 | } 63 | 64 | .p-checkbox.p-disabled .p-checkbox-box .p-checkbox-icon { 65 | @apply text-surface-700 dark:text-surface-900 66 | } 67 | 68 | .p-checkbox-sm, 69 | .p-checkbox-sm .p-checkbox-box { 70 | @apply w-4 h-4 71 | } 72 | 73 | .p-checkbox-sm .p-checkbox-icon { 74 | @apply w-3 h-3 75 | } 76 | 77 | .p-checkbox-lg, 78 | .p-checkbox-lg .p-checkbox-box { 79 | @apply w-6 h-6 80 | } 81 | 82 | .p-checkbox-lg .p-checkbox-icon { 83 | @apply w-4 h-4 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/primevue/togglebutton.css: -------------------------------------------------------------------------------- 1 | .p-togglebutton { 2 | @apply inline-flex items-center justify-center overflow-hidden relative cursor-pointer select-none 3 | border border-surface-100 dark:border-surface-950 rounded-md 4 | bg-surface-100 dark:bg-surface-950 5 | text-surface-500 dark:text-surface-400 text-base font-medium 6 | px-4 py-2 7 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 8 | disabled:opacity-100 disabled:cursor-default 9 | disabled:bg-surface-200 disabled:border-surface-200 disabled:text-surface-500 10 | disabled:dark:bg-surface-700 disabled:dark:border-surface-700 disabled:dark:text-surface-400 11 | transition-colors duration-300 12 | before:bg-transparent before:absolute before:start-1 before:top-1 before:rounded-md before:w-[calc(100%-0.5rem)] before:h-[calc(100%-0.5rem)] 13 | before:transition-colors before:duration-200 14 | } 15 | 16 | .p-togglebutton-content { 17 | @apply relative inline-flex items-center justify-center gap-2 18 | } 19 | 20 | .p-togglebutton-label, 21 | .p-togglebutton-icon { 22 | @apply relative transition-none 23 | } 24 | 25 | .p-togglebutton.p-togglebutton-checked::before { 26 | @apply bg-surface-0 dark:bg-surface-800 shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02),0px_1px_2px_0px_rgba(0,0,0,0.04)] 27 | } 28 | 29 | .p-togglebutton:not(:disabled):not(.p-togglebutton-checked):hover { 30 | @apply bg-surface-100 dark:bg-surface-950 text-surface-700 dark:text-surface-300 31 | } 32 | 33 | .p-togglebutton.p-togglebutton-checked { 34 | @apply bg-surface-100 dark:bg-surface-950 border-surface-100 dark:border-surface-950 text-surface-900 dark:text-surface-0 35 | } 36 | 37 | .p-togglebutton.p-invalid { 38 | @apply border-red-400 dark:border-red-300 39 | } 40 | 41 | .p-togglebutton-icon { 42 | @apply text-surface-500 dark:text-surface-400 43 | } 44 | 45 | .p-togglebutton:not(:disabled):not(.p-togglebutton-checked):hover .p-togglebutton-icon { 46 | @apply text-surface-700 dark:text-surface-300 47 | } 48 | 49 | .p-togglebutton.p-togglebutton-checked .p-togglebutton-icon { 50 | @apply text-surface-900 dark:text-surface-0 51 | } 52 | 53 | .p-togglebutton:disabled .p-togglebutton-icon { 54 | @apply text-surface-500 dark:text-surface-400 55 | } 56 | 57 | .p-togglebutton-sm { 58 | @apply text-sm px-[0.75rem] py-[0.375rem] 59 | } 60 | 61 | .p-togglebutton-lg { 62 | @apply text-lg px-[1.25rem] py-[0.625rem] 63 | } 64 | -------------------------------------------------------------------------------- /src/components/targets/premiumizeSettings.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | {{ i18n.t('common.settings.apiKey') }} 33 | 43 | 51 | {{ 52 | $field.error?.message 53 | }} 54 | 55 | 56 | 57 | {{ i18n.t('common.settings.timeout.title') }} 58 | 59 | 60 | (connectionSuccessful = value)" 64 | /> 65 | 66 | -------------------------------------------------------------------------------- /src/assets/primevue/tailwind.css: -------------------------------------------------------------------------------- 1 | @import './common'; 2 | 3 | /* Form */ 4 | @import './autocomplete'; 5 | @import './cascadeselect'; 6 | @import './checkbox'; 7 | @import './colorpicker'; 8 | @import './datepicker'; 9 | @import './iconfield'; 10 | @import './iftalabel'; 11 | @import './inputgroup'; 12 | @import './inputnumber'; 13 | @import './inputotp'; 14 | @import './inputtext'; 15 | @import './floatlabel'; 16 | @import './knob'; 17 | @import './listbox'; 18 | @import './multiselect'; 19 | @import './password'; 20 | @import './radiobutton'; 21 | @import './rating'; 22 | @import './select'; 23 | @import './selectbutton'; 24 | @import './slider'; 25 | @import './textarea'; 26 | @import './togglebutton'; 27 | @import './toggleswitch'; 28 | @import './treeselect'; 29 | 30 | /* Button */ 31 | @import './button'; 32 | @import './buttongroup'; 33 | @import './speeddial'; 34 | @import './splitbutton'; 35 | 36 | /* Data */ 37 | @import './datatable'; 38 | @import './dataview'; 39 | @import './paginator'; 40 | @import './picklist'; 41 | @import './orderlist'; 42 | @import './organizationchart'; 43 | @import './timeline'; 44 | @import './tree'; 45 | @import './treetable'; 46 | 47 | /* Overlay */ 48 | @import './confirmdialog'; 49 | @import './confirmpopup'; 50 | @import './dialog'; 51 | @import './drawer'; 52 | @import './popover'; 53 | @import './tooltip'; 54 | 55 | /* Menu */ 56 | @import './breadcrumb'; 57 | @import './contextmenu'; 58 | @import './dock'; 59 | @import './menu'; 60 | @import './menubar'; 61 | @import './megamenu'; 62 | @import './panelmenu'; 63 | @import './tieredmenu'; 64 | 65 | /* Panel */ 66 | @import './accordion'; 67 | @import './card'; 68 | @import './divider'; 69 | @import './fieldset'; 70 | @import './panel'; 71 | @import './scrollpanel'; 72 | @import './splitter'; 73 | @import './stepper'; 74 | @import './tabs'; 75 | @import './toolbar'; 76 | 77 | /* File */ 78 | @import './fileupload'; 79 | 80 | /* Message */ 81 | @import './message'; 82 | @import './toast'; 83 | 84 | /* Media */ 85 | @import './carousel'; 86 | @import './galleria'; 87 | @import './image'; 88 | @import './imagecompare'; 89 | 90 | /* Misc */ 91 | @import './avatar'; 92 | @import './badge'; 93 | @import './blockui'; 94 | @import './chip'; 95 | @import './inplace'; 96 | @import './metergroup'; 97 | @import './overlaybadge'; 98 | @import './progressbar'; 99 | @import './progressspinner'; 100 | @import './ripple'; 101 | @import './scrolltop'; 102 | @import './skeleton'; 103 | @import './tag'; 104 | @import './terminal'; 105 | -------------------------------------------------------------------------------- /src/assets/primevue/tabs.css: -------------------------------------------------------------------------------- 1 | .p-tabs { 2 | @apply flex flex-col 3 | } 4 | 5 | .p-tablist { 6 | @apply flex relative 7 | } 8 | 9 | .p-tabs-scrollable > .p-tablist { 10 | @apply overflow-hidden 11 | } 12 | 13 | .p-tablist-viewport { 14 | @apply overflow-x-auto overflow-y-hidden overscroll-y-contain overscroll-x-auto 15 | } 16 | 17 | .p-tablist-viewport::-webkit-scrollbar { 18 | @apply hidden 19 | } 20 | 21 | .p-tablist-tab-list { 22 | @apply relative flex bg-surface-0 dark:bg-surface-900 border-b border-surface-200 dark:border-surface-700 23 | } 24 | 25 | .p-tablist-content { 26 | @apply flex-grow 27 | } 28 | 29 | .p-tablist-nav-button { 30 | @apply !absolute flex-shrink-0 top-0 z-20 h-full flex items-center justify-center cursor-pointer 31 | bg-surface-0 dark:bg-surface-900 text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-0 w-10 32 | shadow-[0px_0px_10px_50px_rgba(255,255,255,0.6)] dark:shadow-[0px_0px_10px_50px] dark:shadow-surface-900/50 33 | focus-visible:z-10 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-primary 34 | transition-colors duration-200 35 | } 36 | 37 | .p-tablist-prev-button { 38 | @apply start-0 39 | } 40 | 41 | .p-tablist-next-button { 42 | @apply end-0 43 | } 44 | 45 | .p-tablist-prev-button:dir(rtl), 46 | .p-tablist-next-button:dir(rtl) { 47 | @apply rotate-180 48 | } 49 | 50 | .p-tab { 51 | @apply flex-shrink-0 cursor-pointer select-none relative whitespace-nowrap py-4 px-[1.125rem] 52 | border-b border-surface-200 dark:border-surface-700 font-semibold 53 | text-surface-500 dark:text-surface-400 54 | transition-colors duration-200 -mb-px 55 | } 56 | 57 | .p-tab.p-disabled { 58 | @apply cursor-default 59 | } 60 | 61 | .p-tab:not(.p-disabled):focus-visible { 62 | @apply z-10 outline outline-1 outline-offset-[-1px] outline-primary 63 | } 64 | 65 | .p-tab:not(.p-tab-active):not(.p-disabled):hover { 66 | @apply text-surface-700 dark:text-surface-0 67 | } 68 | 69 | .p-tab-active { 70 | @apply border-primary text-primary 71 | } 72 | 73 | .p-tabpanels { 74 | @apply bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-0 75 | pt-[0.875rem] pb-[1.125rem] px-[1.125rem] outline-none 76 | } 77 | 78 | .p-tablist-active-bar { 79 | @apply z-10 block absolute -bottom-px h-px bg-primary transition-[left] duration-200 ease-[cubic-bezier(0.35,0,0.25,1)] 80 | } 81 | 82 | .p-tablist-viewport { 83 | @apply [scrollbar-behavior:smooth] [scrollbar-width:none] 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/primevue/floatlabel.css: -------------------------------------------------------------------------------- 1 | .p-floatlabel { 2 | @apply block relative 3 | } 4 | 5 | .p-floatlabel label { 6 | @apply absolute pointer-events-none top-1/2 -translate-y-1/2 transition-all duration-200 ease-out leading-none font-medium 7 | start-3 text-surface-500 dark:text-surface-400 8 | } 9 | 10 | .p-floatlabel:has(.p-textarea) label { 11 | @apply top-2 translate-y-0 12 | } 13 | 14 | .p-floatlabel:has(.p-inputicon:first-child) label { 15 | @apply start-10 16 | } 17 | 18 | .p-floatlabel:has(.p-invalid) label { 19 | @apply text-red-400 dark:text-red-300 20 | } 21 | 22 | .p-floatlabel:has(input:focus) label, 23 | .p-floatlabel:has(input.p-filled) label, 24 | .p-floatlabel:has(input:-webkit-autofill) label, 25 | .p-floatlabel:has(textarea:focus) label, 26 | .p-floatlabel:has(textarea.p-filled) label, 27 | .p-floatlabel:has(.p-inputwrapper-focus) label, 28 | .p-floatlabel:has(.p-inputwrapper-filled) label { 29 | @apply -top-5 translate-y-0 text-xs font-normal 30 | } 31 | 32 | .p-floatlabel:has(input.p-filled) label, 33 | .p-floatlabel:has(textarea.p-filled) label, 34 | .p-floatlabel:has(.p-inputwrapper-filled) label { 35 | @apply text-surface-500 dark:text-surface-400 36 | } 37 | 38 | .p-floatlabel:has(input:focus) label, 39 | .p-floatlabel:has(input:-webkit-autofill) label, 40 | .p-floatlabel:has(textarea:focus) label , 41 | .p-floatlabel:has(.p-inputwrapper-focus) label { 42 | @apply text-primary 43 | } 44 | 45 | .p-floatlabel-in .p-inputtext, 46 | .p-floatlabel-in .p-textarea, 47 | .p-floatlabel-in .p-select-label, 48 | .p-floatlabel-in .p-multiselect-label, 49 | .p-floatlabel-in .p-autocomplete-input-multiple, 50 | .p-floatlabel-in .p-cascadeselect-label, 51 | .p-floatlabel-in .p-treeselect-label { 52 | @apply pt-6 pb-2 53 | } 54 | 55 | .p-floatlabel-in:has(input:focus) label, 56 | .p-floatlabel-in:has(input.p-filled) label, 57 | .p-floatlabel-in:has(input:-webkit-autofill) label, 58 | .p-floatlabel-in:has(textarea:focus) label, 59 | .p-floatlabel-in:has(textarea.p-filled) label, 60 | .p-floatlabel-in:has(.p-inputwrapper-focus) label, 61 | .p-floatlabel-in:has(.p-inputwrapper-filled) label { 62 | @apply top-2 63 | } 64 | 65 | .p-floatlabel-on:has(input:focus) label, 66 | .p-floatlabel-on:has(input.p-filled) label, 67 | .p-floatlabel-on:has(input:-webkit-autofill) label, 68 | .p-floatlabel-on:has(textarea:focus) label, 69 | .p-floatlabel-on:has(textarea.p-filled) label, 70 | .p-floatlabel-on:has(.p-inputwrapper-focus) label, 71 | .p-floatlabel-on:has(.p-inputwrapper-filled) label { 72 | @apply top-0 -translate-y-1/2 rounded-sm bg-surface-0 dark:bg-surface-950 py-0 px-[0.125rem] 73 | } 74 | -------------------------------------------------------------------------------- /src/assets/primevue/organizationchart.css: -------------------------------------------------------------------------------- 1 | .p-organizationchart-table { 2 | @apply border-spacing-0 border-separate my-0 mx-auto 3 | } 4 | 5 | .p-organizationchart-table > tbody > tr > td { 6 | @apply text-center align-top py-0 px-3 7 | } 8 | 9 | .p-organizationchart-node { 10 | @apply inline-block relative py-3 px-4 rounded-md 11 | border border-surface-200 dark:border-surface-700 12 | bg-surface-0 dark:bg-surface-900 13 | text-surface-700 dark:text-surface-0 14 | transition-colors duration-200 15 | } 16 | 17 | .p-organizationchart-node:has(.p-organizationchart-node-toggle-button) { 18 | @apply pt-3 px-4 pb-5 19 | } 20 | 21 | .p-organizationchart-node.p-organizationchart-node-selectable:not(.p-organizationchart-node-selected):hover { 22 | @apply bg-surface-100 text-surface-800 dark:bg-surface-800 dark:text-surface-0 23 | } 24 | 25 | .p-organizationchart-node-selected { 26 | @apply bg-highlight 27 | } 28 | 29 | .p-organizationchart-node-toggle-button { 30 | @apply absolute -bottom-3 -ms-3 z-20 start-1/2 select-none cursor-pointer w-6 h-6 no-underline rounded-full 31 | inline-flex items-center justify-center transition-colors duration-200 32 | border border-surface-200 dark:border-surface-700 33 | bg-surface-0 text-surface-500 hover:bg-surface-100 hover:text-surface-700 34 | dark:bg-surface-900 dark:text-surface-400 dark:hover:bg-surface-800 dark:hover:text-surface-0 35 | focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary 36 | } 37 | .p-organizationchart-node-toggle-button-icon { 38 | @apply relative top-px 39 | } 40 | 41 | .p-organizationchart-connector-down { 42 | @apply my-0 mx-auto h-6 w-px bg-surface-200 dark:bg-surface-700 43 | } 44 | 45 | .p-organizationchart-connector-right { 46 | @apply rounded-none 47 | } 48 | 49 | .p-organizationchart-connector-left { 50 | @apply rounded-none border-e border-surface-200 dark:border-surface-700 51 | } 52 | 53 | .p-organizationchart-connector-top { 54 | @apply border-t border-surface-200 dark:border-surface-700 55 | } 56 | 57 | .p-organizationchart-node-selectable { 58 | @apply cursor-pointer 59 | } 60 | 61 | .p-organizationchart-connectors :nth-child(1 of .p-organizationchart-connector-left) { 62 | @apply border-e-0 63 | } 64 | 65 | .p-organizationchart-connectors :nth-last-child(1 of .p-organizationchart-connector-left) { 66 | @apply rounded-se-md 67 | } 68 | 69 | .p-organizationchart-connectors :nth-child(1 of .p-organizationchart-connector-right) { 70 | @apply border-s border-surface-200 dark:border-surface-700 rounded-ss-md 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/primevue/inputnumber.css: -------------------------------------------------------------------------------- 1 | @import './inputtext'; 2 | 3 | .p-inputnumber { 4 | @apply inline-flex relative 5 | } 6 | 7 | .p-inputnumber-button { 8 | @apply flex items-center justify-center flex-grow-0 flex-shrink-0 basis-auto cursor-pointer w-10 9 | bg-transparent enabled:hover:bg-surface-100 enabled:active:bg-surface-200 10 | border border-surface-300 enabled:hover:border-surface-300 enabled:active:border-surface-300 11 | text-surface-400 enabled:hover:text-surface-500 enabled:active:text-surface-600 12 | dark:bg-transparent dark:enabled:hover:bg-surface-800 dark:enabled:active:bg-surface-700 13 | dark:border-surface-700 dark:enabled:hover:border-surface-700 dark:enabled:active:border-surface-700 14 | dark:text-surface-400 dark:enabled:hover:text-surface-300 dark:enabled:active:text-surface-200 15 | transition-colors duration-200 16 | } 17 | 18 | .p-inputnumber-stacked .p-inputnumber-button { 19 | @apply relative flex-auto border-none 20 | } 21 | 22 | .p-inputnumber-stacked .p-inputnumber-button-group { 23 | @apply flex flex-col absolute top-px end-px h-[calc(100%-2px)] z-10 24 | } 25 | 26 | .p-inputnumber-stacked .p-inputnumber-increment-button { 27 | @apply p-0 rounded-tr-[5px] 28 | } 29 | 30 | .p-inputnumber-stacked .p-inputnumber-decrement-button { 31 | @apply p-0 rounded-br-[5px] 32 | } 33 | 34 | .p-inputnumber-horizontal .p-inputnumber-increment-button { 35 | @apply order-3 rounded-e-md border-s-0 36 | } 37 | 38 | .p-inputnumber-horizontal .p-inputnumber-input { 39 | @apply order-2 rounded-none 40 | } 41 | 42 | .p-inputnumber-horizontal .p-inputnumber-decrement-button { 43 | @apply order-1 rounded-s-md border-r-0 44 | } 45 | 46 | .p-floatlabel:has(.p-inputnumber-horizontal) label { 47 | @apply ms-10 48 | } 49 | 50 | .p-inputnumber-vertical { 51 | @apply flex-col 52 | } 53 | 54 | .p-inputnumber-vertical .p-inputnumber-button { 55 | @apply py-2 56 | } 57 | 58 | .p-inputnumber-vertical .p-inputnumber-increment-button { 59 | @apply order-1 rounded-ss-md rounded-se-md w-full border-b-0 60 | } 61 | 62 | .p-inputnumber-vertical .p-inputnumber-input { 63 | @apply order-2 rounded-none text-center 64 | } 65 | 66 | .p-inputnumber-vertical .p-inputnumber-decrement-button { 67 | @apply order-3 rounded-ee-md rounded-es-md w-full border-t-0 68 | } 69 | 70 | .p-inputnumber-input { 71 | @apply flex-auto 72 | } 73 | 74 | .p-inputnumber-fluid { 75 | @apply w-full 76 | } 77 | 78 | .p-inputnumber-fluid .p-inputnumber-input { 79 | @apply w-[1%] 80 | } 81 | 82 | .p-inputnumber-fluid.p-inputnumber-vertical .p-inputnumber-input { 83 | @apply w-full 84 | } 85 | -------------------------------------------------------------------------------- /src/services/logger/loggerDB.ts: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie/dist/dexie.js' 2 | 3 | export class NZBDonkeyDatabase extends Dexie { 4 | // Declare implicit table properties. 5 | // (just to inform Typescript. Instantiated by Dexie in stores() method) 6 | debugLog!: Dexie.Table // number = type of the primkey 7 | nzbLog!: Dexie.Table // number = type of the primkey 8 | 9 | constructor() { 10 | super('NZBDonkeyDatabase') 11 | this.version(1).stores({ 12 | debugLog: '++id, date, type, text, source, error', 13 | nzbLog: '++id, date, status, targets, searchEngine, source, error', 14 | }) 15 | } 16 | } 17 | 18 | export interface IDebugLog { 19 | id?: number 20 | date: number 21 | type: DebugLogType 22 | text: string 23 | source: string 24 | error: string 25 | } 26 | 27 | export interface DebugLogProtocolMap { 28 | debbugLoggerLog(data: IDebugLog): void 29 | debbugLoggerGet(): IDebugLog[] 30 | debbugLoggerGetLazy(data: DebugLogQuery): IDebugLog[] 31 | debbugLoggerClear(): void 32 | debbugLoggerCount(data: DebugLogQuery): number 33 | debbugLoggerDownload(): Promise 34 | debbugLoggerGetSources(): string[] 35 | } 36 | 37 | export type DebugLogType = 'info' | 'warn' | 'error' 38 | export type DebugLogSortField = 'date' 39 | export type DebugLogSortOrder = 'asc' | 'desc' 40 | export type DebugLogFilter = { 41 | type?: DebugLogType 42 | source?: string 43 | text?: string 44 | } 45 | export type DebugLogQuery = { 46 | first: number 47 | last: number 48 | sortField?: DebugLogSortField 49 | sortOrder?: DebugLogSortOrder 50 | filter?: DebugLogFilter 51 | } 52 | 53 | export interface INZBLog { 54 | id?: number 55 | date: number 56 | status?: NZBStatus 57 | title: string 58 | header: string 59 | password: string 60 | filename: string 61 | targets: Target[] 62 | searchEngine?: string 63 | source?: string 64 | errorMessage?: string 65 | } 66 | 67 | export type NZBLogFilter = { 68 | status?: NZBStatus 69 | information?: string 70 | } 71 | export type NZBLogQuery = { 72 | first: number 73 | last: number 74 | filter?: NZBLogFilter 75 | } 76 | 77 | export interface NzbLogProtocolMap { 78 | nzbLoggerLog(data: INZBLog): void 79 | nzbLoggerGet(): INZBLog[] 80 | nzbLoggerGetLazy(data: { first: number; last: number }): INZBLog[] 81 | nzbLoggerClear(): void 82 | } 83 | 84 | export type NZBStatus = 'initiated' | 'searching' | 'fetched' | 'error' | 'warn' | 'success' 85 | export type Target = { 86 | name: string 87 | category?: string 88 | status?: TargetStatus 89 | errorMessage?: string 90 | } 91 | export type TargetStatus = 'inactive' | 'pending' | 'success' | 'error' 92 | 93 | export const db = new NZBDonkeyDatabase() 94 | -------------------------------------------------------------------------------- /src/entrypoints/background/interception/helperFunction.ts: -------------------------------------------------------------------------------- 1 | import { RequestDetails } from './declarativeNetRequestHandler' 2 | 3 | import { browser, Browser } from '#imports' 4 | import * as interception from '@/services/interception' 5 | import log from '@/services/logger/debugLogger' 6 | 7 | export function prepareRequest({ body, url, method }: RequestDetails, setting: interception.DomainSettings): Request { 8 | let requestBody: BodyInit | null | undefined = undefined 9 | let contentType: string = '' 10 | if (body) { 11 | switch (setting.postDataHandling) { 12 | case 'sendAsFormData': { 13 | const formData = new FormData() 14 | for (const key in body) { 15 | for (const value of body[key]) { 16 | formData.append(key, value) 17 | } 18 | } 19 | requestBody = formData 20 | contentType = 'multipart/form-data' 21 | break 22 | } 23 | default: { 24 | const urlSearchParameters = new URLSearchParams() 25 | for (const key in body) { 26 | for (const value of body[key]) { 27 | urlSearchParameters.append(key, value) 28 | } 29 | } 30 | requestBody = urlSearchParameters.toString() 31 | contentType = 'application/x-www-form-urlencoded;charset=UTF-8' 32 | break 33 | } 34 | } 35 | } 36 | const headers = new Headers({ 'X-NZBBDonkey': 'true' }) 37 | if (contentType) headers.append('Content-Type', contentType) 38 | return new Request(url, { 39 | method: method, 40 | body: requestBody, 41 | headers: headers, 42 | }) 43 | } 44 | 45 | // Timeout settings for waiting loops 46 | const step = 100 47 | const timeout = 10000 48 | 49 | export async function waitForTabToLoad(tabId: number): Promise { 50 | let tab = await browser.tabs.get(tabId) 51 | if (tab.status === 'complete') return tab 52 | log.info(`waiting for tab ${tabId} to finish loading...`) 53 | let counter = 0 54 | while (tab.status === 'loading') { 55 | await new Promise((resolve) => setTimeout(resolve, step)) 56 | // Refresh tab info 57 | tab = await browser.tabs.get(tabId) 58 | counter++ 59 | // Calculate timeout 60 | if (counter >= timeout / step) { 61 | throw new Error(`timeout while waiting for tab ${tabId} to finish loading`) 62 | } 63 | } 64 | log.info(`tab ${tab.id} has finished loading after ${counter * step} ms with URL: ${tab.url}`) 65 | return tab 66 | } 67 | 68 | export function addTimestampToURL(request: Request, ruleId: number): Request { 69 | const url = new URL(request.url) 70 | // Use a combination of timestamp and counter for uniqueness 71 | const uniqueId = `${Date.now()}${ruleId}` 72 | url.searchParams.append('x-nzbdonkey', uniqueId) 73 | return new Request(url.toString(), request) 74 | } 75 | -------------------------------------------------------------------------------- /src/assets/primevue/radiobutton.css: -------------------------------------------------------------------------------- 1 | .p-radiobutton { 2 | @apply relative inline-flex select-none w-5 h-5 3 | } 4 | 5 | .p-radiobutton-input { 6 | @apply cursor-pointer disabled:cursor-default appearance-none absolute start-0 top-0 w-full h-full m-0 p-0 opacity-0 z-10 7 | border border-transparent rounded-full 8 | } 9 | 10 | .p-radiobutton-box { 11 | @apply flex justify-center items-center rounded-full 12 | border border-surface-300 dark:border-surface-700 13 | bg-surface-0 dark:bg-surface-950 14 | w-5 h-5 15 | transition-colors duration-200 16 | shadow-[0_1px_2px_0_rgba(18,18,23,0.05)] 17 | } 18 | 19 | .p-radiobutton-icon { 20 | @apply bg-transparent text-xs w-3 h-3 rounded-full 21 | transition-all duration-200 backface-hidden 22 | } 23 | 24 | .p-radiobutton-icon { 25 | @apply scale-[0.1] 26 | } 27 | 28 | .p-radiobutton:not(.p-disabled):has(.p-radiobutton-input:hover) .p-radiobutton-box { 29 | @apply border-surface-400 dark:border-surface-600 30 | } 31 | 32 | .p-radiobutton-checked .p-radiobutton-box { 33 | @apply border-primary bg-primary 34 | } 35 | 36 | .p-radiobutton-checked .p-radiobutton-box .p-radiobutton-icon { 37 | @apply bg-primary-contrast visible 38 | } 39 | 40 | .p-radiobutton-checked .p-radiobutton-box .p-radiobutton-icon { 41 | @apply scale-100 42 | } 43 | 44 | .p-radiobutton-checked:not(.p-disabled):has(.p-radiobutton-input:hover) .p-radiobutton-box { 45 | @apply border-primary-emphasis bg-primary-emphasis 46 | } 47 | 48 | .p-radiobutton:not(.p-disabled):has(.p-radiobutton-input:hover).p-radiobutton-checked .p-radiobutton-box .p-radiobutton-icon { 49 | @apply bg-primary-contrast 50 | } 51 | 52 | .p-radiobutton:not(.p-disabled):has(.p-radiobutton-input:focus-visible) .p-radiobutton-box { 53 | @apply outline outline-1 outline-offset-2 outline-primary 54 | } 55 | 56 | .p-radiobutton.p-invalid > .p-radiobutton-box { 57 | @apply border-red-400 dark:border-red-300 58 | } 59 | 60 | .p-radiobutton.p-variant-filled .p-radiobutton-box { 61 | @apply bg-surface-50 dark:bg-surface-800 62 | } 63 | 64 | .p-radiobutton.p-disabled { 65 | @apply opacity-100 66 | } 67 | 68 | .p-radiobutton.p-disabled .p-radiobutton-box { 69 | @apply bg-surface-200 dark:bg-surface-400 70 | border-surface-300 dark:border-surface-700 71 | } 72 | 73 | .p-radiobutton-checked.p-disabled .p-radiobutton-box .p-radiobutton-icon { 74 | @apply bg-surface-700 dark:bg-surface-400 75 | } 76 | 77 | .p-radiobutton-sm, 78 | .p-radiobutton-sm .p-radiobutton-box { 79 | @apply w-4 h-4 80 | } 81 | 82 | .p-radiobutton-sm .p-radiobutton-icon { 83 | @apply w-2 h-2 84 | } 85 | 86 | .p-radiobutton-lg, 87 | .p-radiobutton-lg .p-radiobutton-box { 88 | @apply w-6 h-6 89 | } 90 | 91 | .p-radiobutton-lg .p-radiobutton-icon { 92 | @apply w-4 h-4 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/settingsUtilities.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, toRaw, watch } from 'vue' 2 | 3 | import { browser, storage } from '#imports' 4 | 5 | export type Settings = { 6 | name: string 7 | defaults: T 8 | } 9 | 10 | /** 11 | * Synchronizes a reactive `Ref` with browser storage. 12 | * @param {Settings} settingsConfig - Configuration containing the name and default values of the settings. 13 | * @return {Promise>} A reactive Ref object synchronized with the storage. 14 | */ 15 | export const useSettings = async ({ name, defaults }: Settings): Promise> => { 16 | const settings = storage.defineItem(`sync:${name}`, { 17 | fallback: defaults, 18 | }) 19 | 20 | const settingsRef = ref(await settings.getValue()) as Ref 21 | let isUpdatingFromStorage = false // Flag to prevent infinite loops 22 | 23 | // Watch for changes in storage and update settingsRef 24 | settings.watch((value) => { 25 | isUpdatingFromStorage = true 26 | settingsRef.value = value 27 | isUpdatingFromStorage = false 28 | }) 29 | 30 | // Watch for changes in settingsRef and save them to storage 31 | watch( 32 | settingsRef, 33 | async (value) => { 34 | if (!isUpdatingFromStorage) { 35 | await settings.setValue(toRaw(value)) 36 | } 37 | }, 38 | { deep: true } 39 | ) 40 | return settingsRef 41 | } 42 | 43 | /** 44 | * Retrieves settings from browser storage. 45 | * @param {Settings} settingsConfig - Configuration containing the name and default values of the settings. 46 | * @return {Promise} The retrieved settings object. 47 | */ 48 | export const getSettings = async ({ name, defaults }: Settings): Promise => { 49 | const settings = await browser.storage.sync.get({ [name]: defaults }) 50 | return settings[name] as T 51 | } 52 | 53 | /** 54 | * Saves new settings to browser storage. 55 | * @param {Settings} settingsConfig - Configuration containing the name of the settings. 56 | * @param {T} newSettings - The new settings object to be saved. 57 | * @return {Promise} Resolves when the settings are saved. 58 | */ 59 | export const setSettings = async ({ name }: Settings, newSettings: T): Promise => { 60 | await browser.storage.sync.set({ [name]: newSettings }) 61 | } 62 | 63 | /** 64 | * Watches for changes in browser storage and triggers a callback. 65 | * @param {Settings} settingsConfig - Configuration containing the name of the settings. 66 | * @param {(settings: T) => void} callback - Function to call when the settings change. 67 | */ 68 | export const watchSettings = ({ name }: Settings, callback: (settings: T) => void) => { 69 | browser.storage.sync.onChanged.addListener(async (changes) => { 70 | if (changes[name]) { 71 | const settings = await getSettings({ name, defaults: changes[name].newValue }) 72 | callback(settings) 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/services/searchengines/functions.ts: -------------------------------------------------------------------------------- 1 | import { SearchEngine } from './settings' 2 | 3 | import log from '@/services/logger/debugLogger' 4 | import { NZBObject, textToNzbObject } from '@/services/nzbfile/nzbObject' 5 | import { FetchOptions, useFetch } from '@/utils/fetchUtilities' 6 | 7 | export const cleanHeader = (header: string, engine: SearchEngine): string => { 8 | if (engine.settings.removeUnderscore) { 9 | header = header.replace('_', ' ') 10 | } 11 | if (engine.settings.removeHyphen) { 12 | header = header.replace('-', ' ') 13 | } 14 | if (engine.settings.setIntoQuotes) { 15 | header = `"${header}"` 16 | } 17 | return header 18 | } 19 | 20 | export const search = async (header: string, options: FetchOptions, engine: SearchEngine): Promise => { 21 | header = cleanHeader(header, engine) 22 | log.info(`searching search engine "${engine.name}" for header: ${header}`) 23 | try { 24 | const searchOptions = setURL(options, engine.settings.searchURL, header) 25 | log.info(`Search url is: ${searchOptions.url}`) 26 | const response = await (await useFetch(searchOptions)).text() 27 | log.info(`Search engine "${engine.name}" has sent a response`) 28 | return response 29 | } catch (e) { 30 | const error = e instanceof Error ? e : new Error(String(e)) 31 | log.warn(`error while searching on search engine "${engine.name}"`, error) 32 | throw error 33 | } 34 | } 35 | 36 | export const setURL = (options: FetchOptions, url: string, searchString: string = ''): FetchOptions => { 37 | url = url.replace(/%s/, searchString) 38 | return { 39 | ...options, 40 | url: url, 41 | } 42 | } 43 | 44 | export const download = async (options: FetchOptions, engine: SearchEngine): Promise => { 45 | let nzbTextFile: string 46 | try { 47 | nzbTextFile = await (await useFetch(options)).text() 48 | } catch (e) { 49 | const error = e instanceof Error ? e : new Error(String(e)) 50 | log.warn(`error while trying to download the NZB file from search engine "${engine.name}"`, error) 51 | throw error 52 | } 53 | let nzbFile: NZBObject 54 | try { 55 | nzbFile = textToNzbObject(nzbTextFile) 56 | log.info(`the response from search engine "${engine.name}" is a valid NZB file`) 57 | return nzbFile 58 | } catch { 59 | log.warn(`the response from search engine "${engine.name}" is not a valid NZB file`) 60 | throw new Error(`not a valid NZB file`) 61 | } 62 | } 63 | 64 | export const convertToNzbObject = (nzbTextFile: string, engine: SearchEngine): NZBObject => { 65 | let nzbFile: NZBObject 66 | try { 67 | nzbFile = textToNzbObject(nzbTextFile) 68 | log.info(`the response from search engine "${engine.name}" is a valid NZB file`) 69 | } catch { 70 | log.warn(`the response from search engine "${engine.name}" is not a valid NZB file`) 71 | throw new Error('not a valid NZB file') 72 | } 73 | return nzbFile 74 | } 75 | --------------------------------------------------------------------------------