├── BadBetterMediaPlayer.plugin.js
├── BetterMediaPlayer.plugin.js
└── README.md
/BadBetterMediaPlayer.plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @name BadBetterMediaPlayer
3 | * @version 1.2.12
4 | * @author unknown81311_&_Doggybootsy
5 | * @description Deny this plugin please
6 | * @authorLink https://betterdiscord.app/plugin?id=377
7 | * @source https://github.com/unknown81311/BetterMediaPlayer
8 | * @updateUrl https://raw.githubusercontent.com/unknown81311/BetterMediaPlayer/main/BetterMediaPlayer.plugin.js
9 | * @invite yYJA3qQE5F
10 | */
11 |
12 | module.exports = () => {
13 | return { start(), stop() {} };
14 | }
15 |
--------------------------------------------------------------------------------
/BetterMediaPlayer.plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @name BetterMediaPlayer
3 | * @version 1.2.16
4 | * @author unknown81311_&_Doggybootsy
5 | * @description Adds more features to the MediaPlayer inside of Discord. (**Only adds PIP and Loop!**)
6 | * @authorLink https://betterdiscord.app/plugin?id=377
7 | * @source https://github.com/unknown81311/BetterMediaPlayer
8 | * @updateUrl https://raw.githubusercontent.com/unknown81311/BetterMediaPlayer/main/BetterMediaPlayer.plugin.js
9 | * @invite yYJA3qQE5F
10 | */
11 |
12 | const { Webpack, DOM, React, Data, UI } = new BdApi("BetterMediaPlayer");
13 | const classes = Object.assign({}, Webpack.getModule(m => m.controlIcon && m.video), Webpack.getModule(m => m.button && m.colorBrand));
14 |
15 | const [
16 | PopoutWindowStore,
17 | dispatcher,
18 | useStateFromStores,
19 | PopoutWindow,
20 | errorClasses,
21 | { errorPage, buttons },
22 | Flex,
23 | intl,
24 | inviteActions,
25 | InviteModalStore,
26 | native,
27 | GuildStore,
28 | hljs,
29 | Guild,
30 | GuildMemberCountStore,
31 | VolumeSlider,
32 | DurationBar,
33 | scrollerClasses
34 | ] = Webpack.getBulk(
35 | { filter: m => m.getWindow },
36 | { filter: m => m.subscribe && m.dispatch },
37 | { filter: Webpack.Filters.byStrings('"useStateFromStores"'), searchExports: true },
38 | { filter: m => m.render?.toString().includes("Missing guestWindow reference") },
39 | { filter: m => m.wrapper && m.note },
40 | { filter: m => m.errorPage && m.buttons },
41 | { filter: m => m.defaultProps?.basis, searchExports: true },
42 | { filter: m => m.Messages },
43 | { filter: m => m.resolveInvite },
44 | { filter: m => m.getName?.() === "InviteModalStore" },
45 | { filter: m => m.minimize && m.requireModule },
46 | { filter: m => m.getName?.() === "GuildStore" },
47 | { filter: m => m.highlight },
48 | { filter: m => m.prototype?.getEveryoneRoleId && m.prototype.getIconURL },
49 | { filter: m => m.getName?.() === "GuildMemberCountStore" },
50 | { filter: Webpack.Filters.byStrings("sliderClassName:", "onDragEnd:this.handleDragEnd", "handleValueChange") },
51 | { filter: m => m.Types?.DURATION },
52 | { filter: m => m.thin && m.customTheme }
53 | );
54 |
55 | const Button = BdApi.Components.Button;
56 |
57 | const { isOpen: originalIsOpen } = InviteModalStore;
58 | const { minimize: originalMinimize, focus: originalFocus } = native;
59 |
60 | const c = classes.wrapperControlsHidden.split(" ")[1];
61 | const getAllMediaPlayers = (parent = document) => !parent?.querySelectorAll ? [] : Array.from(parent.querySelectorAll(`.${c}:not([data-bmp-hook]) > .${classes.video}`), (node) => {
62 | node.parentElement.setAttribute("data-bmp-hook", "");
63 | return node;
64 | });
65 |
66 | const appendLoopButton = (videoButtons) => {
67 | /** @type {HTMLVideoElement} */
68 | const video = videoButtons.parentElement.querySelector("video");
69 |
70 | const node = document.createElement("div");
71 | node.addEventListener("click", () => {
72 | if (video.loop = !video.loop) node.classList.add("BMP_active");
73 | else node.classList.remove("BMP_active");
74 | });
75 |
76 | node.classList.add("BMP_button");
77 | if (video.loop) node.classList.add("BMP_active");
78 |
79 | node.innerHTML = ``;
83 |
84 | videoButtons.insertBefore(node, videoButtons.childNodes[1]);
85 | };
86 |
87 | function Replay({ width, height }) {
88 | return React.createElement("svg", {
89 | width: width,
90 | height: height,
91 | viewBox: "0 0 24 24",
92 | children: React.createElement("path", {
93 | fill: "currentColor",
94 | d: "M12,5 L12,1 L7,6 L12,11 L12,7 C15.31,7 18,9.69 18,13 C18,16.31 15.31,19 12,19 C8.69,19 6,16.31 6,13 L4,13 C4,17.42 7.58,21 12,21 C16.42,21 20,17.42 20,13 C20,8.58 16.42,5 12,5 L12,5 Z"
95 | })
96 | });
97 | };
98 | function Pause({ width, height }) {
99 | return React.createElement("svg", {
100 | width: width,
101 | height: height,
102 | viewBox: "0 0 18 18",
103 | children: React.createElement("path", {
104 | fill: "currentColor",
105 | d: "M5.25 2.25226H7.5C7.9125 2.25226 8.25 2.58976 8.25 3.00226V15.0023C8.25 15.4148 7.9125 15.7523 7.5 15.7523H5.25C4.8375 15.7523 4.5 15.4148 4.5 15.0023V3.00226C4.5 2.58976 4.8375 2.25226 5.25 2.25226ZM11.25 2.25226H13.5C13.9125 2.25226 14.25 2.58976 14.25 3.00226V15.0023C14.25 15.4148 13.9125 15.7523 13.5 15.7523H11.25C10.8375 15.7523 10.5 15.4148 10.5 15.0023V3.00226C10.5 2.58976 10.8375 2.25226 11.25 2.25226Z"
106 | })
107 | });
108 | };
109 | function Play({ width, height }) {
110 | return React.createElement("svg", {
111 | width: width,
112 | height: height,
113 | viewBox: "0 0 18 18",
114 | children: React.createElement("path", {
115 | fill: "currentColor",
116 | d: "M6.01053 2.82974C5.01058 2.24153 3.75 2.96251 3.75 4.12264V13.8774C3.75 15.0375 5.01058 15.7585 6.01053 15.1703L14.3021 10.2929C15.288 9.71294 15.288 8.28709 14.3021 7.70711L6.01053 2.82974Z"
117 | })
118 | });
119 | };
120 | function Loop({ width, height }) {
121 | return React.createElement("svg", {
122 | width: width,
123 | height: height,
124 | viewBox: "-5 0 459 459.648",
125 | children: [
126 | React.createElement("path", {
127 | fill: "currentColor",
128 | fillRule: "evenodd",
129 | clipRule: "evenodd",
130 | d: "m416.324219 293.824219c0 26.507812-21.492188 48-48 48h-313.375l63.199219-63.199219-22.625-22.625-90.511719 90.511719c-6.246094 6.25-6.246094 16.375 0 22.625l90.511719 90.511719 22.625-22.625-63.199219-63.199219h313.375c44.160156-.054688 79.945312-35.839844 80-80v-64h-32zm0 0"
131 | }),
132 | React.createElement("path", {
133 | fill: "currentColor",
134 | fillRule: "evenodd",
135 | clipRule: "evenodd",
136 | d: "m32.324219 165.824219c0-26.511719 21.488281-48 48-48h313.375l-63.199219 63.199219 22.625 22.625 90.511719-90.511719c6.246093-6.25 6.246093-16.375 0-22.625l-90.511719-90.511719-22.625 22.625 63.199219 63.199219h-313.375c-44.160157.050781-79.949219 35.839843-80 80v64h32zm0 0"
137 | })
138 | ]
139 | });
140 | };
141 | function Download({ width, height }) {
142 | return React.createElement("svg", {
143 | width: width,
144 | height: height,
145 | viewBox: "0 0 24 24",
146 | children: [
147 | React.createElement("path", {
148 | fill: "currentColor",
149 | fillRule: "evenodd",
150 | clipRule: "evenodd",
151 | d: "M16.293 9.293L17.707 10.707L12 16.414L6.29297 10.707L7.70697 9.293L11 12.586V2H13V12.586L16.293 9.293ZM18 20V18H20V20C20 21.102 19.104 22 18 22H6C4.896 22 4 21.102 4 20V18H6V20H18Z"
152 | })
153 | ]
154 | });
155 | };
156 |
157 | const useVolume = (() => {
158 | const listeners = new Set();
159 |
160 | return function useVolume() {
161 | const [ volume, setVolume ] = React.useState(() => Data.load("volume") ?? 1);
162 |
163 | React.useLayoutEffect(() => {
164 | function listener(newVolume) {
165 | setVolume(newVolume);
166 | };
167 |
168 | listeners.add(listener);
169 | return () => void listeners.delete(listener);
170 | }, [ listeners ]);
171 |
172 | function setNewVolume(newVolume) {
173 | Data.save("volume", newVolume);
174 | for (const listener of listeners) listener(newVolume);
175 | };
176 |
177 | return [ volume, setNewVolume ];
178 | };
179 | })();
180 | const useMuted = (() => {
181 | const listeners = new Set();
182 |
183 | return function useMuted() {
184 | const [ muted, setMuted ] = React.useState(() => Data.load("muted") ?? false);
185 |
186 | React.useLayoutEffect(() => {
187 | function listener(isMuted) {
188 | setMuted(isMuted);
189 | };
190 |
191 | listeners.add(listener);
192 | return () => void listeners.delete(listener);
193 | }, [ ]);
194 |
195 | function setIsMuted(isMuted) {
196 | Data.save("muted", isMuted);
197 | for (const listener of listeners) listener(isMuted);
198 | };
199 |
200 | return [ muted, setIsMuted ];
201 | };
202 | })();
203 |
204 | function calcTime(time) {
205 | const minutes = Math.floor(time / 60);
206 | const seconds = time - (minutes * 60);
207 | return { minutes, seconds };
208 | };
209 |
210 | function Duration({ currentTime, duration }) {
211 | const parsedCurrentTime = React.useMemo(() => calcTime(currentTime), [ currentTime ]);
212 | const parsedDuration = React.useMemo(() => calcTime(duration), [ currentTime ]);
213 |
214 | return React.createElement("div", {
215 | id: "duration",
216 | children: [
217 | `${parsedCurrentTime.minutes}:${10 > parsedCurrentTime.seconds ? `0${parsedCurrentTime.seconds}` : parsedCurrentTime.seconds}`,
218 | "/",
219 | `${parsedDuration.minutes}:${10 > parsedDuration.seconds ? `0${parsedDuration.seconds}` : parsedDuration.seconds}`
220 | ]
221 | })
222 | };
223 |
224 | function getBuffers(node) {
225 | const buffers = [];
226 | for (let index = 0; index < node.buffered.length; index++) {
227 | const start = node.buffered.start(index);
228 | const end = node.buffered.end(index);
229 | if (!(end - start < 1)) {
230 | buffers.push([ start / node.duration, (end - start) / node.duration ]);
231 | };
232 | };
233 | return buffers;
234 | };
235 |
236 | class ErrorBoundary extends React.Component {
237 | state = { didError: false, errorInfo: new Error("Minified React error #999; Fake React Error (for debugging)") }
238 | componentDidCatch(error) {
239 | this.setState({ didError: true, errorInfo: error });
240 | };
241 | render() {
242 | return React.createElement(React.Fragment, {
243 | children: [
244 | this.state.didError && React.createElement(ErrorSplash, { ...this.props, errorInfo: this.state.errorInfo }),
245 | !this.state.didError && React.createElement(PictureInPicture, this.props)
246 | ]
247 | });
248 | };
249 | };
250 |
251 | let resolvedInvite;
252 | function JoinGuild() {
253 | const guild = useStateFromStores([ GuildStore ], () => GuildStore.getGuild("864267123694370836"));
254 |
255 | const [ invite, setInvite ] = React.useState(resolvedInvite);
256 |
257 | const guildInfo = React.useMemo(() => {
258 | if (guild ? false : !invite) return;
259 | const guildObject = guild ? guild : new Guild(invite.guild);
260 |
261 | return {
262 | name: guildObject.name,
263 | members: invite ? invite.approximate_member_count : GuildMemberCountStore.getMemberCount("864267123694370836"),
264 | online: invite ? invite.approximate_presence_count : GuildMemberCountStore.getOnlineCount("864267123694370836"),
265 | url: guildObject.getIconURL()
266 | };
267 | }, [ guild, invite ]);
268 |
269 | React.useLayoutEffect(() => {
270 | if (invite) return;
271 | (async function () {
272 | const { invite } = await inviteActions.resolveInvite("yYJA3qQE5F", "Desktop Modal");
273 | resolvedInvite = invite;
274 | setInvite(invite);
275 | })();
276 | }, [ ]);
277 |
278 | const join = React.useCallback(async () => {
279 | if (guild) return;
280 |
281 | // Prevent from focusing to main window
282 | InviteModalStore.isOpen = () => true;
283 | native.minimize = () => {};
284 | native.focus = () => {};
285 |
286 | await dispatcher.dispatch({ type: "INVITE_MODAL_OPEN", invite });
287 |
288 | InviteModalStore.isOpen = originalIsOpen;
289 | native.minimize = originalMinimize;
290 | native.focus = originalFocus;
291 | }, [ guild, invite ]);
292 |
293 | const transitionTo = React.useCallback(() => {
294 | inviteActions.transitionToInviteSync(invite);
295 | }, [ guild, invite ]);
296 |
297 | const onClick = React.useCallback(() => {
298 | if (!invite) return;
299 | if (guild) transitionTo();
300 | else join();
301 | }, [ transitionTo, join ]);
302 |
303 | return React.createElement("div", {
304 | style: {
305 | display: "flex",
306 | background: "var(--background-secondary)",
307 | padding: 8,
308 | borderRadius: 8,
309 | border: "1px solid var(--background-secondary-alt)"
310 | },
311 | children: [
312 | guildInfo ? React.createElement("img", {
313 | src: guildInfo.url,
314 | height: 38,
315 | width: 38,
316 | style: {
317 | borderRadius: "50%"
318 | }
319 | }) : React.createElement("div", {
320 | style: {
321 | height: 38,
322 | width: 38,
323 | borderRadius: "50%",
324 | background: "var(--background-secondary-alt)"
325 | }
326 | }),
327 | React.createElement("div", {
328 | style: {
329 | color: "var(--text-normal)",
330 | flex: "1 0",
331 | padding: "0 8px",
332 | display: "flex",
333 | flexDirection: "column",
334 | justifyContent: "space-between"
335 | },
336 | children: [
337 | React.createElement("div", {
338 | style: {
339 | fontSize: "large",
340 | fontWeight: 600
341 | },
342 | children: [
343 | guildInfo && guildInfo.name,
344 | !guildInfo && "Resolving..."
345 | ]
346 | }),
347 |
348 | React.createElement("div", {
349 | style: {
350 | display: "flex"
351 | },
352 | children: [
353 | React.createElement("div", {
354 | style: {
355 | display: "flex"
356 | },
357 | children: [
358 | React.createElement("div", {
359 | style: {
360 | margin: "auto 8px auto 0",
361 | width: 8,
362 | height: 8,
363 | borderRadius: "50%",
364 | background: "var(--green-360)"
365 | }
366 | }),
367 | guildInfo ? guildInfo.online : "0"
368 | ]
369 | }),
370 | React.createElement("div", {
371 | style: {
372 | display: "flex"
373 | },
374 | children: [
375 | React.createElement("div", {
376 | style: {
377 | margin: "auto 8px",
378 | width: 8,
379 | height: 8,
380 | borderRadius: "50%",
381 | background: "var(--primary-400)"
382 | }
383 | }),
384 | guildInfo ? guildInfo.members : "0"
385 | ]
386 | })
387 | ]
388 | })
389 | ]
390 | }),
391 | React.createElement(Button, {
392 | color: Button.Colors.GREEN,
393 | disabled: guild ? false : !invite,
394 | children: [
395 | guild && intl.Messages.JOINED_GUILD,
396 | (!guild && invite) && intl.Messages.INVITE_MODAL_BUTTON.message.replace("**!!{guildName}!!**", guildInfo.name),
397 | (!guild && !invite) && intl.Messages.INVITE_BUTTON_RESOLVING
398 | ],
399 | onClick: onClick
400 | })
401 | ]
402 | });
403 | };
404 |
405 | function ErrorModal({ src, errorInfo }) {
406 | const highlighted = React.useMemo(() => hljs.highlight(errorInfo.stack, { language: "js" }), [ ]);
407 |
408 | const [ copied, setCopied ] = React.useState(false);
409 |
410 | const setNotCopied = React.useMemo(() => _.debounce(() => setCopied(false), 1000), [ ]);
411 |
412 | const copy = React.useCallback(() => {
413 | setCopied(true);
414 | setNotCopied();
415 | DiscordNative.clipboard.copy(errorInfo.stack);
416 | }, [ ]);
417 |
418 | return React.createElement("div", {
419 | children: [
420 | React.createElement(JoinGuild, { src }),
421 | React.createElement("div", {
422 | style: {
423 | width: "calc(100% - 16px)",
424 | height: 1,
425 | margin: 8,
426 | background: "var(--background-secondary-alt)"
427 | }
428 | }),
429 | React.createElement("div", {
430 | style: {
431 | position: "relative"
432 | },
433 | children: [
434 | React.createElement("h2", {
435 | style: {
436 | color: "var(--header-primary)",
437 | fontWeight: 800,
438 | marginBottom: 8
439 | },
440 | children: "Error Stack"
441 | }),
442 | React.createElement("pre", {
443 | className: `${scrollerClasses.thin} ${scrollerClasses.fade} ${scrollerClasses.customTheme}`,
444 | style: {
445 | maxHeight: 200,
446 | background: "var(--background-secondary)",
447 | border: "1px solid var(--background-secondary-alt)",
448 | color: "var(--text-normal)",
449 | overflow: "auto",
450 | padding: 8,
451 | userSelect: "text",
452 | borderRadius: 6
453 | },
454 | children: React.createElement("code", { dangerouslySetInnerHTML: { __html: highlighted.value } })
455 | }),
456 | React.createElement("div", {
457 | className: `button copy${copied ? " copied" : ""}`,
458 | style: {
459 | position: "absolute",
460 | bottom: 8,
461 | right: 8,
462 | zIndex: 1
463 | },
464 | onClick: copy,
465 | children: React.createElement("svg", {
466 | viewBox: "0 0 24 24",
467 | height: 24,
468 | width: 24,
469 | children: [
470 | React.createElement("path", {
471 | fill: "currentColor",
472 | d: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z"
473 | }),
474 | React.createElement("path", {
475 | fill: "currentColor",
476 | d: "M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z"
477 | })
478 | ]
479 | })
480 | })
481 | ]
482 | })
483 | ]
484 | });
485 | };
486 |
487 | function ErrorSplash({ src, errorInfo, windowKey }) {
488 | const Window = useStateFromStores([ PopoutWindowStore ], () => PopoutWindowStore.getWindow(windowKey))
489 |
490 | React.useLayoutEffect(() => {
491 | const style = document.createElement("style");
492 | style.innerText = `.button {
493 | display: flex;
494 | color: var(--interactive-normal);
495 | cursor: pointer;
496 | padding: 4px;
497 | border-radius: 4px;
498 | } .button:hover {
499 | color: var(--interactive-hover);
500 | background-color: var(--background-modifier-hover);
501 | } .button:hover:active {
502 | color: var(--interactive-active);
503 | background-color: var(--background-modifier-active);
504 | } .copy {
505 | background: var(--background-modifier-accent);
506 | } .copied {
507 | color: var(--button-positive-background)
508 | } .copied:hover {
509 | color: var(--button-positive-background-hover)
510 | } .copied:hover:active {
511 | color: var(--button-positive-background-active)
512 | }`;
513 | Window.document.head.appendChild(style);
514 | }, [ ]);
515 |
516 | return React.createElement(React.Fragment, {
517 | children: [
518 | React.createElement("div", {
519 | className: `${errorClasses.wrapper} ${errorPage}`,
520 | style: {
521 | display: "flex",
522 | position: "fixed",
523 | left: 0,
524 | top: 0,
525 | minHeight: "100%"
526 | },
527 | children: [
528 | React.createElement(Flex, {
529 | align: Flex.Align.CENTER,
530 | justify: Flex.Justify.CENTER,
531 | direction: Flex.Direction.VERTICAL,
532 | className: classes.flexWrapper,
533 | shrink: 1,
534 | grow: 1,
535 | children: [
536 | React.createElement(Flex.Child, {
537 | className: errorClasses.image,
538 | grow: 0,
539 | shrink: 1,
540 | wrap: false
541 | }),
542 | React.createElement(Flex.Child, {
543 | className: errorClasses.text,
544 | grow: 0,
545 | shrink: 1,
546 | wrap: false,
547 | children: [
548 | React.createElement("h2", {
549 | className: errorClasses.title,
550 | children: intl.Messages.UNSUPPORTED_BROWSER_TITLE
551 | }),
552 | React.createElement("div", {
553 | className: errorClasses.note,
554 | children: React.createElement("div", {
555 | children: [
556 | React.createElement("p", {}, intl.Messages.CRASH_UNEXPECTED.replace("Discord", "Better Media Player")),
557 | React.createElement("p", {}, intl.Messages.ERRORS_UNEXPECTED_CRASH.replace("Discord", "Better Media Player"))
558 | ]
559 | })
560 | })
561 | ]
562 | }),
563 | React.createElement("div", {
564 | className: buttons,
565 | children: [
566 | React.createElement(Button, {
567 | size: Button.Sizes.LARGE,
568 | onClick: () => Window.close(),
569 | children: intl.Messages.CLOSE
570 | })
571 | ]
572 | })
573 | ]
574 | })
575 | ]
576 | }),
577 | React.createElement("div", {
578 | className: "button",
579 | style: {
580 | right: 8,
581 | bottom: 8,
582 | position: "fixed"
583 | },
584 | onClick: () => UI.alert(generateTitle(errorInfo), React.createElement(ErrorModal, { src, errorInfo })),
585 | children: React.createElement("svg", {
586 | viewBox: "0 0 24 24",
587 | height: 24,
588 | width: 24,
589 | children: React.createElement("path", {
590 | fill: "currentColor",
591 | d: "M12 2C6.486 2 2 6.487 2 12C2 17.515 6.486 22 12 22C17.514 22 22 17.515 22 12C22 6.487 17.514 2 12 2ZM12 18.25C11.31 18.25 10.75 17.691 10.75 17C10.75 16.31 11.31 15.75 12 15.75C12.69 15.75 13.25 16.31 13.25 17C13.25 17.691 12.69 18.25 12 18.25ZM13 13.875V15H11V12H12C13.104 12 14 11.103 14 10C14 8.896 13.104 8 12 8C10.896 8 10 8.896 10 10H8C8 7.795 9.795 6 12 6C14.205 6 16 7.795 16 10C16 11.861 14.723 13.429 13 13.875Z"
592 | })
593 | })
594 | })
595 | ]
596 | })
597 | };
598 |
599 | function generateTitle(error) {
600 | const reactError = error.message.match(/Minified React error #[0-9]+;/);
601 |
602 | if (!reactError) return intl.Messages.HELP;
603 |
604 | const [ message ] = reactError;
605 |
606 | return message.slice(0, message.length - 1);
607 | };
608 |
609 | function PictureInPicture({ src, windowKey }) {
610 | /** @type {React.RefObject} */
611 | const videoRef = React.useRef(null);
612 | const [ state, setState ] = React.useState(0);
613 | /** @type {Window} */
614 | const Window = useStateFromStores([ PopoutWindowStore ], () => PopoutWindowStore.getWindow(windowKey))
615 |
616 | React.useLayoutEffect(() => {
617 | const video = videoRef.current;
618 | if (!video) return;
619 |
620 | const style = document.createElement("style");
621 | style.innerText = `#wrapper {
622 | width: 100%;
623 | height: 100%;
624 | }
625 | #wrapper:hover #controls, .show-controls #controls {
626 | transform: translateX(-50%);
627 | opacity: 1;
628 | }
629 | #video {
630 | width: 100%;
631 | height: 100%;
632 | background: black;
633 | }
634 | #loop, #download, #video, #action, #pin, #volume {
635 | cursor: pointer;
636 | color: var(--interactive-normal);
637 | }
638 | #loop:hover, #download:hover, #video:hover, #action:hover, #pin:hover, #volume:hover {
639 | cursor: pointer;
640 | color: var(--interactive-hover);
641 | }
642 | #loop:active:hover, #download:active:hover, #video:active:hover, #action:active:hover, #pin:active:hover, #volume:active:hover {
643 | cursor: pointer;
644 | color: var(--interactive-active);
645 | }
646 | #controls {
647 | position: fixed;
648 | left: 50%;
649 | box-sizing: border-box;
650 | width: min(calc(100vw - 32px), 500px);
651 | bottom: 16px;
652 | transform: translateX(-50%) translatey(calc(100% + 16px));
653 | background: rgba(0, 0, 0, 0.294);
654 | color: white;
655 | padding: 8px;
656 | height: 40px;
657 | border-radius: 24px;
658 | opacity: 0;
659 | transition: transform 150ms ease, opacity 150ms ease;
660 | display: flex;
661 | gap: 8px;
662 | align-items: center;
663 | }
664 | .volumeSlider {
665 | transform: translate(10px, -7px);
666 | background: rgba(0, 0, 0, 0.294);
667 | }
668 | #loop.active {
669 | color: var(--brand-experiment);
670 | }
671 | #slider {
672 | display: flex;
673 | width: 100%;
674 | align-self: stretch;
675 | }
676 | #duration {
677 | display: flex;
678 | align-items: center;
679 | flex: 0 0 auto;
680 | }`;
681 | Window.document.head.appendChild(style);
682 |
683 | function play() { setState(0); };
684 | function pause() { setState(1); };
685 | function ended() {
686 | setState(2);
687 | showControls(true);
688 | };
689 |
690 | video.addEventListener("play", play);
691 | video.addEventListener("pause", pause);
692 | video.addEventListener("ended", ended);
693 |
694 | return () => {
695 | video.removeEventListener("play", play);
696 | video.removeEventListener("pause", pause);
697 | video.removeEventListener("ended", ended);
698 | };
699 | }, [ ]);
700 |
701 | const [ volume, setVolume ] = useVolume();
702 | const [ muted, setMuted ] = useMuted();
703 | React.useLayoutEffect(() => {
704 | const video = videoRef.current;
705 | if (!video) return;
706 | video.volume = muted ? 0 : volume;
707 | }, [ volume, muted ]);
708 |
709 | const [ duration, setDuration ] = React.useState(0);
710 | const [ currentTime, setCurrentTime ] = React.useState(0);
711 |
712 | const durationBar = React.useRef();
713 |
714 | const [ paused, setPaused ] = React.useState(false);
715 | const [ shouldShowControls, showControls ] = React.useState(false);
716 |
717 | const [ buffers, setBuffers ] = React.useState([ ]);
718 |
719 | return React.createElement("div", {
720 | id: "wrapper",
721 | className: shouldShowControls ? "show-controls" : "",
722 | onContextMenu: () => showControls(!shouldShowControls),
723 | children: [
724 | React.createElement("video", {
725 | id: "video",
726 | src,
727 | autoPlay: true,
728 | ref: videoRef,
729 | onProgress: () => setBuffers(getBuffers(videoRef.current)),
730 | onClick: () => {
731 | setPaused(!videoRef.current.paused);
732 | showControls(!videoRef.current.paused);
733 | if (videoRef.current.paused) videoRef.current.play();
734 | else videoRef.current.pause();
735 | },
736 | onTimeUpdate: (event) => {
737 | if (!durationBar.current) return;
738 | setCurrentTime(event.currentTarget.currentTime);
739 | durationBar.current.setGrabber(
740 | event.currentTarget.currentTime / event.currentTarget.duration
741 | );
742 | },
743 | onLoadedData: (event) => setDuration(event.currentTarget.duration)
744 | }),
745 | React.createElement("div", {
746 | id: "controls",
747 | children: [
748 | React.createElement("div", {
749 | id: "action",
750 | children: [
751 | state === 0 && React.createElement(Pause, { width: 24, height: 24 }),
752 | state === 1 && React.createElement(Play, { width: 24, height: 24 }),
753 | state === 2 && React.createElement(Replay, { width: 24, height: 24 })
754 | ],
755 | onClick: () => {
756 | setPaused(!videoRef.current.paused);
757 | showControls(!videoRef.current.paused);
758 | if (videoRef.current.paused) videoRef.current.play();
759 | else videoRef.current.pause();
760 | }
761 | }),
762 | React.createElement("div", {
763 | id: "loop",
764 | children: React.createElement(Loop, { width: 24, height: 24 }),
765 | onClick: (event) => {
766 | const video = videoRef.current;
767 | if (!video) return;
768 |
769 | if (video.loop = !video.loop) event.currentTarget.classList.add("active");
770 | else event.currentTarget.classList.remove("active");
771 | }
772 | }),
773 | React.createElement(Duration, {
774 | currentTime: Math.floor(currentTime),
775 | duration: Math.floor(duration)
776 | }),
777 | React.createElement("div", {
778 | id: "slider",
779 | children: React.createElement(DurationBar, {
780 | buffers,
781 | currentWindow: Window,
782 | type: "DURATION",
783 | value: duration,
784 | onDrag: (multiplier) => {
785 | if (multiplier === 1) {
786 | videoRef.current.pause();
787 | setPaused(true);
788 | }
789 | else videoRef.current.currentTime = multiplier * videoRef.current.duration;
790 |
791 | setCurrentTime(videoRef.current.currentTime);
792 |
793 | if (durationBar.current) durationBar.current.setGrabber(multiplier);
794 | },
795 | onDragEnd: () => {
796 | if (!paused) videoRef.current.play();
797 | },
798 | onDragStart: () => {
799 | videoRef.current.pause();
800 | },
801 | ref: durationBar
802 | })
803 | }),
804 | React.createElement("div", {
805 | id: "volume",
806 | children: React.createElement(VolumeSlider, {
807 | minValue: 0,
808 | maxValue: 1,
809 | value: volume,
810 | currentWindow: Window,
811 | onValueChange: (volume) => {
812 | setVolume(volume);
813 | setMuted(false);
814 | },
815 | muted,
816 | onToggleMute: () => setMuted(!muted),
817 | sliderClassName: "volumeSlider"
818 | })
819 | }),
820 | React.createElement("a", {
821 | id: "download",
822 | href: src,
823 | target: "_blank",
824 | rel: "noreferrer noopener",
825 | role: "button",
826 | children: React.createElement(Download, { width: 24, height: 24 })
827 | })
828 | ]
829 | })
830 | ]
831 | });
832 | };
833 |
834 | function Popout({ src, windowKey }) {
835 | const fileName = React.useMemo(() => {
836 | const dirname = DiscordNative.fileManager.dirname(src);
837 | return src.replace(`${dirname}/`, "");
838 | }, [ ]);
839 |
840 | return React.createElement(PopoutWindow, {
841 | windowKey: windowKey,
842 | withTitleBar: true,
843 | macOSFrame: true,
844 | title: `${fileName} - Discord`,
845 | children: React.createElement(ErrorBoundary, {
846 | windowKey,
847 | src: src
848 | })
849 | });
850 | };
851 |
852 | const encodeBase64 = (str) => {
853 | return btoa(new TextEncoder().encode(str).reduce((data, byte) => data + String.fromCharCode(byte), ""));
854 | };
855 |
856 | const appendPipButton = (videoButtons) => {
857 | /** @type {HTMLVideoElement} */
858 | const video = videoButtons.parentElement.querySelector("video");
859 |
860 | const node = document.createElement("div");
861 | node.addEventListener("click", () => {
862 | const windowKey = `DISCORD_PIP_${encodeBase64(video.src)}`;
863 |
864 | if (node.classList.contains("BMP_active")) {
865 | node.classList.remove("BMP_active");
866 | return PopoutWindowStore.unmountWindow(windowKey);
867 | };
868 |
869 | dispatcher.dispatch({
870 | type: "POPOUT_WINDOW_OPEN",
871 | key: windowKey,
872 | render: () => React.createElement(Popout, {
873 | windowKey,
874 | src: video.src
875 | }),
876 | features: {popout: true}
877 | });
878 | // Listener to remove the active class
879 | function listener() {
880 | if (PopoutWindowStore.getWindowOpen(windowKey)) return;
881 |
882 | node.classList.remove("BMP_active");
883 | PopoutWindowStore.removeChangeListener(listener);
884 | };
885 | PopoutWindowStore.addChangeListener(listener);
886 |
887 | node.classList.add("BMP_active");
888 | })
889 |
890 | node.classList.add("BMP_button");
891 |
892 | node.innerHTML = ``;
896 |
897 | videoButtons.insertBefore(node, videoButtons.childNodes[videoButtons.childNodes.length - 1])
898 | };
899 |
900 | const observer = new MutationObserver((records) => {
901 | for (const { addedNodes } of records) {
902 | /** @type {HTMLElement} */
903 | const videoButton = Array.from(addedNodes).find(node => node.classList.value === classes.videoControls);
904 | if (!videoButton) continue;
905 |
906 | appendPipButton(videoButton);
907 | appendLoopButton(videoButton);
908 | };
909 | });
910 |
911 | module.exports = class BetterMediaPlayer {
912 | observer(record) {
913 | for (const added of record.addedNodes) {
914 | for (const video of getAllMediaPlayers(added)) {
915 | const videoButton = video.parentElement.querySelector(`.${classes.videoControls}`);
916 |
917 | if (videoButton) {
918 | appendPipButton(videoButton);
919 | appendLoopButton(videoButton);
920 | }
921 | else observer.observe(video.parentElement, {
922 | childList: true,
923 | attributes: true
924 | });
925 | }
926 | }
927 | };
928 | start() {
929 | this.observer({ addedNodes: [ document ] });
930 |
931 | DOM.addStyle(".BMP_active svg { color: var(--brand-500) }");
932 | };
933 | stop() {
934 | DOM.removeStyle();
935 |
936 | Array.from(document.querySelectorAll("[data-bmp-hook]"), node => node.removeAttribute("data-bmp-hook"));
937 | Array.from(document.querySelectorAll(".BMP_button"), node => node.remove());
938 |
939 | for (const key of PopoutWindowStore.getWindowKeys()) {
940 | if (!key.startsWith("DISCORD_PIP_")) continue;
941 | try {
942 | PopoutWindowStore.unmountWindow(key);
943 | }
944 | catch (error) {
945 | console.groupCollapsed(
946 | `%cBMP%c Error accord when unmounting%c\n${key.replace("DISCORD_PIP_", "")}`,
947 | "color: #202124; padding: 3px 2px; background: #ed4245; border-radius: 3px;",
948 | "color: red",
949 | "color: yellow"
950 | );
951 | console.error(error);
952 | console.groupEnd();
953 | };
954 | };
955 |
956 | observer.disconnect();
957 | };
958 | };
959 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # BetterMediaPlayer
3 |
4 | Adds more functionality to the media player in discord.
5 |
6 | ## Example
7 |
8 | 
9 |
10 |
--------------------------------------------------------------------------------