From c66f1612fb0daa58e0373ee914f9ad7ae562aadf Mon Sep 17 00:00:00 2001 From: Evie Date: Sun, 11 May 2025 12:59:13 +0700 Subject: [PATCH 1/4] chore(plugins/SpotifyModal): push new src --- .../{src => .src}/components/Buttons.tsx | 0 .../components/ControlContextMenu.tsx | 0 .../{src => .src}/components/Controls.tsx | 0 .../{src => .src}/components/Icons.tsx | 0 .../{src => .src}/components/Modal.tsx | 0 .../components/Popouts/AuthLinkGenerator.tsx | 0 .../{src => .src}/components/Popouts/index.ts | 0 .../{src => .src}/components/Seekbar.tsx | 0 .../{src => .src}/components/Settings.tsx | 0 .../{src => .src}/components/TrackDetails.tsx | 0 .../{src => .src}/components/index.ts | 0 plugins/SpotifyModal/{src => .src}/config.ts | 0 plugins/SpotifyModal/.src/index.tsx | 95 ++++++++++++++ plugins/SpotifyModal/{src => .src}/patches.ts | 0 .../SpotifyModal/{src => .src}/style/icon.css | 0 .../{src => .src}/style/index.css | 0 .../SpotifyModal/{src => .src}/style/misc.css | 0 .../{src => .src}/style/modal/controls.css | 0 .../{src => .src}/style/modal/index.css | 0 .../{src => .src}/style/modal/main-view.css | 0 .../{src => .src}/style/modal/seekbar.css | 0 .../style/modal/track-details.css | 0 .../{src => .src}/style/scrolling-anim.css | 0 .../{src => .src}/types/events.ts | 0 .../SpotifyModal/{src => .src}/types/index.ts | 0 .../{src => .src}/types/stores.ts | 0 .../SpotifyModal/{src => .src}/util/events.ts | 0 .../SpotifyModal/{src => .src}/util/index.ts | 0 .../SpotifyModal/{src => .src}/util/misc.ts | 0 .../{src => .src}/util/spotify.ts | 0 plugins/SpotifyModal/manifest.json | 2 - plugins/SpotifyModal/src/Modal.tsx | 124 ++++++++++++++++++ plugins/SpotifyModal/src/index.tsx | 116 +++++----------- plugins/SpotifyModal/src/style.css | 107 +++++++++++++++ plugins/SpotifyModal/src/types.ts | 16 +++ pnpm-workspace.yaml | 4 + 36 files changed, 382 insertions(+), 82 deletions(-) rename plugins/SpotifyModal/{src => .src}/components/Buttons.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/ControlContextMenu.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/Controls.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/Icons.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/Modal.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/Popouts/AuthLinkGenerator.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/Popouts/index.ts (100%) rename plugins/SpotifyModal/{src => .src}/components/Seekbar.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/Settings.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/TrackDetails.tsx (100%) rename plugins/SpotifyModal/{src => .src}/components/index.ts (100%) rename plugins/SpotifyModal/{src => .src}/config.ts (100%) create mode 100644 plugins/SpotifyModal/.src/index.tsx rename plugins/SpotifyModal/{src => .src}/patches.ts (100%) rename plugins/SpotifyModal/{src => .src}/style/icon.css (100%) rename plugins/SpotifyModal/{src => .src}/style/index.css (100%) rename plugins/SpotifyModal/{src => .src}/style/misc.css (100%) rename plugins/SpotifyModal/{src => .src}/style/modal/controls.css (100%) rename plugins/SpotifyModal/{src => .src}/style/modal/index.css (100%) rename plugins/SpotifyModal/{src => .src}/style/modal/main-view.css (100%) rename plugins/SpotifyModal/{src => .src}/style/modal/seekbar.css (100%) rename plugins/SpotifyModal/{src => .src}/style/modal/track-details.css (100%) rename plugins/SpotifyModal/{src => .src}/style/scrolling-anim.css (100%) rename plugins/SpotifyModal/{src => .src}/types/events.ts (100%) rename plugins/SpotifyModal/{src => .src}/types/index.ts (100%) rename plugins/SpotifyModal/{src => .src}/types/stores.ts (100%) rename plugins/SpotifyModal/{src => .src}/util/events.ts (100%) rename plugins/SpotifyModal/{src => .src}/util/index.ts (100%) rename plugins/SpotifyModal/{src => .src}/util/misc.ts (100%) rename plugins/SpotifyModal/{src => .src}/util/spotify.ts (100%) create mode 100644 plugins/SpotifyModal/src/Modal.tsx create mode 100644 plugins/SpotifyModal/src/style.css create mode 100644 plugins/SpotifyModal/src/types.ts create mode 100644 pnpm-workspace.yaml diff --git a/plugins/SpotifyModal/src/components/Buttons.tsx b/plugins/SpotifyModal/.src/components/Buttons.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Buttons.tsx rename to plugins/SpotifyModal/.src/components/Buttons.tsx diff --git a/plugins/SpotifyModal/src/components/ControlContextMenu.tsx b/plugins/SpotifyModal/.src/components/ControlContextMenu.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/ControlContextMenu.tsx rename to plugins/SpotifyModal/.src/components/ControlContextMenu.tsx diff --git a/plugins/SpotifyModal/src/components/Controls.tsx b/plugins/SpotifyModal/.src/components/Controls.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Controls.tsx rename to plugins/SpotifyModal/.src/components/Controls.tsx diff --git a/plugins/SpotifyModal/src/components/Icons.tsx b/plugins/SpotifyModal/.src/components/Icons.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Icons.tsx rename to plugins/SpotifyModal/.src/components/Icons.tsx diff --git a/plugins/SpotifyModal/src/components/Modal.tsx b/plugins/SpotifyModal/.src/components/Modal.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Modal.tsx rename to plugins/SpotifyModal/.src/components/Modal.tsx diff --git a/plugins/SpotifyModal/src/components/Popouts/AuthLinkGenerator.tsx b/plugins/SpotifyModal/.src/components/Popouts/AuthLinkGenerator.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Popouts/AuthLinkGenerator.tsx rename to plugins/SpotifyModal/.src/components/Popouts/AuthLinkGenerator.tsx diff --git a/plugins/SpotifyModal/src/components/Popouts/index.ts b/plugins/SpotifyModal/.src/components/Popouts/index.ts similarity index 100% rename from plugins/SpotifyModal/src/components/Popouts/index.ts rename to plugins/SpotifyModal/.src/components/Popouts/index.ts diff --git a/plugins/SpotifyModal/src/components/Seekbar.tsx b/plugins/SpotifyModal/.src/components/Seekbar.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Seekbar.tsx rename to plugins/SpotifyModal/.src/components/Seekbar.tsx diff --git a/plugins/SpotifyModal/src/components/Settings.tsx b/plugins/SpotifyModal/.src/components/Settings.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/Settings.tsx rename to plugins/SpotifyModal/.src/components/Settings.tsx diff --git a/plugins/SpotifyModal/src/components/TrackDetails.tsx b/plugins/SpotifyModal/.src/components/TrackDetails.tsx similarity index 100% rename from plugins/SpotifyModal/src/components/TrackDetails.tsx rename to plugins/SpotifyModal/.src/components/TrackDetails.tsx diff --git a/plugins/SpotifyModal/src/components/index.ts b/plugins/SpotifyModal/.src/components/index.ts similarity index 100% rename from plugins/SpotifyModal/src/components/index.ts rename to plugins/SpotifyModal/.src/components/index.ts diff --git a/plugins/SpotifyModal/src/config.ts b/plugins/SpotifyModal/.src/config.ts similarity index 100% rename from plugins/SpotifyModal/src/config.ts rename to plugins/SpotifyModal/.src/config.ts diff --git a/plugins/SpotifyModal/.src/index.tsx b/plugins/SpotifyModal/.src/index.tsx new file mode 100644 index 0000000..c020a9b --- /dev/null +++ b/plugins/SpotifyModal/.src/index.tsx @@ -0,0 +1,95 @@ +import React from 'react'; + +import { fluxDispatcher } from 'replugged/common'; +import { ErrorBoundary } from 'replugged/components'; + +import { mergeClassNames } from '@shared/dom'; +import { hackyCSSFix, restartDiscordDialog } from '@shared/misc'; + +import { config } from './config'; +import { ErrorPlaceholder, Modal } from './components'; +import { containerClasses, globalEvents, initMisc, initSpotify, logger } from './util'; +import { SpotifyStore } from './types'; + +import './style/index.css'; + +let styleElement: HTMLLinkElement; + +export const renderModal = (): React.ReactElement => ( +
+ +
+ +
+
+ } + onError={(error: Error, message: React.ErrorInfo) => + logger._.error('(modal)', `rendering failed\n`, error, '\n', message) + }> + + + +); + +export const emitEvent = ( + data: SpotifyStore.PayloadEvents, + account: SpotifyStore.Account, +): void => { + if (data.type === 'PLAYER_STATE_CHANGED' && typeof data.event.state?.timestamp === 'number') + data.event.state.timestamp = Date.now(); + + globalEvents.emit('event', { accountId: account.accountId, data }); +}; + +const postConnectionOpenListener = (): void => { + fluxDispatcher.unsubscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); + + logger.log('(start)', 'waited for POST_CONNECTION_OPEN'); + globalEvents.emit('ready'); + + // hacky fix for loading css after timing out + if (!document.querySelector('link[href*="lib.evelyn.SpotifyModal"]')) { + logger._.log( + '(start)', + 'manually loading CSS since Replugged timed us out (we still loaded successfully!)', + ); + + styleElement = hackyCSSFix('lib.evelyn.SpotifyModal')!; + } +}; + +// to detect account switches - we need to reset the modal +const loginSuccessListener = (): void => { + globalEvents.emit('accountSwitch'); + fluxDispatcher.subscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); +}; + +export const start = async (): Promise => { + await Promise.allSettled([initMisc(), initSpotify()]); + + if (!document.getElementById('spotify-modal-root')) + fluxDispatcher.subscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); + + fluxDispatcher.subscribe('LOGIN_SUCCESS', loginSuccessListener); +}; + +export const stop = async (): Promise => { + await restartDiscordDialog('SpotifyModal', config.get('pluginStopBehavior')); + + fluxDispatcher.unsubscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); + fluxDispatcher.unsubscribe('LOGIN_SUCCESS', loginSuccessListener); + + styleElement?.remove?.(); +}; + +export { Settings } from './components'; + +export * as util from './util'; +export * as components from './components'; diff --git a/plugins/SpotifyModal/src/patches.ts b/plugins/SpotifyModal/.src/patches.ts similarity index 100% rename from plugins/SpotifyModal/src/patches.ts rename to plugins/SpotifyModal/.src/patches.ts diff --git a/plugins/SpotifyModal/src/style/icon.css b/plugins/SpotifyModal/.src/style/icon.css similarity index 100% rename from plugins/SpotifyModal/src/style/icon.css rename to plugins/SpotifyModal/.src/style/icon.css diff --git a/plugins/SpotifyModal/src/style/index.css b/plugins/SpotifyModal/.src/style/index.css similarity index 100% rename from plugins/SpotifyModal/src/style/index.css rename to plugins/SpotifyModal/.src/style/index.css diff --git a/plugins/SpotifyModal/src/style/misc.css b/plugins/SpotifyModal/.src/style/misc.css similarity index 100% rename from plugins/SpotifyModal/src/style/misc.css rename to plugins/SpotifyModal/.src/style/misc.css diff --git a/plugins/SpotifyModal/src/style/modal/controls.css b/plugins/SpotifyModal/.src/style/modal/controls.css similarity index 100% rename from plugins/SpotifyModal/src/style/modal/controls.css rename to plugins/SpotifyModal/.src/style/modal/controls.css diff --git a/plugins/SpotifyModal/src/style/modal/index.css b/plugins/SpotifyModal/.src/style/modal/index.css similarity index 100% rename from plugins/SpotifyModal/src/style/modal/index.css rename to plugins/SpotifyModal/.src/style/modal/index.css diff --git a/plugins/SpotifyModal/src/style/modal/main-view.css b/plugins/SpotifyModal/.src/style/modal/main-view.css similarity index 100% rename from plugins/SpotifyModal/src/style/modal/main-view.css rename to plugins/SpotifyModal/.src/style/modal/main-view.css diff --git a/plugins/SpotifyModal/src/style/modal/seekbar.css b/plugins/SpotifyModal/.src/style/modal/seekbar.css similarity index 100% rename from plugins/SpotifyModal/src/style/modal/seekbar.css rename to plugins/SpotifyModal/.src/style/modal/seekbar.css diff --git a/plugins/SpotifyModal/src/style/modal/track-details.css b/plugins/SpotifyModal/.src/style/modal/track-details.css similarity index 100% rename from plugins/SpotifyModal/src/style/modal/track-details.css rename to plugins/SpotifyModal/.src/style/modal/track-details.css diff --git a/plugins/SpotifyModal/src/style/scrolling-anim.css b/plugins/SpotifyModal/.src/style/scrolling-anim.css similarity index 100% rename from plugins/SpotifyModal/src/style/scrolling-anim.css rename to plugins/SpotifyModal/.src/style/scrolling-anim.css diff --git a/plugins/SpotifyModal/src/types/events.ts b/plugins/SpotifyModal/.src/types/events.ts similarity index 100% rename from plugins/SpotifyModal/src/types/events.ts rename to plugins/SpotifyModal/.src/types/events.ts diff --git a/plugins/SpotifyModal/src/types/index.ts b/plugins/SpotifyModal/.src/types/index.ts similarity index 100% rename from plugins/SpotifyModal/src/types/index.ts rename to plugins/SpotifyModal/.src/types/index.ts diff --git a/plugins/SpotifyModal/src/types/stores.ts b/plugins/SpotifyModal/.src/types/stores.ts similarity index 100% rename from plugins/SpotifyModal/src/types/stores.ts rename to plugins/SpotifyModal/.src/types/stores.ts diff --git a/plugins/SpotifyModal/src/util/events.ts b/plugins/SpotifyModal/.src/util/events.ts similarity index 100% rename from plugins/SpotifyModal/src/util/events.ts rename to plugins/SpotifyModal/.src/util/events.ts diff --git a/plugins/SpotifyModal/src/util/index.ts b/plugins/SpotifyModal/.src/util/index.ts similarity index 100% rename from plugins/SpotifyModal/src/util/index.ts rename to plugins/SpotifyModal/.src/util/index.ts diff --git a/plugins/SpotifyModal/src/util/misc.ts b/plugins/SpotifyModal/.src/util/misc.ts similarity index 100% rename from plugins/SpotifyModal/src/util/misc.ts rename to plugins/SpotifyModal/.src/util/misc.ts diff --git a/plugins/SpotifyModal/src/util/spotify.ts b/plugins/SpotifyModal/.src/util/spotify.ts similarity index 100% rename from plugins/SpotifyModal/src/util/spotify.ts rename to plugins/SpotifyModal/.src/util/spotify.ts diff --git a/plugins/SpotifyModal/manifest.json b/plugins/SpotifyModal/manifest.json index deba8f6..9974ad3 100644 --- a/plugins/SpotifyModal/manifest.json +++ b/plugins/SpotifyModal/manifest.json @@ -16,8 +16,6 @@ "license": "MIT", "type": "replugged-plugin", "renderer": "src/index.tsx", - "reloadRequired": true, - "plaintextPatches": "src/patches.ts", "source": "https://github.com/Socketlike/replugged-plugins/blob/main/plugins/SpotifyModal", "image": "https://media.discordapp.net/attachments/907813363294277652/1140263816823844864/DiscordCanary_qrl9e1908P.gif" } diff --git a/plugins/SpotifyModal/src/Modal.tsx b/plugins/SpotifyModal/src/Modal.tsx new file mode 100644 index 0000000..810a5d4 --- /dev/null +++ b/plugins/SpotifyModal/src/Modal.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { Logger } from 'replugged'; +import { ErrorBoundary } from 'replugged/components'; + +import { SpotifyStore } from './types'; + +const log = Logger.plugin('SpotifyModal', '#1DB954'); + +export const ModalFallback = (): React.ReactElement => ( + <>uh oh. something went wrong while rendering the modal. +); + +export const Modal = (props: { + store: SpotifyStore; + fluxHooks: typeof import('replugged/common').fluxHooks; +}): React.ReactElement => { + const { store, fluxHooks } = props; + + const [state, setState] = React.useState>(); + const [active, setActive] = React.useState(false); + const [paused, setPaused] = React.useState(true); + + const socket = fluxHooks.useStateFromStores([store], () => { + const socket = store.getActiveSocketAndDevice()?.socket; + const _state = store.getPlayerState(socket?.accountId); + const _active = Boolean(socket); + + if (active !== _active) { + log.log('active state update', _active); + setActive(_active); + } + + if ((!socket || _active) && state !== _state) { + if (_state) { + log.log('player state update', _state); + setState(_state); + } + + if (paused !== Boolean(_state)) setPaused(Boolean(_state)); + } + + return socket; + }); + + const trackNameElement = React.useRef(); + const artistsElement = React.useRef(); + + function handleOverflow(element: HTMLElement): void { + if (!element?.parentElement) return; + + if (element.scrollWidth > element.parentElement.clientWidth) { + // 60px/s + element.style.animationDuration = `${(element.scrollWidth / 45) * 1.1}s`; + element.style.animationDelay = `-${(element.scrollWidth / 45) * 1.1 * 0.349}s`; + element.classList.add('overflow'); + } else element.classList.remove('overflow'); + } + + React.useEffect(() => { + handleOverflow(trackNameElement.current); + handleOverflow(artistsElement.current); + }, [state]); + + return active && state ? ( + <> +
+ + + +
+ +
+
{ + handleOverflow(e); + artistsElement.current = e; + }} + className='artists'> + {Object.values(state.track.artists || []).map((props, i, arr) => ( + <> + {props.name} + {arr.length - 1 !== i && ', '} + + ))} +
+
+
+
+ + ) : ( + <> + ); +}; + +export default (props: { + store: SpotifyStore; + fluxHooks: typeof import('replugged/common').fluxHooks; +}): React.ReactElement => { + const [error, setError] = React.useState(); + const [info, setInfo] = React.useState(); + + return ( +
+ { + setError(e); + setInfo(i); + }} + fallback={() => }> + + +
+ ); +}; diff --git a/plugins/SpotifyModal/src/index.tsx b/plugins/SpotifyModal/src/index.tsx index c020a9b..4141e2d 100644 --- a/plugins/SpotifyModal/src/index.tsx +++ b/plugins/SpotifyModal/src/index.tsx @@ -1,95 +1,51 @@ -import React from 'react'; +import { Injector, Logger } from 'replugged'; +import { getOwnerInstance, waitFor } from 'replugged/util'; +import { fluxHooks } from 'replugged/common'; +import webpack from 'replugged/webpack'; -import { fluxDispatcher } from 'replugged/common'; -import { ErrorBoundary } from 'replugged/components'; - -import { mergeClassNames } from '@shared/dom'; -import { hackyCSSFix, restartDiscordDialog } from '@shared/misc'; - -import { config } from './config'; -import { ErrorPlaceholder, Modal } from './components'; -import { containerClasses, globalEvents, initMisc, initSpotify, logger } from './util'; import { SpotifyStore } from './types'; +import { default as Main } from './Modal'; -import './style/index.css'; - -let styleElement: HTMLLinkElement; - -export const renderModal = (): React.ReactElement => ( -
- -
- -
-
- } - onError={(error: Error, message: React.ErrorInfo) => - logger._.error('(modal)', `rendering failed\n`, error, '\n', message) - }> - - - -); - -export const emitEvent = ( - data: SpotifyStore.PayloadEvents, - account: SpotifyStore.Account, -): void => { - if (data.type === 'PLAYER_STATE_CHANGED' && typeof data.event.state?.timestamp === 'number') - data.event.state.timestamp = Date.now(); - - globalEvents.emit('event', { accountId: account.accountId, data }); -}; - -const postConnectionOpenListener = (): void => { - fluxDispatcher.unsubscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); +import './style.css'; - logger.log('(start)', 'waited for POST_CONNECTION_OPEN'); - globalEvents.emit('ready'); +const log = Logger.plugin('SpotifyModal', '#1DB954'); +const injector = new Injector(); - // hacky fix for loading css after timing out - if (!document.querySelector('link[href*="lib.evelyn.SpotifyModal"]')) { - logger._.log( - '(start)', - 'manually loading CSS since Replugged timed us out (we still loaded successfully!)', - ); +let store = webpack.getByStoreName('SpotifyStore'); +let userAreaElement: Element; +let forceUpdateUserArea: () => void; - styleElement = hackyCSSFix('lib.evelyn.SpotifyModal')!; - } -}; +let modalInstance =
; -// to detect account switches - we need to reset the modal -const loginSuccessListener = (): void => { - globalEvents.emit('accountSwitch'); - fluxDispatcher.subscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); -}; +export function start(): void { + void (async () => { + store = webpack.getByStoreName('SpotifyStore'); -export const start = async (): Promise => { - await Promise.allSettled([initMisc(), initSpotify()]); + userAreaElement = await waitFor('[class^=panels_] > [class^=container_]'); - if (!document.getElementById('spotify-modal-root')) - fluxDispatcher.subscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); + if (!userAreaElement) { + log.error('unable to get user area element. maybe the selector broke?'); + return; + } - fluxDispatcher.subscribe('LOGIN_SUCCESS', loginSuccessListener); -}; + const owner = getOwnerInstance(userAreaElement); -export const stop = async (): Promise => { - await restartDiscordDialog('SpotifyModal', config.get('pluginStopBehavior')); + if (!owner) { + log.error('unable to get user area React owner instance.'); + return; + } - fluxDispatcher.unsubscribe('POST_CONNECTION_OPEN', postConnectionOpenListener); - fluxDispatcher.unsubscribe('LOGIN_SUCCESS', loginSuccessListener); + injector.after(owner, 'render', (_, res) => { + return [modalInstance, res]; + }); - styleElement?.remove?.(); -}; + forceUpdateUserArea = () => owner.forceUpdate(); -export { Settings } from './components'; + forceUpdateUserArea(); + })(); +} -export * as util from './util'; -export * as components from './components'; +export function stop(): void { + injector.uninjectAll(); + forceUpdateUserArea?.(); +} diff --git a/plugins/SpotifyModal/src/style.css b/plugins/SpotifyModal/src/style.css new file mode 100644 index 0000000..a0827d5 --- /dev/null +++ b/plugins/SpotifyModal/src/style.css @@ -0,0 +1,107 @@ +@keyframes marquee { + 0% { + transform: translateX(60%); + } + + 35% { + transform: translateX(0); + } + + 45% { + transform: translateX(0); + } + + 100% { + transform: translateX(-100%); + } +} + +#spotify-modal { + font-size: 14px; + + color: var(--text-normal); + + border-bottom: 1px solid var(--background-modifier-accent); +} + +#spotify-modal:empty { + border-bottom: none; +} + +#spotify-modal > :not(.divider) { + margin: 8px; +} + +#spotify-modal .details { + display: flex; + + flex-direction: row; + gap: 8px; +} + +#spotify-modal .details .container { + display: flex; + + flex-direction: column; + gap: 4px; + + min-width: 0; + + justify-content: center; +} + +#spotify-modal .details .track-name, +#spotify-modal .details .artists { + text-wrap: nowrap; + + width: min-content; +} + +html.reduce-motion #spotify-modal .details .track-name-container, +html.reduce-motion #spotify-modal .details .artists-container { + text-overflow: ellipsis; +} + +#spotify-modal .details .track-name-container, +#spotify-modal .details .artists-container { + overflow: hidden; +} + +#spotify-modal .details .track-name.overflow, +#spotify-modal .details .artists.overflow { + animation: 0s linear infinite marquee; +} + +html.reduce-motion #spotify-modal .details .track-name, +html.reduce-motion #spotify-modal .details .artists { + animation: none; +} + +#spotify-modal .details .track-name { + display: block; + + font-weight: 500; + + color: var(--text-normal); +} + +#spotify-modal .details .artists a { + color: var(--text-secondary); +} + +#spotify-modal .details .track-name:hover, +#spotify-modal .details .artists:hover { + animation-play-state: paused; +} + +#spotify-modal .details .track-name:hover, +#spotify-modal .details .artists a:hover { + text-decoration: underline; +} + +#spotify-modal .details .cover-art { + border-radius: 8px; + + aspect-ratio: 1 / 1; + width: 56px; +} diff --git a/plugins/SpotifyModal/src/types.ts b/plugins/SpotifyModal/src/types.ts new file mode 100644 index 0000000..488dc92 --- /dev/null +++ b/plugins/SpotifyModal/src/types.ts @@ -0,0 +1,16 @@ +import { getByStoreName } from 'replugged/webpack'; + +export interface SpotifyStore extends ReturnType { + getActiveSocketAndDevice(): { socket: { accountId: string } }; + getPlayerState(id: string): + | (Record & { + track: { + album: { image: { url: string } }; + artists: Array<{ name: string; external_urls: { spotify: string } }>; + name: string; + id: string; + }; + }) + | null; + getActivity(): Record | null; +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..44ba719 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - esbuild + - replugged From d3c841c9b9163795753cee655551983d0b7fb5e6 Mon Sep 17 00:00:00 2001 From: Evelyn <36399055+Socketlike@users.noreply.github.com> Date: Sun, 11 May 2025 15:58:39 +0700 Subject: [PATCH 2/4] fix(SpotifyModal/style): marquee changes 0%: translateX 60% -> 100% kf name changed to `spotify-modal-marquee` to prevent duplication with other potential css --- plugins/SpotifyModal/src/Modal.tsx | 2 +- plugins/SpotifyModal/src/style.css | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/SpotifyModal/src/Modal.tsx b/plugins/SpotifyModal/src/Modal.tsx index 810a5d4..0b9101d 100644 --- a/plugins/SpotifyModal/src/Modal.tsx +++ b/plugins/SpotifyModal/src/Modal.tsx @@ -51,7 +51,7 @@ export const Modal = (props: { if (element.scrollWidth > element.parentElement.clientWidth) { // 60px/s element.style.animationDuration = `${(element.scrollWidth / 45) * 1.1}s`; - element.style.animationDelay = `-${(element.scrollWidth / 45) * 1.1 * 0.349}s`; + element.style.animationDelay = `-${(element.scrollWidth / 45) * 1.1 * 0.449}s`; element.classList.add('overflow'); } else element.classList.remove('overflow'); } diff --git a/plugins/SpotifyModal/src/style.css b/plugins/SpotifyModal/src/style.css index a0827d5..45d704a 100644 --- a/plugins/SpotifyModal/src/style.css +++ b/plugins/SpotifyModal/src/style.css @@ -1,13 +1,13 @@ -@keyframes marquee { +@keyframes spotify-modal-marquee { 0% { - transform: translateX(60%); + transform: translateX(100%); } - 35% { + 45% { transform: translateX(0); } - 45% { + 55% { transform: translateX(0); } @@ -57,11 +57,6 @@ width: min-content; } -html.reduce-motion #spotify-modal .details .track-name-container, -html.reduce-motion #spotify-modal .details .artists-container { - text-overflow: ellipsis; -} - #spotify-modal .details .track-name-container, #spotify-modal .details .artists-container { overflow: hidden; @@ -69,12 +64,17 @@ html.reduce-motion #spotify-modal .details .artists-container { #spotify-modal .details .track-name.overflow, #spotify-modal .details .artists.overflow { - animation: 0s linear infinite marquee; + animation: 0s linear infinite spotify-modal-marquee; } html.reduce-motion #spotify-modal .details .track-name, html.reduce-motion #spotify-modal .details .artists { animation: none; + + text-overflow: ellipsis; + + overflow: hidden; + width: unset; } #spotify-modal .details .track-name { From f7c49fbbfcdcbcfefe2842e7b0ed628a7775423f Mon Sep 17 00:00:00 2001 From: Evelyn <36399055+Socketlike@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:25:54 +0700 Subject: [PATCH 3/4] feat: seekbar n other insignificant changes --- plugins/SpotifyModal/src/Modal.tsx | 234 ++++++++++++++++++++++------- plugins/SpotifyModal/src/index.tsx | 2 +- plugins/SpotifyModal/src/style.css | 33 +++- plugins/SpotifyModal/src/types.ts | 22 ++- plugins/SpotifyModal/src/utils.ts | 18 +++ 5 files changed, 248 insertions(+), 61 deletions(-) create mode 100644 plugins/SpotifyModal/src/utils.ts diff --git a/plugins/SpotifyModal/src/Modal.tsx b/plugins/SpotifyModal/src/Modal.tsx index 0b9101d..fa088ce 100644 --- a/plugins/SpotifyModal/src/Modal.tsx +++ b/plugins/SpotifyModal/src/Modal.tsx @@ -1,88 +1,107 @@ import React from 'react'; import { Logger } from 'replugged'; -import { ErrorBoundary } from 'replugged/components'; +import { ErrorBoundary, SliderItem, Tooltip } from 'replugged/components'; -import { SpotifyStore } from './types'; +import { ConnectedAccount, SpotifyStore } from './types'; +import * as utils from './utils'; const log = Logger.plugin('SpotifyModal', '#1DB954'); -export const ModalFallback = (): React.ReactElement => ( - <>uh oh. something went wrong while rendering the modal. -); +function formatTimestamp(timestamp: number): string { + let seconds = Math.floor(timestamp / 1000); + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; -export const Modal = (props: { - store: SpotifyStore; - fluxHooks: typeof import('replugged/common').fluxHooks; -}): React.ReactElement => { - const { store, fluxHooks } = props; + return `${hours ? `${hours}:` : ''}${String(minutes).padStart(hours ? 2 : 1, '0')}:${String(seconds).padStart(2, '0')}`; +} - const [state, setState] = React.useState>(); - const [active, setActive] = React.useState(false); - const [paused, setPaused] = React.useState(true); +function handleOverflow(element: HTMLElement, parentLevel = 1): void { + if (!element) return; - const socket = fluxHooks.useStateFromStores([store], () => { - const socket = store.getActiveSocketAndDevice()?.socket; - const _state = store.getPlayerState(socket?.accountId); - const _active = Boolean(socket); + let parent = element; + for (let i = 0; i < parentLevel; i++) parent = parent?.parentElement; - if (active !== _active) { - log.log('active state update', _active); - setActive(_active); - } + if (!parent || parent === element) return; - if ((!socket || _active) && state !== _state) { - if (_state) { - log.log('player state update', _state); - setState(_state); - } + if (element.scrollWidth > parent.clientWidth) { + // 60px/s + element.style.animationDuration = `${(element.scrollWidth / 45) * 1.1}s`; + element.style.animationDelay = `-${(element.scrollWidth / 45) * 1.1 * 0.449}s`; + element.classList.add('overflow'); + } else element.classList.remove('overflow'); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const useInterval = (callback: (...args: any[]) => void, delay: number): void => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const savedCallback = React.useRef<(...args: any[]) => void>(); - if (paused !== Boolean(_state)) setPaused(Boolean(_state)); + React.useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + React.useEffect(() => { + if (delay !== null) { + let id = setInterval(() => savedCallback.current(), delay); + return () => clearInterval(id); } + }, [delay]); +}; - return socket; - }); +export const ModalFallback = (): React.ReactElement => ( + <>uh oh. something went wrong while rendering the modal. +); + +export const TrackDetails = (props: { + state: ReturnType; +}): React.ReactElement => { + const { state } = props; const trackNameElement = React.useRef(); const artistsElement = React.useRef(); - function handleOverflow(element: HTMLElement): void { - if (!element?.parentElement) return; - - if (element.scrollWidth > element.parentElement.clientWidth) { - // 60px/s - element.style.animationDuration = `${(element.scrollWidth / 45) * 1.1}s`; - element.style.animationDelay = `-${(element.scrollWidth / 45) * 1.1 * 0.449}s`; - element.classList.add('overflow'); - } else element.classList.remove('overflow'); - } - React.useEffect(() => { - handleOverflow(trackNameElement.current); - handleOverflow(artistsElement.current); + handleOverflow(trackNameElement.current, 2); + handleOverflow(artistsElement.current, 2); }, [state]); - return active && state ? ( - <> -
+ return ( +
+ -
-
+ +
+ -
+ +
+ +
+ props.name) + .join(', ')}>
{ - handleOverflow(e); + handleOverflow(e, 2); artistsElement.current = e; }} className='artists'> @@ -93,9 +112,120 @@ export const Modal = (props: { ))}
-
+
+
+ ); +}; + +export const Seekbar = (props: { + account: ConnectedAccount; + start: number; + end: number; + paused: boolean; + active: boolean; +}): React.ReactNode => { + const { account, start, end, paused, active } = props; + + const [current, setCurrent] = React.useState(0); + const ref = React.useRef<{ setState(props: { value: number }): void }>(); + + const isSeeking = React.useRef(false); + + useInterval(() => { + if (!active || paused || isSeeking.current) return; + + setCurrent(Math.min(Date.now() - start, end)); + }, 500); + + // discord's sliders are no longer dumb, which means they won't react to prop changes + // after first render so we need to set its state manually + React.useEffect(() => { + ref.current?.setState?.({ value: current }); + }, [current]); + + return ( +
+
+ {formatTimestamp(current)} + {formatTimestamp(end)} +
+ { + if (!isSeeking.current) isSeeking.current = true; + + setCurrent(v); + }} + onChange={(v) => { + void utils.spotify.seekTo(account?.accessToken, v).then((res) => { + isSeeking.current = false; + }); + }} + /> +
+ ); +}; + +export const Modal = (props: { + store: SpotifyStore; + fluxHooks: typeof import('replugged/common').fluxHooks; +}): React.ReactElement => { + const { store, fluxHooks } = props; + + const [state, setState] = React.useState>(); + const [activity, setActivity] = React.useState>(); + const [active, setActive] = React.useState(false); + const [paused, setPaused] = React.useState(true); + + const _socket = fluxHooks.useStateFromStores([store], () => { + const socket = store.getActiveSocketAndDevice()?.socket; + const _state = store.getPlayerState(socket?.accountId); + const _active = Boolean(socket); + const _activity = store.getActivity(); + + if (active !== _active) { + log.log('active state update', _active); + setActive(_active); + } + + if ((!socket || _active) && state !== _state) { + if (_state) { + log.log('player state update', _state); + setState(_state); + } + + if (_activity) { + log.log('activity update', _activity); + setActivity(_activity); + } + + if (paused !== !_state) setPaused(!_state); + } + + return socket; + }); + + return active && state ? ( + <> + + ) : ( <> diff --git a/plugins/SpotifyModal/src/index.tsx b/plugins/SpotifyModal/src/index.tsx index 4141e2d..9d806dc 100644 --- a/plugins/SpotifyModal/src/index.tsx +++ b/plugins/SpotifyModal/src/index.tsx @@ -19,7 +19,7 @@ let modalInstance =
; export function start(): void { void (async () => { - store = webpack.getByStoreName('SpotifyStore'); + store ??= webpack.getByStoreName('SpotifyStore'); userAreaElement = await waitFor('[class^=panels_] > [class^=container_]'); diff --git a/plugins/SpotifyModal/src/style.css b/plugins/SpotifyModal/src/style.css index 45d704a..4ebefeb 100644 --- a/plugins/SpotifyModal/src/style.css +++ b/plugins/SpotifyModal/src/style.css @@ -17,10 +17,16 @@ } #spotify-modal { + display: flex; + + flex-direction: column; + gap: 10px; + font-size: 14px; color: var(--text-normal); + margin: 8px; border-bottom: 1px solid var(--background-modifier-accent); } @@ -28,10 +34,6 @@ border-bottom: none; } -#spotify-modal > :not(.divider) { - margin: 8px; -} - #spotify-modal .details { display: flex; @@ -43,6 +45,7 @@ display: flex; flex-direction: column; + flex-grow: 1; gap: 4px; min-width: 0; @@ -78,7 +81,7 @@ html.reduce-motion #spotify-modal .details .artists { } #spotify-modal .details .track-name { - display: block; + display: inline-block; font-weight: 500; @@ -105,3 +108,23 @@ html.reduce-motion #spotify-modal .details .artists { aspect-ratio: 1 / 1; width: 56px; } + +#spotify-modal .seekbar-container .timestamps { + display: flex; + justify-content: space-between; + + margin-bottom: -2px; +} + +#spotify-modal .seekbar { + --grabber-size: 12px !important; + --bar-size: 6px !important; +} + +#spotify-modal .seekbar + [class^='divider'] { + display: none; +} + +#spotify-modal .seekbar .bar [class^='barFill'] { + background-color: var(--spotify); +} diff --git a/plugins/SpotifyModal/src/types.ts b/plugins/SpotifyModal/src/types.ts index 488dc92..ece6434 100644 --- a/plugins/SpotifyModal/src/types.ts +++ b/plugins/SpotifyModal/src/types.ts @@ -1,16 +1,32 @@ import { getByStoreName } from 'replugged/webpack'; -export interface SpotifyStore extends ReturnType { +type FluxStore = ReturnType; + +export interface SpotifyStore extends FluxStore { getActiveSocketAndDevice(): { socket: { accountId: string } }; getPlayerState(id: string): | (Record & { + account: ConnectedAccount; track: { - album: { image: { url: string } }; + album: { image: { url: string }; name: string }; + // eslint-disable-next-line @typescript-eslint/naming-convention artists: Array<{ name: string; external_urls: { spotify: string } }>; name: string; id: string; + duration: number; }; + startTime: number; }) | null; - getActivity(): Record | null; + getActivity(): { + timestamps: { start: number; end: number }; + } | null; +} + +export interface ConnectedAccount { + name: string; + id: string; + type: string; + revoked: boolean; + accessToken?: string; } diff --git a/plugins/SpotifyModal/src/utils.ts b/plugins/SpotifyModal/src/utils.ts new file mode 100644 index 0000000..ff4361e --- /dev/null +++ b/plugins/SpotifyModal/src/utils.ts @@ -0,0 +1,18 @@ +const BASE_URL = 'https://api.spotify.com/v1/'; +export const spotify = { + async seekTo(token: string, position: number): Promise { + if (!token) return false; + + const url = new URL('me/player/seek', BASE_URL); + url.searchParams.set('position_ms', Math.floor(position).toString()); + + return fetch(url, { + method: 'put', + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then((res) => res.ok) + .catch(() => false); + }, +}; From 3b0e722854110cc9efedb2cb07b5e939f94d43e2 Mon Sep 17 00:00:00 2001 From: Evelyn <36399055+Socketlike@users.noreply.github.com> Date: Sun, 8 Jun 2025 12:58:14 +0700 Subject: [PATCH 4/4] feat(SpotifyModal): settings ui, seekbar anim - settings ui reimplemented - seekbar now has animation when hidden on blur --- plugins/SpotifyModal/src/Modal.tsx | 84 +++++++++++++++++++--- plugins/SpotifyModal/src/Settings.tsx | 73 +++++++++++++++++++ plugins/SpotifyModal/src/config.ts | 100 ++++++++++++++++++++++++++ plugins/SpotifyModal/src/index.tsx | 11 ++- plugins/SpotifyModal/src/style.css | 35 ++++++++- plugins/SpotifyModal/src/types.ts | 4 ++ plugins/SpotifyModal/src/utils.ts | 24 +++++-- 7 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 plugins/SpotifyModal/src/Settings.tsx create mode 100644 plugins/SpotifyModal/src/config.ts diff --git a/plugins/SpotifyModal/src/Modal.tsx b/plugins/SpotifyModal/src/Modal.tsx index fa088ce..7278b70 100644 --- a/plugins/SpotifyModal/src/Modal.tsx +++ b/plugins/SpotifyModal/src/Modal.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { Logger } from 'replugged'; +import { fluxHooks, toast } from 'replugged/common'; import { ErrorBoundary, SliderItem, Tooltip } from 'replugged/components'; -import { ConnectedAccount, SpotifyStore } from './types'; +import { ConnectedAccount, ConnectedAccountsUtils, SpotifyStore } from './types'; +import { useConfig } from './config'; import * as utils from './utils'; const log = Logger.plugin('SpotifyModal', '#1DB954'); @@ -121,12 +123,16 @@ export const TrackDetails = (props: { export const Seekbar = (props: { account: ConnectedAccount; + connectedAccountsUtils: ConnectedAccountsUtils; start: number; end: number; paused: boolean; active: boolean; }): React.ReactNode => { - const { account, start, end, paused, active } = props; + const { account, connectedAccountsUtils, start, end, paused, active } = props; + + const enabled = useConfig('seekbar.enabled', true); + const collapseOnBlur = useConfig('seekbar.collapseOnBlur', false); const [current, setCurrent] = React.useState(0); const ref = React.useRef<{ setState(props: { value: number }): void }>(); @@ -146,7 +152,12 @@ export const Seekbar = (props: { }, [current]); return ( -
+
{formatTimestamp(current)} {formatTimestamp(end)} @@ -168,7 +179,55 @@ export const Seekbar = (props: { setCurrent(v); }} onChange={(v) => { - void utils.spotify.seekTo(account?.accessToken, v).then((res) => { + void utils.spotify.seekTo(account?.accessToken, v).then(async (res) => { + if (res === 0) + toast.toast( + '[SpotifyModal] Internal plugin error. Please check console.', + toast.Kind.FAILURE, + ); + else if (res === 401) { + const newToken = await connectedAccountsUtils + .refreshAccountToken('spotify', account.id) + .catch(() => {}); + + if (!newToken) + toast.toast( + '[SpotifyModal] Authentication error: Could not refresh expired token. Please perform this action in your Spotify player.', + toast.Kind.FAILURE, + ); + else { + res = await utils.spotify.seekTo(newToken, v); + + if (res === 401) + toast.toast( + '[SpotifyModal] Authentication error: Could not refresh expired token. Please perform this action in your Spotify player.', + toast.Kind.FAILURE, + ); + else if (res !== 200 && res !== 204) { + log.error("couldn't resume action after refreshing token; code", res); + + toast.toast( + '[SpotifyModal] Resuming action after reauthentication errored. Please check console.', + toast.Kind.FAILURE, + ); + } + } + } else if (res === 404) + toast.toast( + '[SpotifyModal] Player is idle and cannot be accessed. Please perform this action in your Spotify player.', + toast.Kind.FAILURE, + ); + else if (res === 403) + toast.toast( + '[SpotifyModal] Bad OAuth request. The Spotify account was probably unlinked from your Discord account.', + toast.Kind.FAILURE, + ); + else if (res === 429) + toast.toast( + '[SpotifyModal] Too many requests. Please slow down.', + toast.Kind.FAILURE, + ); + isSeeking.current = false; }); }} @@ -179,9 +238,9 @@ export const Seekbar = (props: { export const Modal = (props: { store: SpotifyStore; - fluxHooks: typeof import('replugged/common').fluxHooks; + connectedAccountsUtils: ConnectedAccountsUtils; }): React.ReactElement => { - const { store, fluxHooks } = props; + const { connectedAccountsUtils, store } = props; const [state, setState] = React.useState>(); const [activity, setActivity] = React.useState>(); @@ -221,6 +280,7 @@ export const Modal = (props: { { const [error, setError] = React.useState(); const [info, setInfo] = React.useState(); + const reduceMotion = useConfig('general.reduceMotion', 'discord'); + return ( -
+
{ setError(e); diff --git a/plugins/SpotifyModal/src/Settings.tsx b/plugins/SpotifyModal/src/Settings.tsx new file mode 100644 index 0000000..0255ae3 --- /dev/null +++ b/plugins/SpotifyModal/src/Settings.tsx @@ -0,0 +1,73 @@ +import { Category, SelectItem, SwitchItem, Text, FormItem } from 'replugged/components'; + +import { useSetting } from 'replugged/util'; + +import config from './config'; + +export default (): React.ReactElement => { + const version = useSetting(config, 'v'); + + const general = { + reduceMotion: useSetting(config, 'general.reduceMotion'), + }; + + const controls = { + enabled: useSetting(config, 'controls.enabled'), + collapseOnBlur: useSetting(config, 'controls.collapseOnBlur'), + }; + + const seekbar = { + enabled: useSetting(config, 'seekbar.enabled'), + collapseOnBlur: useSetting(config, 'seekbar.collapseOnBlur'), + }; + + return ( +
+ + { + let val: string | boolean = value; + + if (val !== 'discord') val = val !== 'false'; + + general.reduceMotion.onChange(val); + }}> + Reduce motion + + + + + Enable controls + + Collapse on blur + + + + + Enable seekbar + + Collapse on blur + + + + + settings v{version.value} + +
+ ); +}; diff --git a/plugins/SpotifyModal/src/config.ts b/plugins/SpotifyModal/src/config.ts new file mode 100644 index 0000000..308ffd0 --- /dev/null +++ b/plugins/SpotifyModal/src/config.ts @@ -0,0 +1,100 @@ +import { React } from 'replugged/common'; + +import { init } from 'replugged/settings'; + +// can't use an interface for this +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type Config = { + v: number; + + 'general.reduceMotion': 'discord' | boolean; + + 'controls.enabled': boolean; + 'controls.collapseOnBlur': boolean; + + 'seekbar.enabled': boolean; + 'seekbar.collapseOnBlur': boolean; +}; + +const config = await init('lib.evelyn.SpotifyModal', { + v: 0, + + 'controls.enabled': true, + 'controls.collapseOnBlur': false, + + 'seekbar.enabled': true, + 'seekbar.collapseOnBlur': false, +}); + +const currentConfigVersion = config.get('v'); + +if (currentConfigVersion === 0) { + const controlsVisibilityState = + // @ts-expect-error - can't fix this + config.get('controlsVisibilityState') as 'always' | 'hidden' | 'auto'; + + // @ts-expect-error - can't fix this + const seekbarEnabled: boolean = config.get('seekbarEnabled'); + const seekbarVisibilityState = + // @ts-expect-error - can't fix this + config.get('controlsVisibilityState') as 'always' | 'hidden' | 'auto'; + + config.set('controls.enabled', controlsVisibilityState !== 'hidden'); + config.set('controls.collapseOnBlur', controlsVisibilityState === 'auto'); + + config.set('seekbar.enabled', seekbarEnabled); + config.set('seekbar.collapseOnBlur', seekbarVisibilityState === 'auto'); + + config.set('v', 1); +} + +const events = new EventTarget(); + +const origSet = config.set; +const origDelete = config.delete; + +config.set = function (key: T, value: D) { + origSet.call(this, key, value); + + events.dispatchEvent(new CustomEvent('set', { detail: { key } })); +}; + +config.delete = function (key: T): boolean { + const res = origDelete.call(this, key); + + if (res) events.dispatchEvent(new CustomEvent('delete', { detail: { key } })); + + return res; +}; + +/* + this uses the 2 monkey patches above to work + + due to this, config modifications that does not use this settings instance specifically + will not update any React UI that is using this hook. +*/ +export const useConfig = (key: T, fallback?: D): D => { + const [state, setState] = React.useState(config.get(key, fallback)); + + React.useEffect(() => { + const listener = (e: Event): void => { + const { + detail: { key: k }, + } = e as CustomEvent<{ key: T }>; + + if (k === key) setState(config.get(k)); + }; + + events.addEventListener('set', listener); + events.addEventListener('delete', listener); + + return () => { + events.removeEventListener('set', listener); + events.removeEventListener('delete', listener); + }; + }, []); + + return state as D; +}; + +export default config; diff --git a/plugins/SpotifyModal/src/index.tsx b/plugins/SpotifyModal/src/index.tsx index 9d806dc..3460f94 100644 --- a/plugins/SpotifyModal/src/index.tsx +++ b/plugins/SpotifyModal/src/index.tsx @@ -1,9 +1,8 @@ import { Injector, Logger } from 'replugged'; import { getOwnerInstance, waitFor } from 'replugged/util'; -import { fluxHooks } from 'replugged/common'; import webpack from 'replugged/webpack'; -import { SpotifyStore } from './types'; +import { ConnectedAccountsUtils, SpotifyStore } from './types'; import { default as Main } from './Modal'; import './style.css'; @@ -12,14 +11,16 @@ const log = Logger.plugin('SpotifyModal', '#1DB954'); const injector = new Injector(); let store = webpack.getByStoreName('SpotifyStore'); +let connectedAccountsUtils: ConnectedAccountsUtils = webpack.getByProps('refreshAccessToken'); let userAreaElement: Element; let forceUpdateUserArea: () => void; -let modalInstance =
; +let modalInstance: React.ReactElement; export function start(): void { void (async () => { store ??= webpack.getByStoreName('SpotifyStore'); + connectedAccountsUtils ??= await webpack.waitForProps(['refreshAccountToken']); userAreaElement = await waitFor('[class^=panels_] > [class^=container_]'); @@ -35,6 +36,8 @@ export function start(): void { return; } + modalInstance =
; + injector.after(owner, 'render', (_, res) => { return [modalInstance, res]; }); @@ -49,3 +52,5 @@ export function stop(): void { injector.uninjectAll(); forceUpdateUserArea?.(); } + +export { default as Settings } from './Settings'; diff --git a/plugins/SpotifyModal/src/style.css b/plugins/SpotifyModal/src/style.css index 4ebefeb..877196a 100644 --- a/plugins/SpotifyModal/src/style.css +++ b/plugins/SpotifyModal/src/style.css @@ -32,6 +32,8 @@ #spotify-modal:empty { border-bottom: none; + + margin: 0; } #spotify-modal .details { @@ -70,8 +72,10 @@ animation: 0s linear infinite spotify-modal-marquee; } -html.reduce-motion #spotify-modal .details .track-name, -html.reduce-motion #spotify-modal .details .artists { +html.reduce-motion #spotify-modal.use-discord-reduce-motion .details .track-name, +html.reduce-motion #spotify-modal.use-discord-reduce-motion .details .artists, +#spotify-modal.reduce-motion .details .track-name, +#spotify-modal.reduce-motion .details .artists { animation: none; text-overflow: ellipsis; @@ -109,6 +113,33 @@ html.reduce-motion #spotify-modal .details .artists { width: 56px; } +#spotify-modal .seekbar-container:not(.enabled) { + display: none; +} + +#spotify-modal .seekbar-container.collapse-on-blur { + transition: + 0.25s max-height 0.25s, + 0.25s opacity; + + opacity: 0; + max-height: 0; +} + +#spotify-modal:hover .seekbar-container.collapse-on-blur { + transition: + 0.25s max-height, + 0.25s opacity 0.25s; + + opacity: 1; + max-height: 200px; +} + +html.reduce-motion #spotify-modal.use-discord-reduce-motion .seekbar-container.collapse-on-blur, +#spotify-modal.reduce-motion .seekbar-container.collapse-on-blur { + transition: none; +} + #spotify-modal .seekbar-container .timestamps { display: flex; justify-content: space-between; diff --git a/plugins/SpotifyModal/src/types.ts b/plugins/SpotifyModal/src/types.ts index ece6434..d97bea0 100644 --- a/plugins/SpotifyModal/src/types.ts +++ b/plugins/SpotifyModal/src/types.ts @@ -23,6 +23,10 @@ export interface SpotifyStore extends FluxStore { } | null; } +export interface ConnectedAccountsUtils { + refreshAccountToken(type: string, id: string): Promise; +} + export interface ConnectedAccount { name: string; id: string; diff --git a/plugins/SpotifyModal/src/utils.ts b/plugins/SpotifyModal/src/utils.ts index ff4361e..160769c 100644 --- a/plugins/SpotifyModal/src/utils.ts +++ b/plugins/SpotifyModal/src/utils.ts @@ -1,7 +1,14 @@ +import { Logger } from 'replugged'; + +const log = Logger.plugin('SpotifyModal', '#1DB954'); + const BASE_URL = 'https://api.spotify.com/v1/'; export const spotify = { - async seekTo(token: string, position: number): Promise { - if (!token) return false; + async seekTo(token: string, position: number): Promise { + if (!token) { + log.error('missing token for request'); + return 0; + } const url = new URL('me/player/seek', BASE_URL); url.searchParams.set('position_ms', Math.floor(position).toString()); @@ -12,7 +19,16 @@ export const spotify = { Authorization: `Bearer ${token}`, }, }) - .then((res) => res.ok) - .catch(() => false); + .then((res) => res.status) + .catch((e) => { + log.error("couldn't make request to Spotify", e); + return 0; + }); }, }; + +export const classNameFactory = (classNames: Record): string => + Object.entries(classNames) + .filter(([_, enabled]) => enabled) + .map(([className]) => className.trim()) + .join(' ');