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; /**