├── assets └── neptune-screenshot.png ├── injector ├── package.json ├── redux-devtools │ ├── img │ │ ├── logo │ │ │ ├── 16x16.png │ │ │ ├── 38x38.png │ │ │ ├── 48x48.png │ │ │ ├── error.png │ │ │ ├── gray.png │ │ │ ├── 128x128.png │ │ │ └── scalable.png │ │ └── loading.svg │ ├── 079db4a1c8da8ec06700.woff2 │ ├── 56f3f8ac2e0a51c02e1c.woff2 │ ├── c60b44947671d757833d.woff2 │ ├── e46177b21b27cd6643c5.woff2 │ ├── ef865b56e54f6a46f73f.woff2 │ ├── devtools.html │ ├── devtools.bundle.js │ ├── background.bundle.js.LICENSE.txt │ ├── options.bundle.js.LICENSE.txt │ ├── remote.html │ ├── devpanel.html │ ├── window.html │ ├── options.html │ ├── manifest.json │ ├── devpanel.bundle.js.LICENSE.txt │ ├── remote.bundle.js.LICENSE.txt │ ├── window.bundle.js.LICENSE.txt │ ├── content.bundle.js │ └── page.bundle.js ├── preload.js └── index.js ├── types ├── api │ ├── showModal.d.ts │ ├── registerTab.d.ts │ ├── observe.d.ts │ ├── registerRoute.d.ts │ ├── hookContextMenu.d.ts │ ├── intercept.d.ts │ ├── plugins.d.ts │ └── utils.d.ts ├── tsconfig.base.json ├── package.json ├── ui │ └── components.d.ts ├── pnpm-lock.yaml ├── LICENSE └── index.d.ts ├── .prettierrc.json ├── rollup.config.js ├── src ├── api │ ├── showModal.js │ ├── intercept.js │ ├── registerRoute.js │ ├── themes.js │ ├── hookContextMenu.js │ ├── observe.js │ ├── registerTab.js │ ├── utils.js │ └── plugins.js ├── ui │ ├── settings.js │ ├── themesTab.js │ ├── components.js │ └── pluginsTab.js ├── windowObject.js ├── index.js ├── styles.js └── handleExfiltrations.js ├── flake.lock ├── package.json ├── .github └── workflows │ └── build.yml ├── flake.nix ├── README.md ├── LICENSE ├── .gitignore └── pnpm-lock.yaml /assets/neptune-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/assets/neptune-screenshot.png -------------------------------------------------------------------------------- /injector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neptune", 3 | "main": "index.js", 4 | "version": "9999.9999.9999" 5 | } 6 | -------------------------------------------------------------------------------- /types/api/showModal.d.ts: -------------------------------------------------------------------------------- 1 | export function showModal(name: HTMLElement["innerText"], component: CallableFunction): void; 2 | -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/16x16.png -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/38x38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/38x38.png -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/48x48.png -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/error.png -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/gray.png -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/128x128.png -------------------------------------------------------------------------------- /injector/redux-devtools/img/logo/scalable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/img/logo/scalable.png -------------------------------------------------------------------------------- /injector/redux-devtools/079db4a1c8da8ec06700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/079db4a1c8da8ec06700.woff2 -------------------------------------------------------------------------------- /injector/redux-devtools/56f3f8ac2e0a51c02e1c.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/56f3f8ac2e0a51c02e1c.woff2 -------------------------------------------------------------------------------- /injector/redux-devtools/c60b44947671d757833d.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/c60b44947671d757833d.woff2 -------------------------------------------------------------------------------- /injector/redux-devtools/e46177b21b27cd6643c5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/e46177b21b27cd6643c5.woff2 -------------------------------------------------------------------------------- /injector/redux-devtools/ef865b56e54f6a46f73f.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/neptune/HEAD/injector/redux-devtools/ef865b56e54f6a46f73f.woff2 -------------------------------------------------------------------------------- /types/api/registerTab.d.ts: -------------------------------------------------------------------------------- 1 | export function registerTab( 2 | name: HTMLElement["textContent"], 3 | path: string, 4 | component?: CallableFunction, 5 | ): () => void; 6 | -------------------------------------------------------------------------------- /types/api/observe.d.ts: -------------------------------------------------------------------------------- 1 | interface Unobserver { 2 | (): void; 3 | now(): void; 4 | } 5 | export function observe(selector: string, cb: (el: HTMLElement | SVGElement) => any): Unobserver; 6 | -------------------------------------------------------------------------------- /injector/redux-devtools/devtools.html: -------------------------------------------------------------------------------- 1 | Redux DevTools
-------------------------------------------------------------------------------- /types/api/registerRoute.d.ts: -------------------------------------------------------------------------------- 1 | import type { intercept } from "./intercept"; 2 | 3 | export function registerRoute( 4 | path: string, 5 | component: CallableFunction, 6 | ): ReturnType; 7 | -------------------------------------------------------------------------------- /types/api/hookContextMenu.d.ts: -------------------------------------------------------------------------------- 1 | import type { intercept } from "./intercept"; 2 | 3 | export function hookContextMenu( 4 | menuType: string, 5 | name: string, 6 | handler: (e: MouseEvent) => any, 7 | ): ReturnType; 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "bracketSameLine": true, 12 | "arrowParens": "always", 13 | "proseWrap": "always" 14 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | 4 | export default { 5 | input: "src/index.js", 6 | output: { 7 | file: "dist/neptune.js", 8 | format: "iife", 9 | sourcemap: true, 10 | }, 11 | plugins: [ 12 | resolve({ browser: true }), 13 | terser(), 14 | ], 15 | }; -------------------------------------------------------------------------------- /types/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "skipLibCheck": true, 6 | "noEmit": true, 7 | 8 | "module": "ES2022", 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "forceConsistentCasingInFileNames": true, 14 | 15 | "allowJs": true, 16 | "strict": true, 17 | 18 | "types": ["neptune-types"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/showModal.js: -------------------------------------------------------------------------------- 1 | import { ReactiveRoot } from "../ui/components.js"; 2 | import { actions } from "../handleExfiltrations.js"; 3 | import { observe } from "./observe.js"; 4 | 5 | export default function showModal(name, component) { 6 | actions.modal.showReleaseNotes(); 7 | const unob = observe(`[class^="_modalHeader_"]`, (header) => { 8 | unob.now(); 9 | 10 | header.getElementsByTagName("h4")[0].innerText = name; 11 | 12 | header.nextSibling.replaceChildren( 13 | ReactiveRoot({ 14 | children: component, 15 | }), 16 | ); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1733376361, 6 | "narHash": "sha256-aLJxoTDDSqB+/3orsulE6/qdlX6MzDLIITLZqdgMpqo=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "929116e316068c7318c54eb4d827f7d9756d5e9c", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /src/api/intercept.js: -------------------------------------------------------------------------------- 1 | export const interceptors = {}; 2 | 3 | export default function intercept(types, cb, once = false) { 4 | if (typeof types == "string") types = [types]; 5 | 6 | const uninterceptors = []; 7 | const unintercept = () => uninterceptors.forEach((u) => u()); 8 | 9 | for (let type of types) { 10 | if (!interceptors[type]) interceptors[type] = []; 11 | 12 | const handleIntercept = once 13 | ? (...args) => { 14 | unintercept(); 15 | 16 | return cb(...args); 17 | } 18 | : cb; 19 | interceptors[type].push(handleIntercept); 20 | 21 | uninterceptors.push(() => 22 | interceptors[type].splice(interceptors[type].indexOf(handleIntercept), 1), 23 | ); 24 | } 25 | 26 | return unintercept; 27 | } 28 | -------------------------------------------------------------------------------- /injector/redux-devtools/devtools.bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={};function t(e){chrome.devtools.panels.create("Redux","img/logo/scalable.png",e,(function(){}))}e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var r=e.g.document;if(!t&&r&&(r.currentScript&&(t=r.currentScript.src),!t)){var n=r.getElementsByTagName("script");n.length&&(t=n[n.length-1].src)}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.p,chrome.runtime.getBackgroundPage?chrome.runtime.getBackgroundPage((e=>{t(e?"window.html":"devpanel.html")})):t("devpanel.html")})(); -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neptune-types", 3 | "version": "1.0.1", 4 | "description": "TypeScript type declarations for neptune plugins", 5 | "homepage": "https://github.com/uwu/neptune/tree/master/types", 6 | "license": "MS-PL", 7 | "contributors": [ 8 | { 9 | "name": "relative", 10 | "url": "https://relative.im", 11 | "githubUsername": "relative" 12 | } 13 | ], 14 | "main": "", 15 | "types": "index.d.ts", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/uwu/neptune.git", 19 | "directory": "types" 20 | }, 21 | "dependencies": { 22 | "idb-keyval": "^6.2.1", 23 | "immutable": "5.0.0-beta.4", 24 | "redux": "^4.2.1", 25 | "spitroast": "^1.4.3", 26 | "type-fest": "^4.3.1", 27 | "voby": "^0.54.0", 28 | "zod": "^3.2.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /injector/redux-devtools/background.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! 9 | * is-plain-object 10 | * 11 | * Copyright (c) 2014-2017, Jon Schlinkert. 12 | * Released under the MIT License. 13 | */ 14 | 15 | /*! 16 | * isobject 17 | * 18 | * Copyright (c) 2014-2017, Jon Schlinkert. 19 | * Released under the MIT License. 20 | */ 21 | 22 | /*! 23 | * shallow-clone 24 | * 25 | * Copyright (c) 2015-present, Jon Schlinkert. 26 | * Released under the MIT License. 27 | */ 28 | 29 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 30 | -------------------------------------------------------------------------------- /injector/redux-devtools/options.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * scheduler.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neptune", 3 | "version": "1.0.1", 4 | "description": "A client modification for the Tidal music streaming app.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "rollup --config rollup.config.js", 8 | "format": "prettier -wc ./src", 9 | "watch": "rollup --config rollup.config.js -w", 10 | "run": "npm run build && set NEPTUNE_DIST_PATH=%cd%\\dist&& %LOCALAPPDATA%\\TIDAL\\TIDAL.exe" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MS-Pl", 15 | "dependencies": { 16 | "@cumjar/websmack": "^1.2.0", 17 | "@uwu/quartz": "^1.5.1", 18 | "idb-keyval": "^6.2.1", 19 | "quartz-plugin-url-import": "^1.0.1", 20 | "spitroast": "^2.1.6", 21 | "voby": "^0.57.3" 22 | }, 23 | "type": "module", 24 | "devDependencies": { 25 | "@rollup/plugin-node-resolve": "^15.1.0", 26 | "@rollup/plugin-terser": "^0.4.3", 27 | "prettier": "^3.0.0", 28 | "rollup": "^3.26.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /injector/redux-devtools/img/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and push 2 | on: 3 | push: 4 | branches: [master] 5 | paths: 6 | - "src/**" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/checkout@v3 16 | with: 17 | repository: "uwu/neptune-builds" 18 | path: "builds" 19 | token: ${{ secrets.LINK_TOKEN }} 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 20 23 | 24 | - name: Install dependencies 25 | run: | 26 | npm i -g pnpm 27 | pnpm i 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Push builds 33 | run: | 34 | rm $GITHUB_WORKSPACE/builds/* || true 35 | cp -r dist/* $GITHUB_WORKSPACE/builds || true 36 | cd $GITHUB_WORKSPACE/builds 37 | git config --local user.email "actions@github.com" 38 | git config --local user.name "GitHub Actions" 39 | git add . 40 | git commit -m "Build $GITHUB_SHA" || exit 0 41 | git push 42 | -------------------------------------------------------------------------------- /types/ui/components.d.ts: -------------------------------------------------------------------------------- 1 | import type { createElement } from "voby"; 2 | 3 | // Voby doesn't export the Element type ???????????????????????????????????????? 4 | export type Element = ReturnType; 5 | 6 | export interface ReactiveRootPropTypes { 7 | children: Element; 8 | } 9 | export function ReactiveRoot({ children }: ReactiveRootPropTypes): Element; 10 | 11 | export interface SwitchPropTypes { 12 | checked?: HTMLInputElement["checked"]; 13 | onClick: HTMLSpanElement["onclick"]; 14 | } 15 | export function Switch(props: SwitchPropTypes): Element; 16 | 17 | export interface TextInputPropTypes { 18 | placeholder: HTMLInputElement["placeholder"]; 19 | type: HTMLInputElement["type"]; 20 | value: HTMLInputElement["value"]; 21 | onEnter: (ev: Parameters[0]) => any; 22 | } 23 | export function TextInput({ 24 | placeholder = "", 25 | type = "text", 26 | value = "", 27 | onEnter = () => {}, 28 | }: TextInputPropTypes): Element; 29 | 30 | export interface ButtonPropTypes { 31 | onClick: HTMLButtonElement["onclick"]; 32 | children: Element; 33 | } 34 | export function Button({ onClick = () => {}, children }: ButtonPropTypes): Element; 35 | -------------------------------------------------------------------------------- /src/api/registerRoute.js: -------------------------------------------------------------------------------- 1 | import { ReactiveRoot } from "../ui/components.js"; 2 | import intercept from "./intercept.js"; 3 | import { observe } from "./observe.js"; 4 | 5 | const pageNotFoundSelector = `[class^="_pageNotFoundError_"]`; 6 | 7 | const replacePage = (page, component) => { 8 | page.style.display = "none"; 9 | 10 | const neptunePage = document.createElement("div"); 11 | neptunePage.className = "__NEPTUNE_PAGE"; 12 | 13 | page.insertAdjacentElement("afterend", neptunePage); 14 | neptunePage.appendChild(ReactiveRoot({ children: component })); 15 | }; 16 | 17 | intercept("router/NAVIGATED", () => { 18 | for (const page of document.getElementsByClassName("__NEPTUNE_PAGE")) 19 | page.parentElement.removeChild(page); 20 | }); 21 | 22 | export default function registerRoute(path, component) { 23 | return intercept("router/NAVIGATED", ([payload]) => { 24 | if (payload.search != `?neptuneRoute=${path}`) return; 25 | 26 | const pageNotFound = document.querySelector(pageNotFoundSelector); 27 | if (pageNotFound) return replacePage(pageNotFound, component); 28 | 29 | const unob = observe(pageNotFoundSelector, (page) => { 30 | unob.now(); 31 | replacePage(page, component); 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /types/api/intercept.d.ts: -------------------------------------------------------------------------------- 1 | import type { ActionTypes } from "../tidal"; 2 | export type UninterceptFunction = () => void; 3 | 4 | export type ActionType = keyof ActionTypes; 5 | 6 | export type PayloadActionTypeTuple = [ActionTypes[AT], AT]; 7 | export type CallbackFunction = 8 | /** 9 | * @returns `true` to cancel dispatch 10 | * @returns anything else to continue 11 | */ 12 | ([payload, at]: PayloadActionTypeTuple) => 13 | | true 14 | | void 15 | | Promise; 16 | 17 | /** 18 | * intercept redux events 19 | * 20 | * return `true` from callback function to cancel the dispatch 21 | */ 22 | export function intercept( 23 | type: AT, 24 | cb: CallbackFunction, 25 | once?: boolean, 26 | ): UninterceptFunction; 27 | 28 | /** 29 | * if using this signature you will have to manually cast action payloads 30 | * to their proper types because narrowing generic types isn't 31 | * possible yet in TypeScript 32 | * 33 | * return `true` from callback function to cancel the dispatch 34 | */ 35 | export function intercept( 36 | types: AT[], 37 | cb: CallbackFunction, 38 | once?: boolean, 39 | ): UninterceptFunction; 40 | -------------------------------------------------------------------------------- /injector/redux-devtools/remote.html: -------------------------------------------------------------------------------- 1 | RemoteDev
-------------------------------------------------------------------------------- /src/api/themes.js: -------------------------------------------------------------------------------- 1 | import { store } from "voby"; 2 | import { appendStyle, createPersistentObject, parseManifest } from "./utils.js"; 3 | 4 | export const [themesStore, themesStoreReady] = createPersistentObject("NEPTUNE_THEMES", true); 5 | 6 | let updateThemeStyle = appendStyle(""); 7 | 8 | // Not sure why this didn't work previously? 9 | store.on(themesStore, reloadThemes) 10 | 11 | function reloadThemes() { 12 | updateThemeStyle(themesStore.filter(t => t.enabled).map((t) => `@import url("${t.url}")`).join(";")); 13 | } 14 | 15 | export function removeTheme(url) { 16 | themesStore.splice( 17 | themesStore.findIndex((t) => t.url == url), 18 | 1, 19 | ); 20 | } 21 | 22 | export function toggleTheme(url) { 23 | const theme = themesStore.find((t) => t.url == url); 24 | theme.enabled = !theme.enabled; 25 | } 26 | 27 | export async function importTheme(url, enabled = true) { 28 | let manifest; 29 | let text; 30 | 31 | try { 32 | text = await (await fetch(url)).text(); 33 | } catch { 34 | throw "Failed to fetch theme!"; 35 | } 36 | 37 | try { 38 | manifest = parseManifest(text); 39 | } catch (e) { 40 | manifest = { 41 | name: url.split("/").pop(), 42 | author: "Unknown", 43 | description: "No description provided.", 44 | } 45 | } 46 | 47 | themesStore.unshift({ 48 | name: manifest.name, 49 | author: manifest.author, 50 | description: manifest.description, 51 | enabled, 52 | url, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /injector/redux-devtools/devpanel.html: -------------------------------------------------------------------------------- 1 | Redux DevTools
-------------------------------------------------------------------------------- /src/api/hookContextMenu.js: -------------------------------------------------------------------------------- 1 | import intercept from "./intercept.js"; 2 | import { observe } from "./observe.js"; 3 | 4 | export default function hookContextMenu(menuType, name, handler) { 5 | return intercept("contextMenu/OPEN", ([payload]) => { 6 | // TODO: Change this to a switch statement. 7 | 8 | if (payload?.type != menuType) return; 9 | 10 | const unob = observe(`[data-type="contextmenu-item"]`, (elem) => { 11 | unob.now(); 12 | 13 | const contextMenuItem = elem.cloneNode(true); 14 | const contextMenuLabel = contextMenuItem.querySelector(`[class^="_actionTextInner_"]`); 15 | contextMenuLabel.innerText = name; 16 | 17 | const parentClasses = contextMenuLabel.parentElement.classList; 18 | 19 | contextMenuItem.innerHTML = ""; 20 | 21 | const contextMenuWrapper = document.createElement("div"); 22 | contextMenuWrapper.setAttribute("tabindex", "0"); 23 | contextMenuWrapper.classList.add(...parentClasses); 24 | contextMenuWrapper.appendChild(contextMenuLabel); 25 | 26 | contextMenuItem.addEventListener("keyup", (event) => { 27 | if (event.keyCode != 13) return; 28 | 29 | event.target.click(); 30 | }); 31 | 32 | contextMenuItem.appendChild(contextMenuWrapper); 33 | 34 | contextMenuItem.addEventListener("click", (event) => { 35 | handler(event); 36 | }); 37 | 38 | elem.parentElement.appendChild(contextMenuItem); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /types/api/plugins.d.ts: -------------------------------------------------------------------------------- 1 | import type { store } from "voby"; 2 | import type { createPersistentObject } from "./utils"; 3 | 4 | export const [pluginStore, pluginStoreReady]: ReturnType>; 5 | 6 | export const enabled: ReturnType; 7 | 8 | export interface PluginManifest { 9 | name: string; 10 | author: string; 11 | description: string; 12 | hash: string; 13 | } 14 | 15 | export interface Plugin { 16 | id: string; 17 | code: string; 18 | manifest: PluginManifest; 19 | enabled: boolean; 20 | update: boolean; 21 | } 22 | 23 | export function getPluginById(id: Plugin["id"]): Plugin | undefined; 24 | export function disablePlugin(id: Plugin["id"]): Promise; 25 | export function togglePlugin( 26 | id: Plugin["id"], 27 | ): ReturnType | ReturnType; 28 | export function enablePlugin(id: Plugin["id"]): Promise; 29 | export function installPlugin( 30 | id: Plugin["id"], 31 | code: Plugin["code"], 32 | manifest: PluginManifest, 33 | enabled?: boolean, 34 | ): Promise; 35 | export function removePlugin(id: Plugin["id"]): Promise; 36 | export function fetchPluginFromURL(url: string): Promise<[code: string, manifest: PluginManifest]>; 37 | export function reloadPlugin(plugin: Plugin): Promise; 38 | export function installPluginFromURL( 39 | url: Parameters[0], 40 | enabled?: boolean, 41 | ): Promise; 42 | -------------------------------------------------------------------------------- /src/ui/settings.js: -------------------------------------------------------------------------------- 1 | import { $, html } from "voby"; 2 | import { PluginTab } from "./pluginsTab.js"; 3 | import { ThemesTab } from "./themesTab.js"; 4 | import registerRoute from "../api/registerRoute.js"; 5 | import hookContextMenu from "../api/hookContextMenu.js"; 6 | import { pushVirtualRoute } from "../api/utils.js"; 7 | 8 | let selectedTab = $(0); 9 | const tabs = [ 10 | { 11 | name: "Plugins", 12 | component: PluginTab, 13 | }, 14 | { 15 | name: "Themes", 16 | component: ThemesTab, 17 | }, 18 | { 19 | name: "Addon Store", 20 | component: () => html`[WIP]`, 21 | }, 22 | ]; 23 | 24 | function TabButton({ className = "", onClick = () => {}, children }) { 25 | return html``; 31 | } 32 | 33 | registerRoute( 34 | "settings", 35 | html`
36 |
37 |
38 | ${tabs.map( 39 | (tab, idx) => 40 | html`<${TabButton} onClick=${() => selectedTab(idx)} className=${() => 41 | idx == selectedTab() ? "neptune-active-tab" : ""}>${tab.name}`, 42 | )} 43 |
44 |
${() => tabs[selectedTab()].component}
45 |
46 |
`, 47 | ); 48 | 49 | hookContextMenu("USER_PROFILE", "neptune settings", () => pushVirtualRoute("settings")); 50 | -------------------------------------------------------------------------------- /src/api/observe.js: -------------------------------------------------------------------------------- 1 | // code source: https://github.com/KaiHax/kaihax/blob/master/src/patcher.ts 2 | 3 | const observations = new Set(); 4 | const observer = new MutationObserver((records) => { 5 | // de-dupe to be sure 6 | const changedElems = new Set(); 7 | 8 | for (const record of records) { 9 | changedElems.add(record.target); 10 | 11 | for (const e of record.removedNodes) 12 | if (e instanceof HTMLElement || e instanceof SVGElement) changedElems.add(e); 13 | } 14 | 15 | for (const elem of changedElems) 16 | for (const obs of observations) { 17 | if (elem.matches(obs[0])) obs[1](elem); 18 | 19 | elem 20 | .querySelectorAll(obs[0]) 21 | .forEach( 22 | (e) => !obs[2] && (e instanceof HTMLElement || e instanceof SVGElement) && obs[1](e), 23 | ); 24 | } 25 | }); 26 | 27 | const startObserving = () => 28 | observer.observe(document.body, { 29 | subtree: true, 30 | childList: true, 31 | attributes: true, 32 | }); 33 | 34 | const stopObserving = () => observer.disconnect(); 35 | 36 | export function observe(sel, cb) { 37 | if (observations.size === 0) startObserving(); 38 | const entry = [sel, cb, false]; 39 | observations.add(entry); 40 | 41 | const unobs = () => { 42 | observations.delete(entry); 43 | if (observations.size === 0) stopObserving(); 44 | }; 45 | 46 | unobs.now = () => { 47 | entry[2] = true; 48 | unobs(); 49 | }; 50 | 51 | return unobs; 52 | } 53 | export function unobserve() { 54 | observations.clear(); 55 | stopObserving(); 56 | } 57 | -------------------------------------------------------------------------------- /injector/redux-devtools/window.html: -------------------------------------------------------------------------------- 1 | Redux DevTools
-------------------------------------------------------------------------------- /src/windowObject.js: -------------------------------------------------------------------------------- 1 | import * as voby from "voby"; 2 | import * as patcher from "spitroast"; 3 | import * as utils from "./api/utils.js"; 4 | import * as plugins from "./api/plugins.js"; 5 | import * as themes from "./api/themes.js"; 6 | import * as components from "./ui/components.js"; 7 | import intercept from "./api/intercept.js"; 8 | import { observe } from "./api/observe.js"; 9 | import registerTab from "./api/registerTab.js"; 10 | import registerRoute from "./api/registerRoute.js"; 11 | import hookContextMenu from "./api/hookContextMenu.js"; 12 | import showModal from "./api/showModal.js"; 13 | // TODO: AWFUL VOMIT VOMIT KILL MURDER DIE KILL KILL DIE MURDER VOMIT 14 | import { store } from "./handleExfiltrations.js"; 15 | 16 | import quartz from "@uwu/quartz"; 17 | 18 | let currentMediaItem = {}; 19 | 20 | try { 21 | const vibrantColorStyle = utils.appendStyle(""); 22 | 23 | intercept("playbackControls/MEDIA_PRODUCT_TRANSITION", ([{ mediaProduct }]) => { 24 | Object.assign( 25 | currentMediaItem, 26 | store.getState().content.mediaItems[mediaProduct.productId], 27 | ); 28 | const vibrantColor = currentMediaItem?.item?.album?.vibrantColor; 29 | 30 | if (!vibrantColor) return; 31 | 32 | vibrantColorStyle(`:root{--track-vibrant-color:${vibrantColor};--track-vibrant-color-rgb:${utils.convertHexToRGB(vibrantColor)}}`); 33 | }); 34 | } catch {} 35 | 36 | export default { 37 | patcher, 38 | utils, 39 | intercept, 40 | observe, 41 | registerTab, 42 | registerRoute, 43 | hookContextMenu, 44 | showModal, 45 | voby, 46 | plugins, 47 | themes, 48 | components, 49 | currentMediaItem, 50 | quartz 51 | }; 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./ui/settings.js"; 2 | import "./handleExfiltrations.js"; 3 | import windowObject from "./windowObject.js"; 4 | 5 | // Updater 3 6 | if (NeptuneNative.VITE_ACTIVE != true) { 7 | (async () => { 8 | const fsScope = NeptuneNative.createEvalScope(` 9 | const fs = require("fs"); 10 | const path = require("path"); 11 | 12 | var neptuneExports = { 13 | updateFile(name, contents) { 14 | fs.writeFileSync(path.join(process.resourcesPath, "app", name), contents); 15 | } 16 | } 17 | `); 18 | 19 | const updateFile = NeptuneNative.getNativeValue(fsScope, "updateFile"); 20 | 21 | const indexFetch = await fetch( 22 | "https://raw.githubusercontent.com/uwu/neptune/master/injector/index.js", 23 | ); 24 | const preloadFetch = await fetch( 25 | "https://raw.githubusercontent.com/uwu/neptune/master/injector/preload.js", 26 | ); 27 | 28 | if (!(indexFetch.ok || preloadFetch.ok)) return; 29 | 30 | updateFile("index.js", await indexFetch.text()) 31 | updateFile("preload.js", await preloadFetch.text()) 32 | 33 | alert("neptune has been updated. Please restart TIDAL."); 34 | })() 35 | } 36 | 37 | // Restore the console 38 | for (let key in console) { 39 | const orig = console[key]; 40 | 41 | Object.defineProperty(console, key, { 42 | set() { 43 | return true; 44 | }, 45 | get() { 46 | return orig; 47 | }, 48 | }); 49 | } 50 | 51 | // Force properties to be writable for patching 52 | const originalDefineProperty = Object.defineProperty; 53 | 54 | Object.defineProperty = function (...args) { 55 | args[2].configurable = true; 56 | 57 | try { 58 | return originalDefineProperty.apply(this, args); 59 | } catch {} 60 | }; 61 | 62 | Object.freeze = (arg) => arg; 63 | 64 | // If the app fails to load for any reason we simply reload the page. 65 | // setTimeout(() => { 66 | // if (!windowObject.store) window.location.reload(); 67 | // }, 7000); 68 | 69 | window.neptune = windowObject; 70 | -------------------------------------------------------------------------------- /injector/redux-devtools/options.html: -------------------------------------------------------------------------------- 1 | Redux DevTools Options
-------------------------------------------------------------------------------- /src/api/registerTab.js: -------------------------------------------------------------------------------- 1 | import registerRoute from "./registerRoute.js"; 2 | import intercept from "./intercept.js"; 3 | import { actions } from "../handleExfiltrations.js"; 4 | function makeInactive(tab) { 5 | tab.classList.remove(Array.from(tab.classList).find((c) => c.startsWith("_activeItem_"))); 6 | } 7 | 8 | const getTabs = () => document.querySelector(`.sidebarWrapper section[class^="_section_"]`); 9 | 10 | // Automatically set tab to unchecked. 11 | intercept("ROUTER_LOCATION_CHANGED", () => { 12 | for (const tab of document.getElementsByClassName("__NEPTUNE_TAB")) tab.style.color = ""; 13 | }); 14 | 15 | /* 16 | This entire function abuses their router's 404 handling to insert our own tabs. 17 | Because 404s count towards router history, our tabs function perfectly fine even when using the back arrows! 18 | */ 19 | export default function registerTab(name, path, component = () => {}) { 20 | const unobservers = []; 21 | 22 | const addTab = (tabs) => { 23 | const tab = tabs.children[0].cloneNode(true); 24 | makeInactive(tab); 25 | 26 | tab.querySelector(`[class^="_responsiveText_"]`).textContent = name; 27 | 28 | tab.classList.add("__NEPTUNE_TAB"); 29 | 30 | tab.addEventListener("click", (e) => { 31 | e.preventDefault(); 32 | 33 | actions.router.push({ 34 | pathname: `/neptune/${path}`, 35 | replace: true, 36 | }); 37 | }); 38 | 39 | const removeRouteHandler = registerRoute(path, () => { 40 | tab.style.color = "var(--wave-color-solid-accent-fill)"; 41 | 42 | return component; 43 | }); 44 | 45 | tabs.appendChild(tab); 46 | unobservers.push(removeRouteHandler, () => tabs.removeChild(tab)); 47 | }; 48 | 49 | const tabs = getTabs(); 50 | 51 | if (!tabs) { 52 | // Instead of DOM observing we just intercept the first action that gets called after the UI shows. Maybe we can DOM observe later? 53 | unobservers.push( 54 | intercept( 55 | "favorites/SET_FAVORITE_IDS", 56 | () => { 57 | addTab(getTabs()); 58 | }, 59 | true, 60 | ), 61 | ); 62 | } else addTab(tabs); 63 | 64 | return () => unobservers.forEach((u) => u()); 65 | } 66 | -------------------------------------------------------------------------------- /src/ui/themesTab.js: -------------------------------------------------------------------------------- 1 | import { $, For, html, useCleanup } from "voby"; 2 | import { TextInput, Switch, TrashIcon } from "./components"; 3 | import { themesStore, toggleTheme, removeTheme, importTheme } from "../api/themes"; 4 | import { actions } from "../handleExfiltrations.js"; 5 | import { appendStyle } from "../api/utils.js"; 6 | 7 | export function ThemesTab() { 8 | const themeToImport = $(""); 9 | 10 | return html` 11 |
12 | <${TextInput} onEnter=${() => { 13 | importTheme(themeToImport(), false).catch((e) => actions.message.messageError({ message: e })); 14 | themeToImport(""); 15 | }} value=${themeToImport} placeholder="Import theme from URL" /> 16 | <${For} values=${() => themesStore}> 17 | ${(theme) => html`<${ThemeCard} theme=${theme} />`} 18 | 19 |
`; 20 | } 21 | 22 | function ThemeCard({ theme}) { 23 | // TODO: i have no fucking clue why this needs a try catch to not implode in on itself lmfao 24 | useCleanup(() => { try { removeStyle() } catch {} }); 25 | 26 | let removeStyle = () => {}; 27 | 28 | return html`
29 |
30 |
31 |
32 | ${theme.name} 33 | by 34 | ${theme.author} 35 |
36 |
${theme.description}
37 |
38 |
39 | 42 |
{ 43 | try { 44 | removeStyle(); 45 | } catch {} 46 | removeStyle = appendStyle(`@import url("${theme.url}");`) 47 | }} 48 | 49 | onmouseout=${() => removeStyle()} 50 | > 51 | <${Switch} checked=${() => theme.enabled} onClick=${() => toggleTheme(theme.url)} /> 52 |
53 |
54 |
55 |
` 56 | } -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | 6 | outputs = inputs: 7 | let 8 | neptuneOverlay = final: prev: 9 | let 10 | neptune-src = prev.fetchzip { 11 | url = "https://github.com/uwu/neptune/archive/548f93b.zip"; 12 | sha256 = "sha256-oI/bRjL6zjsaA8p8QTeJEB5k+SXkJqSJ/hEAltDenok="; 13 | }; 14 | in 15 | { 16 | # Use the already existing package tidal-hifi and inject neptune in it 17 | tidal-hifi = prev.tidal-hifi.overrideAttrs (old: { 18 | 19 | # Patch neptune into tidal-hifi 20 | # Needing to override the full thing to get everything from the install phase 21 | installPhase = '' 22 | runHook preInstall 23 | 24 | mkdir -p "$out/bin" 25 | cp -R "opt" "$out" 26 | cp -R "usr/share" "$out/share" 27 | chmod -R g-w "$out" 28 | 29 | cp -r ${neptune-src}/injector/ $out/opt/tidal-hifi/resources/app/ 30 | mv $out/opt/tidal-hifi/resources/app.asar $out/opt/tidal-hifi/resources/original.asar 31 | 32 | runHook postInstall 33 | 34 | ''; 35 | }); 36 | 37 | # declare a new package named neptune that uses the new tidal-hifi 38 | neptune = final.tidal-hifi; 39 | }; 40 | 41 | system = "x86_64-linux"; # This setting is based on my system, change if needed 42 | 43 | # The Module Configuration 44 | # Disclaimer: There is no module configuration yet, because I dont have a solution myself, to get access to the InnoDB 45 | 46 | in { 47 | # Overlay used by other flakes 48 | overlays.default = neptuneOverlay; 49 | 50 | # Testing the overlay/package 51 | devShells."${system}".default = let 52 | pkgs = import inputs.nixpkgs { 53 | inherit system; 54 | overlays = [ neptuneOverlay ]; 55 | }; 56 | in pkgs.mkShell { 57 | packages = [ pkgs.neptune ]; 58 | shellHook = '' ${pkgs.neptune}/bin/tidal-hifi ''; # Activating the package/overlay 59 | }; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /types/api/utils.d.ts: -------------------------------------------------------------------------------- 1 | import type { UseStore } from "idb-keyval"; 2 | 3 | interface StyleFn { 4 | /** 5 | * Removes