SpotifyControls: 👋
This commit is contained in:
parent
8cc9957bf7
commit
5409b11a41
6 changed files with 0 additions and 975 deletions
|
|
@ -1,392 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./spotifyStyles.css";
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { openImageModal } from "@utils/discord";
|
||||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { SeekBar } from "./SeekBar";
|
||||
import { SpotifyStore, Track } from "./SpotifyStore";
|
||||
|
||||
const cl = classNameFactory("vc-spotify-");
|
||||
|
||||
function msToHuman(ms: number) {
|
||||
const minutes = ms / 1000 / 60;
|
||||
const m = Math.floor(minutes);
|
||||
const s = Math.floor((minutes - m) * 60);
|
||||
return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function Svg(path: string, label: string) {
|
||||
return () => (
|
||||
<svg
|
||||
className={cl("button-icon", label)}
|
||||
height="24"
|
||||
width="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-label={label}
|
||||
focusable={false}
|
||||
>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// KraXen's icons :yesyes:
|
||||
// from https://fonts.google.com/icons?icon.style=Rounded&icon.set=Material+Icons
|
||||
// older material icon style, but still really good
|
||||
const PlayButton = Svg("M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z", "play");
|
||||
const PauseButton = Svg("M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2zm6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2z", "pause");
|
||||
const SkipPrev = Svg("M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1zm3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z", "previous");
|
||||
const SkipNext = Svg("M7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82zM16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1z", "next");
|
||||
const Repeat = Svg("M7 7h10v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V5H6c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1s1-.45 1-1V7zm10 10H7v-1.79c0-.45-.54-.67-.85-.35l-2.79 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.31.31.85.09.85-.36V19h11c.55 0 1-.45 1-1v-4c0-.55-.45-1-1-1s-1 .45-1 1v3z", "repeat");
|
||||
const Shuffle = Svg("M10.59 9.17L6.12 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l4.46 4.46 1.42-1.4zm4.76-4.32l1.19 1.19L4.7 17.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L17.96 7.46l1.19 1.19c.31.31.85.09.85-.36V4.5c0-.28-.22-.5-.5-.5h-3.79c-.45 0-.67.54-.36.85zm-.52 8.56l-1.41 1.41 3.13 3.13-1.2 1.2c-.31.31-.09.85.36.85h3.79c.28 0 .5-.22.5-.5v-3.79c0-.45-.54-.67-.85-.35l-1.19 1.19-3.13-3.14z", "shuffle");
|
||||
|
||||
function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
className={cl("button")}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyContextMenu({ name, path }: { name: string; path: string; }) {
|
||||
const copyId = `spotify-copy-${name}`;
|
||||
const openId = `spotify-open-${name}`;
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId={`spotify-${name}-menu`}
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label={`Spotify ${name} Menu`}
|
||||
>
|
||||
<Menu.MenuItem
|
||||
key={copyId}
|
||||
id={copyId}
|
||||
label={`Copy ${name} Link`}
|
||||
action={() => copyWithToast("https://open.spotify.com" + path)}
|
||||
icon={LinkIcon}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key={openId}
|
||||
id={openId}
|
||||
label={`Open ${name} in Spotify`}
|
||||
action={() => SpotifyStore.openExternal(path)}
|
||||
icon={OpenExternalIcon}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function makeContextMenu(name: string, path: string) {
|
||||
return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
|
||||
ContextMenuApi.openContextMenu(e, () => <CopyContextMenu name={name} path={path} />);
|
||||
}
|
||||
|
||||
function Controls() {
|
||||
const [isPlaying, shuffle, repeat] = useStateFromStores(
|
||||
[SpotifyStore],
|
||||
() => [SpotifyStore.isPlaying, SpotifyStore.shuffle, SpotifyStore.repeat]
|
||||
);
|
||||
|
||||
const [nextRepeat, repeatClassName] = (() => {
|
||||
switch (repeat) {
|
||||
case "off": return ["context", "repeat-off"] as const;
|
||||
case "context": return ["track", "repeat-context"] as const;
|
||||
case "track": return ["off", "repeat-track"] as const;
|
||||
default: throw new Error(`Invalid repeat state ${repeat}`);
|
||||
}
|
||||
})();
|
||||
|
||||
// the 1 is using position absolute so it does not make the button jump around
|
||||
return (
|
||||
<Flex className={cl("button-row")} style={{ gap: 0 }}>
|
||||
<Button
|
||||
className={classes(cl("button"), cl("shuffle"), cl(shuffle ? "shuffle-on" : "shuffle-off"))}
|
||||
onClick={() => SpotifyStore.setShuffle(!shuffle)}
|
||||
>
|
||||
<Shuffle />
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
Settings.plugins.SpotifyControls.previousButtonRestartsTrack && SpotifyStore.position > 3000 ? SpotifyStore.seek(0) : SpotifyStore.prev();
|
||||
}}>
|
||||
<SkipPrev />
|
||||
</Button>
|
||||
<Button onClick={() => SpotifyStore.setPlaying(!isPlaying)}>
|
||||
{isPlaying ? <PauseButton /> : <PlayButton />}
|
||||
</Button>
|
||||
<Button onClick={() => SpotifyStore.next()}>
|
||||
<SkipNext />
|
||||
</Button>
|
||||
<Button
|
||||
className={classes(cl("button"), cl("repeat"), cl(repeatClassName))}
|
||||
onClick={() => SpotifyStore.setRepeat(nextRepeat)}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
{repeat === "track" && <span className={cl("repeat-1")}>1</span>}
|
||||
<Repeat />
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const seek = debounce((v: number) => {
|
||||
SpotifyStore.seek(v);
|
||||
});
|
||||
|
||||
function SpotifySeekBar() {
|
||||
const { duration } = SpotifyStore.track!;
|
||||
|
||||
const [storePosition, isSettingPosition, isPlaying] = useStateFromStores(
|
||||
[SpotifyStore],
|
||||
() => [SpotifyStore.mPosition, SpotifyStore.isSettingPosition, SpotifyStore.isPlaying]
|
||||
);
|
||||
|
||||
const [position, setPosition] = useState(storePosition);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying && !isSettingPosition) {
|
||||
setPosition(SpotifyStore.position);
|
||||
const interval = setInterval(() => {
|
||||
setPosition(p => p + 1000);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [storePosition, isSettingPosition, isPlaying]);
|
||||
|
||||
const onChange = (v: number) => {
|
||||
if (isSettingPosition) return;
|
||||
setPosition(v);
|
||||
seek(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={cl("progress-bar")}>
|
||||
<Forms.FormText
|
||||
variant="text-xs/medium"
|
||||
className={cl("progress-time") + " " + cl("time-left")}
|
||||
aria-label="Progress"
|
||||
>
|
||||
{msToHuman(position)}
|
||||
</Forms.FormText>
|
||||
<SeekBar
|
||||
initialValue={position}
|
||||
minValue={0}
|
||||
maxValue={duration}
|
||||
onValueChange={onChange}
|
||||
asValueChanges={onChange}
|
||||
onValueRender={msToHuman}
|
||||
/>
|
||||
<Forms.FormText
|
||||
variant="text-xs/medium"
|
||||
className={cl("progress-time") + " " + cl("time-right")}
|
||||
aria-label="Total Duration"
|
||||
>
|
||||
{msToHuman(duration)}
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function AlbumContextMenu({ track }: { track: Track; }) {
|
||||
const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume);
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="spotify-album-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Spotify Album Menu"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
key="open-album"
|
||||
id="open-album"
|
||||
label="Open Album"
|
||||
action={() => SpotifyStore.openExternal(`/album/${track.album.id}`)}
|
||||
icon={OpenExternalIcon}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key="view-cover"
|
||||
id="view-cover"
|
||||
label="View Album Cover"
|
||||
// trolley
|
||||
action={() => openImageModal(track.album.image)}
|
||||
icon={ImageIcon}
|
||||
/>
|
||||
<Menu.MenuControlItem
|
||||
id="spotify-volume"
|
||||
key="spotify-volume"
|
||||
label="Volume"
|
||||
control={(props, ref) => (
|
||||
<Menu.MenuSliderControl
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={volume}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
onChange={debounce((v: number) => SpotifyStore.setVolume(v))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function makeLinkProps(name: string, condition: unknown, path: string) {
|
||||
if (!condition) return {};
|
||||
|
||||
return {
|
||||
role: "link",
|
||||
onClick: () => SpotifyStore.openExternal(path),
|
||||
onContextMenu: makeContextMenu(name, path)
|
||||
} satisfies React.HTMLAttributes<HTMLElement>;
|
||||
}
|
||||
|
||||
function Info({ track }: { track: Track; }) {
|
||||
const img = track?.album?.image;
|
||||
|
||||
const [coverExpanded, setCoverExpanded] = useState(false);
|
||||
|
||||
const i = (
|
||||
<>
|
||||
{img && (
|
||||
<img
|
||||
id={cl("album-image")}
|
||||
src={img.url}
|
||||
alt="Album Image"
|
||||
onClick={() => setCoverExpanded(!coverExpanded)}
|
||||
onContextMenu={e => {
|
||||
ContextMenuApi.openContextMenu(e, () => <AlbumContextMenu track={track} />);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (coverExpanded && img)
|
||||
return (
|
||||
<div id={cl("album-expanded-wrapper")}>
|
||||
{i}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div id={cl("info-wrapper")}>
|
||||
{i}
|
||||
<div id={cl("titles")}>
|
||||
<Forms.FormText
|
||||
variant="text-sm/semibold"
|
||||
id={cl("song-title")}
|
||||
className={cl("ellipoverflow")}
|
||||
title={track.name}
|
||||
{...makeLinkProps("Song", track.id, `/track/${track.id}`)}
|
||||
>
|
||||
{track.name}
|
||||
</Forms.FormText>
|
||||
{track.artists.some(a => a.name) && (
|
||||
<Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}>
|
||||
<span className={cl("song-info-prefix")}>by </span>
|
||||
{track.artists.map((a, i) => (
|
||||
<React.Fragment key={a.name}>
|
||||
<span
|
||||
className={cl("artist")}
|
||||
style={{ fontSize: "inherit" }}
|
||||
title={a.name}
|
||||
{...makeLinkProps("Artist", a.id, `/artist/${a.id}`)}
|
||||
>
|
||||
{a.name}
|
||||
</span>
|
||||
{i !== track.artists.length - 1 && <span className={cl("comma")}>{", "}</span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Forms.FormText>
|
||||
)}
|
||||
{track.album.name && (
|
||||
<Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}>
|
||||
<span className={cl("song-info-prefix")}>on </span>
|
||||
<span
|
||||
id={cl("album-title")}
|
||||
className={cl("album")}
|
||||
style={{ fontSize: "inherit" }}
|
||||
title={track.album.name}
|
||||
{...makeLinkProps("Album", track.album.id, `/album/${track.album.id}`)}
|
||||
>
|
||||
{track.album.name}
|
||||
</span>
|
||||
</Forms.FormText>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Player() {
|
||||
const track = useStateFromStores(
|
||||
[SpotifyStore],
|
||||
() => SpotifyStore.track,
|
||||
null,
|
||||
(prev, next) => prev?.id ? (prev.id === next?.id) : prev?.name === next?.name
|
||||
);
|
||||
|
||||
const device = useStateFromStores(
|
||||
[SpotifyStore],
|
||||
() => SpotifyStore.device,
|
||||
null,
|
||||
(prev, next) => prev?.id === next?.id
|
||||
);
|
||||
|
||||
const isPlaying = useStateFromStores([SpotifyStore], () => SpotifyStore.isPlaying);
|
||||
const [shouldHide, setShouldHide] = useState(false);
|
||||
|
||||
// Hide player after 5 minutes of inactivity
|
||||
|
||||
React.useEffect(() => {
|
||||
setShouldHide(false);
|
||||
if (!isPlaying) {
|
||||
const timeout = setTimeout(() => setShouldHide(true), 1000 * 60 * 5);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
if (!track || !device?.is_active || shouldHide)
|
||||
return null;
|
||||
|
||||
const exportTrackImageStyle = {
|
||||
"--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div id={cl("player")} style={exportTrackImageStyle}>
|
||||
<Info track={track} />
|
||||
<SpotifySeekBar />
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { LazyComponent } from "@utils/lazyReact";
|
||||
import { Slider } from "@webpack/common";
|
||||
|
||||
export const SeekBar = LazyComponent(() => {
|
||||
const SliderClass = Slider.$$vencordGetWrappedComponent();
|
||||
|
||||
// Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish.
|
||||
// We extend their class and override their `getDerivedStateFromProps` to update the value
|
||||
return class SeekBar extends SliderClass {
|
||||
static getDerivedStateFromProps(props: any, state: any) {
|
||||
const newState = super.getDerivedStateFromProps!(props, state);
|
||||
if (newState) {
|
||||
newState.value = props.initialValue;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { findByProps, findByPropsLazy, proxyLazyWebpack } from "@webpack";
|
||||
import { Flux, FluxDispatcher } from "@webpack/common";
|
||||
|
||||
export interface Track {
|
||||
id: string;
|
||||
name: string;
|
||||
duration: number;
|
||||
isLocal: boolean;
|
||||
album: {
|
||||
id: string;
|
||||
name: string;
|
||||
image: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
artists: {
|
||||
id: string;
|
||||
href: string;
|
||||
name: string;
|
||||
type: string;
|
||||
uri: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface PlayerState {
|
||||
accountId: string;
|
||||
track: Track | null;
|
||||
volumePercent: number,
|
||||
isPlaying: boolean,
|
||||
repeat: boolean,
|
||||
position: number,
|
||||
context?: any;
|
||||
device?: Device;
|
||||
|
||||
// added by patch
|
||||
actual_repeat: Repeat;
|
||||
shuffle: boolean;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
type Repeat = "off" | "track" | "context";
|
||||
|
||||
// Don't wanna run before Flux and Dispatcher are ready!
|
||||
export const SpotifyStore = proxyLazyWebpack(() => {
|
||||
// For some reason ts hates extends Flux.Store
|
||||
const { Store } = Flux;
|
||||
|
||||
const SpotifySocket = findByProps("getActiveSocketAndDevice");
|
||||
const SpotifyAPI = findByPropsLazy("vcSpotifyMarker");
|
||||
|
||||
const API_BASE = "https://api.spotify.com/v1/me/player";
|
||||
|
||||
class SpotifyStore extends Store {
|
||||
public mPosition = 0;
|
||||
public _start = 0;
|
||||
|
||||
public track: Track | null = null;
|
||||
public device: Device | null = null;
|
||||
public isPlaying = false;
|
||||
public repeat: Repeat = "off";
|
||||
public shuffle = false;
|
||||
public volume = 0;
|
||||
|
||||
public isSettingPosition = false;
|
||||
|
||||
public openExternal(path: string) {
|
||||
const url = Settings.plugins.SpotifyControls.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp")
|
||||
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
|
||||
: "https://open.spotify.com" + path;
|
||||
|
||||
VencordNative.native.openExternal(url);
|
||||
}
|
||||
|
||||
// Need to keep track of this manually
|
||||
public get position(): number {
|
||||
let pos = this.mPosition;
|
||||
if (this.isPlaying) {
|
||||
pos += Date.now() - this._start;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
public set position(p: number) {
|
||||
this.mPosition = p;
|
||||
this._start = Date.now();
|
||||
}
|
||||
|
||||
prev() {
|
||||
this._req("post", "/previous");
|
||||
}
|
||||
|
||||
next() {
|
||||
this._req("post", "/next");
|
||||
}
|
||||
|
||||
setVolume(percent: number) {
|
||||
this._req("put", "/volume", {
|
||||
query: {
|
||||
volume_percent: Math.round(percent)
|
||||
}
|
||||
|
||||
}).then(() => {
|
||||
this.volume = percent;
|
||||
this.emitChange();
|
||||
});
|
||||
}
|
||||
|
||||
setPlaying(playing: boolean) {
|
||||
this._req("put", playing ? "/play" : "/pause");
|
||||
}
|
||||
|
||||
setRepeat(state: Repeat) {
|
||||
this._req("put", "/repeat", {
|
||||
query: { state }
|
||||
});
|
||||
}
|
||||
|
||||
setShuffle(state: boolean) {
|
||||
this._req("put", "/shuffle", {
|
||||
query: { state }
|
||||
}).then(() => {
|
||||
this.shuffle = state;
|
||||
this.emitChange();
|
||||
});
|
||||
}
|
||||
|
||||
seek(ms: number) {
|
||||
if (this.isSettingPosition) return Promise.resolve();
|
||||
|
||||
this.isSettingPosition = true;
|
||||
|
||||
return this._req("put", "/seek", {
|
||||
query: {
|
||||
position_ms: Math.round(ms)
|
||||
}
|
||||
}).catch((e: any) => {
|
||||
console.error("[VencordSpotifyControls] Failed to seek", e);
|
||||
this.isSettingPosition = false;
|
||||
});
|
||||
}
|
||||
|
||||
_req(method: "post" | "get" | "put", route: string, data: any = {}) {
|
||||
if (this.device?.is_active)
|
||||
(data.query ??= {}).device_id = this.device.id;
|
||||
|
||||
const { socket } = SpotifySocket.getActiveSocketAndDevice();
|
||||
return SpotifyAPI[method](socket.accountId, socket.accessToken, {
|
||||
url: API_BASE + route,
|
||||
...data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const store = new SpotifyStore(FluxDispatcher, {
|
||||
SPOTIFY_PLAYER_STATE(e: PlayerState) {
|
||||
store.track = e.track;
|
||||
store.device = e.device ?? null;
|
||||
store.isPlaying = e.isPlaying ?? false;
|
||||
store.volume = e.volumePercent ?? 0;
|
||||
store.repeat = e.actual_repeat || "off";
|
||||
store.shuffle = e.shuffle ?? false;
|
||||
store.position = e.position ?? 0;
|
||||
store.isSettingPosition = false;
|
||||
store.emitChange();
|
||||
},
|
||||
SPOTIFY_SET_DEVICES({ devices }: { devices: Device[]; }) {
|
||||
store.device = devices.find(d => d.is_active) ?? devices[0] ?? null;
|
||||
store.emitChange();
|
||||
}
|
||||
});
|
||||
|
||||
return store;
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
.vc-spotify-button-row {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: 0.2s;
|
||||
transition-property: height;
|
||||
}
|
||||
|
||||
#vc-spotify-player:hover .vc-spotify-button-row {
|
||||
opacity: 1;
|
||||
height: 32px;
|
||||
pointer-events: auto;
|
||||
|
||||
/* only transition opacity on show to prevent clipping */
|
||||
transition-property: height, opacity;
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
import hoverOnlyStyle from "./hoverOnly.css?managed";
|
||||
import { Player } from "./PlayerComponent";
|
||||
|
||||
function toggleHoverControls(value: boolean) {
|
||||
(value ? enableStyle : disableStyle)(hoverOnlyStyle);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "SpotifyControls",
|
||||
description: "Adds a Spotify player above the account panel",
|
||||
authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev],
|
||||
options: {
|
||||
hoverControls: {
|
||||
description: "Show controls on hover",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
onChange: v => toggleHoverControls(v)
|
||||
},
|
||||
useSpotifyUris: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms",
|
||||
default: false
|
||||
},
|
||||
previousButtonRestartsTrack: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Restart currently playing track when pressing the previous button if playtime is >3s",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
// HEY THOR UPDATE YOUTUBE MUSIC CONTROLS IF THIS SHIT CHANGES TY :)
|
||||
find: "this.isCopiedStreakGodlike",
|
||||
replacement: {
|
||||
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
|
||||
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/,
|
||||
// react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ...
|
||||
replace: "$self.PanelWrapper,{VencordOriginal:$1,"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".PLAYER_DEVICES",
|
||||
replacement: [{
|
||||
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it)
|
||||
match: /get:(\i)\.bind\(null,(\i\.\i)\.get\)/,
|
||||
replace: "post:$1.bind(null,$2.post),vcSpotifyMarker:1,$&"
|
||||
},
|
||||
{
|
||||
// Spotify Connect API returns status 202 instead of 204 when skipping tracks.
|
||||
// Discord rejects 202 which causes the request to send twice. This patch prevents this.
|
||||
match: /202===\i\.status/,
|
||||
replace: "false",
|
||||
}]
|
||||
},
|
||||
{
|
||||
find: 'repeat:"off"!==',
|
||||
replacement: [
|
||||
{
|
||||
// Discord doesn't give you shuffle state and the repeat kind, only a boolean
|
||||
match: /repeat:"off"!==(\i),/,
|
||||
replace: "shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&"
|
||||
},
|
||||
{
|
||||
match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/,
|
||||
replace: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls),
|
||||
|
||||
PanelWrapper({ VencordOriginal, ...props }) {
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div className="vc-spotify-fallback">
|
||||
<p>Failed to render Spotify Modal :(</p>
|
||||
<p >Check the console for errors</p>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Player />
|
||||
</ErrorBoundary>
|
||||
|
||||
<VencordOriginal {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
#vc-spotify-player {
|
||||
padding: 12px;
|
||||
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt)));
|
||||
margin: 0;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
|
||||
/* so custom themes can easily change it */
|
||||
--vc-spotify-green: var(--spotify, #1db954);
|
||||
--vc-spotify-green-90: color-mix(in hsl, var(--vc-spotify-green), transparent 90%);
|
||||
--vc-spotify-green-80: color-mix(in hsl, var(--vc-spotify-green), transparent 80%);
|
||||
}
|
||||
|
||||
.theme-light #vc-spotify-player {
|
||||
background: var(--bg-overlay-3, var(--background-base-lower-alt));
|
||||
}
|
||||
|
||||
.theme-dark #vc-spotify-player {
|
||||
background: var(--bg-overlay-1, var(--background-base-lower-alt));
|
||||
}
|
||||
|
||||
.vc-spotify-button {
|
||||
margin: 0 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--interactive-normal);
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-spotify-button:hover {
|
||||
color: var(--interactive-hover);
|
||||
background-color: var(--background-modifier-selected);
|
||||
}
|
||||
|
||||
.vc-spotify-button-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.vc-spotify-shuffle .vc-spotify-button-icon,
|
||||
.vc-spotify-repeat .vc-spotify-button-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
/* .vc-spotify-button:hover {
|
||||
filter: brightness(1.3);
|
||||
} */
|
||||
|
||||
.vc-spotify-repeat-context,
|
||||
.vc-spotify-repeat-track,
|
||||
.vc-spotify-shuffle-on {
|
||||
background-color: var(--vc-spotify-green-90);
|
||||
}
|
||||
|
||||
.vc-spotify-repeat-context:hover,
|
||||
.vc-spotify-repeat-track:hover,
|
||||
.vc-spotify-shuffle-on:hover {
|
||||
background-color: var(--vc-spotify-green-80);
|
||||
}
|
||||
|
||||
.vc-spotify-tooltip-text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding-right: 0.2em;
|
||||
max-width: 100%;
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
.vc-spotify-repeat-1 {
|
||||
font-size: 70%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.vc-spotify-button-row {
|
||||
justify-content: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.vc-spotify-secondary-song-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vc-spotify-song-info-prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#vc-spotify-info-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 3em;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
#vc-spotify-album-image {
|
||||
height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 3px;
|
||||
transition: filter 0.2s;
|
||||
}
|
||||
|
||||
#vc-spotify-album-image:hover {
|
||||
filter: brightness(1.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#vc-spotify-album-expanded-wrapper #vc-spotify-album-image {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#vc-spotify-titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.2rem;
|
||||
align-items: flex-start;
|
||||
place-content: flex-start center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#vc-spotify-song-title {
|
||||
color: var(--header-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vc-spotify-ellipoverflow {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vc-spotify-artist,
|
||||
.vc-spotify-album {
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.vc-spotify-comma {
|
||||
color: var(--header-secondary);
|
||||
}
|
||||
|
||||
.vc-spotify-artist[role="link"]:hover,
|
||||
#vc-spotify-album-title[role="link"]:hover,
|
||||
#vc-spotify-song-title[role="link"]:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar {
|
||||
position: relative;
|
||||
color: var(--text-default);
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"] {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"] [class^="bar"] {
|
||||
height: 3px !important;
|
||||
top: calc(12px - 4px / 2 + var(--bar-offset));
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"] [class^="barFill"] {
|
||||
background-color: var(--interactive-active);
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"]:hover [class^="barFill"] {
|
||||
background-color: var(--vc-spotify-green);
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] {
|
||||
/* these importants are necessary, it applies a width and height through inline styles */
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
margin-top: calc(17px/-2 + var(--bar-offset)/2);
|
||||
margin-left: -0.5px;
|
||||
background-color: var(--interactive-active);
|
||||
border-color: var(--interactive-normal);
|
||||
color: var(--interactive-normal);
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#vc-spotify-progress-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vc-spotify-progress-time {
|
||||
font-size: 12px;
|
||||
top: 10px;
|
||||
position: absolute;
|
||||
margin-top: 8px;
|
||||
font-family: var(--font-code);
|
||||
}
|
||||
|
||||
.vc-spotify-time-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vc-spotify-time-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.vc-spotify-fallback {
|
||||
padding: 0.5em;
|
||||
color: var(--text-default);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue