From 138c7e815cdfec79c086c83b22e1944a95afe502 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Wed, 4 Mar 2026 19:41:57 +0000 Subject: [PATCH] feat: settings modal + player hijack concept --- package.json | 3 + pnpm-lock.yaml | 187 +++++++++++++++++++++++++++++++++++++++ src/app.tsx | 76 +++++++++++++++- src/settings.tsx | 173 ++++++++++++++++++++++++++++++++++++ src/styles.module.css | 98 ++++++++++++++++++++ src/types/spicetify.d.ts | 54 +++++------ 6 files changed, 556 insertions(+), 35 deletions(-) create mode 100644 src/settings.tsx create mode 100644 src/styles.module.css diff --git a/package.json b/package.json index a91a6bb..5de7f60 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,8 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "spicetify-creator": "^1.0.17" + }, + "dependencies": { + "@jellyfin/sdk": "^0.13.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1500af5..2e9a97e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@jellyfin/sdk': + specifier: ^0.13.0 + version: 0.13.0(axios@1.13.6) devDependencies: '@types/react': specifier: ^19.2.14 @@ -33,6 +37,11 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jellyfin/sdk@0.13.0': + resolution: {integrity: sha512-oiBAOXH6s+dKdReSsYgNktBDzbxtg4JVWhEzIxZSxKcWMdSKmBtK41MhXRO7IWAC40DguKUm3nU/Z493qPAlWA==} + peerDependencies: + axios: ^1.12.0 + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -149,6 +158,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -160,6 +172,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -179,6 +194,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001776: resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} @@ -201,6 +220,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -232,10 +255,18 @@ packages: supports-color: optional: true + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -252,6 +283,22 @@ packages: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild-android-64@0.14.54: resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} engines: {node: '>=12'} @@ -412,10 +459,23 @@ packages: resolution: {integrity: sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==} engines: {node: '>=0.10.0'} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -436,6 +496,14 @@ packages: generic-names@4.0.0: resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -453,6 +521,10 @@ packages: resolution: {integrity: sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==} engines: {node: '>=0.10.0'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -460,6 +532,14 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -565,6 +645,18 @@ packages: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -687,6 +779,9 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -856,6 +951,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jellyfin/sdk@0.13.0(axios@1.13.6)': + dependencies: + axios: 1.13.6 + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -938,6 +1037,8 @@ snapshots: ansi-styles@6.2.3: {} + asynckit@0.4.0: {} + at-least-node@1.0.0: {} autoprefixer@10.4.27(postcss@8.5.8): @@ -949,6 +1050,14 @@ snapshots: postcss: 8.5.8 postcss-value-parser: 4.2.0 + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} baseline-browser-mapping@2.10.0: {} @@ -970,6 +1079,11 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001776: {} chalk@4.1.2: @@ -991,6 +1105,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} copy-anything@2.0.6: @@ -1016,9 +1134,17 @@ snapshots: dependencies: ms: 2.1.3 + delayed-stream@1.0.0: {} + detect-libc@2.1.2: optional: true + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.307: {} @@ -1032,6 +1158,21 @@ snapshots: prr: 1.0.1 optional: true + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild-android-64@0.14.54: optional: true @@ -1153,11 +1294,21 @@ snapshots: dependencies: find-file-up: 0.1.3 + follow-redirects@1.15.11: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fraction.js@5.3.4: {} fs-exists-sync@0.1.0: {} @@ -1177,6 +1328,24 @@ snapshots: dependencies: loader-utils: 3.3.1 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -1207,10 +1376,18 @@ snapshots: is-windows: 0.2.0 which: 1.3.1 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -1314,6 +1491,14 @@ snapshots: semver: 5.7.2 optional: true + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime@1.6.0: optional: true @@ -1421,6 +1606,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + proxy-from-env@1.1.0: {} + prr@1.0.1: optional: true diff --git a/src/app.tsx b/src/app.tsx index 9fb4fae..6b7c513 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,8 +1,82 @@ +// TODO: hijack search result, use that as song URI + +import React from "react"; +import { Api, Jellyfin } from "@jellyfin/sdk"; +import SettingsModal from "./settings"; + +const audio = new Audio("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"); +let hijackActive = false; + +export const jellyfin = new Jellyfin({ + clientInfo: { + name: "Spicetify", + version: "1.0.0", + }, + deviceInfo: { + name: "Spotify", + id: "spotify", // TODO: should be unique? + }, +}); + +export let jellyfinApi: Api | undefined; +export const setJellyfinApi = (api: Api) => { + jellyfinApi = api; +}; + async function main() { - while (!Spicetify?.showNotification) { + while (!Spicetify.showNotification || !Spicetify.Platform.History) { await new Promise((resolve) => setTimeout(resolve, 100)); } + new Spicetify.Topbar.Button("Jellyfin", "podcasts", () => { + Spicetify.PopupModal.display({ + title: "Jellyfin", + content: React.createElement(SettingsModal) as unknown as Element, + isLarge: false, + }); + }); + + Spicetify.Platform.History.listen((location) => { + if (location.pathname.startsWith("/search/")) { + const segments = location.pathname.split("/"); + const query = segments[2]; + } + }); + + Spicetify.Player.addEventListener("songchange", async (event) => { + // if (event?.data.item.uri === "spotify:track:72wehM3q2RVZb4XLmAkyTr") { + const oldVolume = Spicetify.Player.getVolume(); + await audio.play(); + + Spicetify.Player.setVolume(0); + hijackActive = true; + Spicetify.Player.setVolume(oldVolume); + }); + + Spicetify.Player.addEventListener("onplaypause", async (event) => { + if (!hijackActive) return; + + if (event?.data.isPaused) { + audio.pause(); + } else { + await audio.play(); + } + }); + + const playback = Spicetify.Platform.PlaybackAPI; + + // Change volume of Jellyfin audio instead of Spotify audio + playback.setVolume = new Proxy(playback.setVolume, { + apply(target, thisArg, args) { + if (hijackActive) { + audio.volume = args[0]; + return; + } else { + return Reflect.apply(target, thisArg, args); + } + }, + }); + // Show message on start. Spicetify.showNotification("Hello!"); } diff --git a/src/settings.tsx b/src/settings.tsx new file mode 100644 index 0000000..dc6b474 --- /dev/null +++ b/src/settings.tsx @@ -0,0 +1,173 @@ +import React, { useState } from "react"; +import { getUserApi } from "@jellyfin/sdk/lib/utils/api/user-api"; + +import styles from "./styles.module.css"; +import { jellyfin, setJellyfinApi } from "./app"; + +export default function SettingsModal() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [url, setUrl] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isUsingQuickConnect, setIsUsingQuickConnect] = useState(false); + const [quickConnectCode, setQuickConnectCode] = useState(""); + + const [isFocused, setIsFocused] = useState(false); + + const login = async () => { + const servers = await jellyfin.discovery.getRecommendedServerCandidates(url); + const best = jellyfin.discovery.findBestServer(servers); + if (!best) { + Spicetify.showNotification("Failed to connect to server!", true); + return; + } + const api = jellyfin.createApi(best.address); + const userApi = getUserApi(api); + + const auth = + isUsingQuickConnect && quickConnectCode.toString().length === 6 + ? await userApi.authenticateWithQuickConnect({ + quickConnectDto: { Secret: "111000" }, + }) + : await userApi.authenticateUserByName({ + authenticateUserByName: { Username: username, Pw: password }, + }); + + if (!auth.data.AccessToken) { + Spicetify.showNotification("Failed to login!", true); + return; + } + + setJellyfinApi(api); + setIsLoggedIn(true); + }; + + if (isLoggedIn) + return ( +
+ + + + + + + + + + +

You're logged in!

+ + + +
+ +
+ ); + + return ( +
+ {isUsingQuickConnect ? ( +
+ + +
+ setQuickConnectCode(e.target.value.replace(/\D/g, ""))} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + // Force caret to always be at the end + onKeyDown={(e) => { + if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) { + e.preventDefault(); + } + }} + // Same here + onSelect={(e) => { + const element = e.target as HTMLInputElement; + element.setSelectionRange(element.value.length, element.value.length); + }} + className={styles.quick_connect_input} + /> + + {Array.from({ length: 6 }).map((_, i) => ( +
+ {quickConnectCode[i]} +
+ ))} +
+
+ ) : ( + <> +
+ + setUrl(e.target.value)} /> +
+ +
+ + setUsername(e.target.value)} /> +
+ +
+ + setPassword(e.target.value)} /> +
+ + )} + +
+
+ or +
+
+ + + +
+ ); +} diff --git a/src/styles.module.css b/src/styles.module.css new file mode 100644 index 0000000..b759354 --- /dev/null +++ b/src/styles.module.css @@ -0,0 +1,98 @@ +.button { + background-color: var(--spice-button); + color: var(--spice-text); + padding: 0.5rem 1rem; + border: none; + outline: none; + cursor: pointer; + width: fit-content; +} + +.hr { + flex-grow: 1; + border: none; + border-top: 1px solid var(--spice-button-disabled); +} + +.modal { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.logged_in { + font-size: 1.25rem; + font-weight: 600; +} + +.input_container { + display: flex; + flex-direction: column; + width: 100%; +} + +.input_container label { + color: var(--spice-subtext); + margin-bottom: 0.25rem; +} + +.input_container input, +.quick_connect_box { + background-color: var(--spice-main-elevated); + color: var(--spice-text); + border: 1px solid var(--spice-card); + padding: 0.5rem 0.6rem; + font-size: 0.95rem; + transition: 200ms border-color; +} + +.input_container input:focus, +.quick_connect_box_active { + border-color: var(--spice-button); +} + +.input_container input::placeholder { + color: var(--spice-subtext); + opacity: 0.5; +} + +.separator { + width: 100%; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--spice-subtext); +} + +.quick_connect { + background-color: var(--spice-main-elevated); +} + +.quick_connect_wrapper { + position: relative; + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 0.25rem; + height: 3rem; +} + +.quick_connect_input { + background-color: transparent !important; + border-color: transparent !important; + color: transparent !important; + position: absolute; + inset: 0; + z-index: 5; +} + +.quick_connect_box { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + border-radius: 0.25rem; + font-size: 1.25rem; + font-weight: 500; +} diff --git a/src/types/spicetify.d.ts b/src/types/spicetify.d.ts index 5e99878..0415f7f 100644 --- a/src/types/spicetify.d.ts +++ b/src/types/spicetify.d.ts @@ -325,8 +325,8 @@ declare namespace Spicetify { */ container: HTMLElement; }; - } - ) => void + }, + ) => void, ): void; /** * Skip to previous track. @@ -519,19 +519,8 @@ declare namespace Spicetify { function put(url: string, body?: Body, headers?: Headers): Promise; function del(url: string, body?: Body, headers?: Headers): Promise; function patch(url: string, body?: Body, headers?: Headers): Promise; - function sub( - url: string, - callback: (b: Response["body"]) => void, - onError?: (e: Error) => void, - body?: Body, - headers?: Headers - ): Promise; - function postSub( - url: string, - body: Body | null, - callback: (b: Response["body"]) => void, - onError?: (e: Error) => void - ): Promise; + function sub(url: string, callback: (b: Response["body"]) => void, onError?: (e: Error) => void, body?: Body, headers?: Headers): Promise; + function postSub(url: string, body: Body | null, callback: (b: Response["body"]) => void, onError?: (e: Error) => void): Promise; function request(method: Method, url: string, body?: Body, headers?: Headers): Promise; function resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise; } @@ -786,7 +775,18 @@ declare namespace Spicetify { * Contains vast array of internal APIs. * Please explore in Devtool Console. */ - const Platform: any; + const Platform: { + PlaybackAPI: any; + History: { + push: (path: Location | string) => void; + replace: (path: Location | string) => void; + goBack: () => void; + goForward: () => void; + listen: (listener: (location: Location) => void) => () => void; + entries: Location[]; + location: Location; + }; + }; /** * Queue object contains list of queuing tracks, * history of played tracks and current track metadata. @@ -1833,14 +1833,7 @@ declare namespace Spicetify { * Create a button on the right side of the playbar */ class Button { - constructor( - label: string, - icon: Icon | string, - onClick?: (self: Button) => void, - disabled?: boolean, - active?: boolean, - registerOnCreate?: boolean - ); + constructor(label: string, icon: Icon | string, onClick?: (self: Button) => void, disabled?: boolean, active?: boolean, registerOnCreate?: boolean); label: string; icon: string; onClick: (self: Button) => void; @@ -1856,14 +1849,7 @@ declare namespace Spicetify { * Create a widget next to track info */ class Widget { - constructor( - label: string, - icon: Icon | string, - onClick?: (self: Widget) => void, - disabled?: boolean, - active?: boolean, - registerOnCreate?: boolean - ); + constructor(label: string, icon: Icon | string, onClick?: (self: Widget) => void, disabled?: boolean, active?: boolean, registerOnCreate?: boolean); label: string; icon: string; onClick: (self: Widget) => void; @@ -2056,7 +2042,7 @@ declare namespace Spicetify { * @return Function to handle GraphQL queries */ function Handler( - context: Record + context: Record, ): (query: (typeof Definitions)[Query | string], variables?: Record, context?: Record) => Promise; } @@ -2077,7 +2063,7 @@ declare namespace Spicetify { label?: string, contextUri?: string, sectionIndex?: number, - dropOriginUri?: string + dropOriginUri?: string, ): (event: React.DragEvent, uris?: string[], label?: string, contextUri?: string, sectionIndex?: number) => void; /**