Plugin

Dearrow

Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow

Media Utility
index.tsx
Download

Source

src/plugins/dearrow/index.tsx
1import "./styles.css";
2
3import { definePluginSettings } from "@api/Settings";
4import ErrorBoundary from "@components/ErrorBoundary";
5import { Devs } from "@utils/constants";
6import { Logger } from "@utils/Logger";
7import definePlugin, { OptionType } from "@utils/types";
8import { Tooltip } from "@webpack/common";
9import type { Component } from "react";
10
11interface Props {
12 embed: {
13 rawTitle: string;
14 provider?: {
15 name: string;
16 };
17 thumbnail: {
18 proxyURL: string;
19 };
20 video: {
21 url: string;
22 };
23
24 dearrow: {
25 enabled: boolean;
26 oldTitle?: string;
27 oldThumb?: string;
28 };
29 };
30}
31
32const enum ReplaceElements {
33 ReplaceAllElements,
34 ReplaceTitlesOnly,
35 ReplaceThumbnailsOnly
36}
37
38const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
39
40async function embedDidMount(this: Component<Props>) {
41 try {
42 const { embed } = this.props;
43 const { replaceElements, dearrowByDefault } = settings.store;
44
45 if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
46
47 const videoId = embedUrlRe.exec(embed.video.url)?.[1];
48 if (!videoId) return;
49
50 const res = await fetch(`https:class="ts-cmt">//sponsor.ajay.app/api/branding?videoID=${videoId}`);
51 if (!res.ok) return;
52
53 const { titles, thumbnails } = await res.json();
54
55 const hasTitle = titles[0]?.votes >= 0;
56 const hasThumb = thumbnails[0]?.votes >= 0 && !thumbnails[0].original;
57
58 if (!hasTitle && !hasThumb) return;
59
60
61 embed.dearrow = {
62 enabled: dearrowByDefault
63 };
64
65 if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
66 const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
67
68 embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;
69 if (dearrowByDefault) embed.rawTitle = replacementTitle;
70 }
71 if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
72 const replacementProxyURL = `https:class="ts-cmt">//dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
73
74 embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL;
75 if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;
76 }
77
78 this.forceUpdate();
79 } catch (err) {
80 new Logger("Dearrow").error("Failed to dearrow embed", err);
81 }
82}
83
84function DearrowButton({ component }: { component: Component<Props>; }) {
85 const { embed } = component.props;
86 if (!embed?.dearrow) return null;
87
88 return (
89 <Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>
90 {({ onMouseEnter, onMouseLeave }) => (
91 <button
92 onMouseEnter={onMouseEnter}
93 onMouseLeave={onMouseLeave}
94 className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
95 onClick={() => {
96 const { enabled, oldThumb, oldTitle } = embed.dearrow;
97 settings.store.dearrowByDefault = !enabled;
98 embed.dearrow.enabled = !enabled;
99 if (oldTitle) {
100 embed.dearrow.oldTitle = embed.rawTitle;
101 embed.rawTitle = oldTitle;
102 }
103 if (oldThumb) {
104 embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
105 embed.thumbnail.proxyURL = oldThumb;
106 }
107
108 component.forceUpdate();
109 }}
110 >
111 {/* Dearrow Icon, taken from https:class="ts-cmt">//dearrow.ajay.app/logo.svg (and optimised) */}
112 <svg
113 xmlns="http:class="ts-cmt">//www.w3.org/2000/svg"
114 width="24px"
115 height="24px"
116 viewBox="0 0 36 36"
117 aria-label="Toggle Dearrow"
118 className="vc-dearrow-icon"
119 >
120 <path
121 fill="#1213BD"
122 d="M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z"
123 />
124 <path
125 fill="#88c9f9"
126 d="m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z"
127 />
128 <path
129 fill="#0a62a5"
130 d="m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z"
131 />
132 </svg>
133
134 </button>
135 )}
136 </Tooltip>
137 );
138}
139
140const settings = definePluginSettings({
141 hideButton: {
142 description: "Hides the Dearrow button from YouTube embeds",
143 type: OptionType.BOOLEAN,
144 default: false,
145 restartNeeded: true
146 },
147 replaceElements: {
148 description: "Choose which elements of the embed will be replaced",
149 type: OptionType.SELECT,
150 restartNeeded: true,
151 options: [
152 { label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
153 { label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
154 { label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
155 ],
156 },
157 dearrowByDefault: {
158 description: "Dearrow videos automatically",
159 type: OptionType.BOOLEAN,
160 default: true,
161 restartNeeded: false
162 }
163});
164
165export default definePlugin({
166 name: "Dearrow",
167 description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
168 tags: ["Media", "Utility"],
169 authors: [Devs.Ven],
170 settings,
171
172 embedDidMount,
173 renderButton(component: Component<Props>) {
174 return (
175 <ErrorBoundary noop>
176 <DearrowButton component={component} />
177 </ErrorBoundary>
178 );
179 },
180
181 patches: [{
182 find: "this.renderInlineMediaEmbed",
183 replacement: [
184 // patch componentDidMount to replace embed thumbnail and title
185 {
186 match: /render\(\)\{.{0,30}let\{embed:/,
187 replace: "componentDidMount=$self.embedDidMount;$&"
188 },
189
190 // add dearrow button
191 {
192 match: /children:\[(?=null!=\i\?(\i)\.renderSuppressButton)/,
193 replace: "children:[$self.renderButton($1),",
194 predicate: () => !settings.store.hideButton
195 }
196 ]
197 }],
198});
199