Plugin
Dearrow
Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./styles.css";8
9
import { definePluginSettings } from "@api/Settings";10
import ErrorBoundary from "@components/ErrorBoundary";11
import { Devs } from "@utils/constants";12
import { Logger } from "@utils/Logger";13
import definePlugin, { OptionType } from "@utils/types";14
import { Tooltip } from "@webpack/common";15
import type { Component } from "react";16
17
interface Props {18
embed: {19
rawTitle: string;20
provider?: {21
name: string;22
};23
thumbnail: {24
proxyURL: string;25
};26
video: {27
url: string;28
};29
30
dearrow: {31
enabled: boolean;32
oldTitle?: string;33
oldThumb?: string;34
};35
};36
}37
38
const enum ReplaceElements {39
ReplaceAllElements,40
ReplaceTitlesOnly,41
ReplaceThumbnailsOnly42
}43
44
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;45
46
async function embedDidMount(this: Component<Props>) {47
try {48
const { embed } = this.props;49
const { replaceElements, dearrowByDefault } = settings.store;50
51
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;52
53
const videoId = embedUrlRe.exec(embed.video.url)?.[1];54
if (!videoId) return;55
56
const res = await fetch(`https:class="ts-cmt">//sponsor.ajay.app/api/branding?videoID=${videoId}`);57
if (!res.ok) return;58
59
const { titles, thumbnails } = await res.json();60
61
const hasTitle = titles[0]?.votes >= 0;62
const hasThumb = thumbnails[0]?.votes >= 0 && !thumbnails[0].original;63
64
if (!hasTitle && !hasThumb) return;65
66
67
embed.dearrow = {68
enabled: dearrowByDefault69
};70
71
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {72
const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");73
74
embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;75
if (dearrowByDefault) embed.rawTitle = replacementTitle;76
}77
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {78
const replacementProxyURL = `https:class="ts-cmt">//dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;79
80
embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL;81
if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;82
}83
84
this.forceUpdate();85
} catch (err) {86
new Logger("Dearrow").error("Failed to dearrow embed", err);87
}88
}89
90
function DearrowButton({ component }: { component: Component<Props>; }) {91
const { embed } = component.props;92
if (!embed?.dearrow) return null;93
94
return (95
<Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>96
{({ onMouseEnter, onMouseLeave }) => (97
<button98
onMouseEnter={onMouseEnter}99
onMouseLeave={onMouseLeave}100
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}101
onClick={() => {102
const { enabled, oldThumb, oldTitle } = embed.dearrow;103
settings.store.dearrowByDefault = !enabled;104
embed.dearrow.enabled = !enabled;105
if (oldTitle) {106
embed.dearrow.oldTitle = embed.rawTitle;107
embed.rawTitle = oldTitle;108
}109
if (oldThumb) {110
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;111
embed.thumbnail.proxyURL = oldThumb;112
}113
114
component.forceUpdate();115
}}116
>117
{/* Dearrow Icon, taken from https:class="ts-cmt">//dearrow.ajay.app/logo.svg (and optimised) */}118
<svg119
xmlns="http:class="ts-cmt">//www.w3.org/2000/svg"120
width="24px"121
height="24px"122
viewBox="0 0 36 36"123
aria-label="Toggle Dearrow"124
className="vc-dearrow-icon"125
>126
<path127
fill="#1213BD"128
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"129
/>130
<path131
fill="#88c9f9"132
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"133
/>134
<path135
fill="#0a62a5"136
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"137
/>138
</svg>139
140
</button>141
)}142
</Tooltip>143
);144
}145
146
const settings = definePluginSettings({147
hideButton: {148
description: "Hides the Dearrow button from YouTube embeds",149
type: OptionType.BOOLEAN,150
default: false,151
restartNeeded: true152
},153
replaceElements: {154
description: "Choose which elements of the embed will be replaced",155
type: OptionType.SELECT,156
restartNeeded: true,157
options: [158
{ label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },159
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },160
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },161
],162
},163
dearrowByDefault: {164
description: "Dearrow videos automatically",165
type: OptionType.BOOLEAN,166
default: true,167
restartNeeded: false168
}169
});170
171
export default definePlugin({172
name: "Dearrow",173
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",174
tags: ["Media", "Utility"],175
authors: [Devs.Ven],176
settings,177
178
embedDidMount,179
renderButton(component: Component<Props>) {180
return (181
<ErrorBoundary noop>182
<DearrowButton component={component} />183
</ErrorBoundary>184
);185
},186
187
patches: [{188
find: "this.renderInlineMediaEmbed",189
replacement: [190
// patch componentDidMount to replace embed thumbnail and title191
{192
match: /render\(\)\{.{0,30}let\{embed:/,193
replace: "componentDidMount=$self.embedDidMount;$&"194
},195
196
// add dearrow button197
{198
match: /children:\[(?=null!=\i\?(\i)\.renderSuppressButton)/,199
replace: "children:[$self.renderButton($1),",200
predicate: () => !settings.store.hideButton201
}202
]203
}],204
});205