Plugin
Dearrow
Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow
1
import "./styles.css";2
3
import { definePluginSettings } from "@api/Settings";4
import ErrorBoundary from "@components/ErrorBoundary";5
import { Devs } from "@utils/constants";6
import { Logger } from "@utils/Logger";7
import definePlugin, { OptionType } from "@utils/types";8
import { Tooltip } from "@webpack/common";9
import type { Component } from "react";10
11
interface 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
32
const enum ReplaceElements {33
ReplaceAllElements,34
ReplaceTitlesOnly,35
ReplaceThumbnailsOnly36
}37
38
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;39
40
async 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: dearrowByDefault63
};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
84
function 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
<button92
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
<svg113
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
<path121
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
<path125
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
<path129
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
140
const settings = definePluginSettings({141
hideButton: {142
description: "Hides the Dearrow button from YouTube embeds",143
type: OptionType.BOOLEAN,144
default: false,145
restartNeeded: true146
},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: false162
}163
});164
165
export 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 title185
{186
match: /render\(\)\{.{0,30}let\{embed:/,187
replace: "componentDidMount=$self.embedDidMount;$&"188
},189
190
// add dearrow button191
{192
match: /children:\[(?=null!=\i\?(\i)\.renderSuppressButton)/,193
replace: "children:[$self.renderButton($1),",194
predicate: () => !settings.store.hideButton195
}196
]197
}],198
});199