Plugin
OpenInApp
Open links in their respective apps instead of your browser
1
import { definePluginSettings } from "@api/Settings";2
import { Devs } from "@utils/constants";3
import definePlugin, { OptionType, PluginNative, SettingsDefinition } from "@utils/types";4
import { showToast, Toasts } from "@webpack/common";5
import type { MouseEvent } from "react";6
7
interface URLReplacementRule {8
match: RegExp;9
replace: (...matches: string[]) => string;10
displayName?: string;11
description: string;12
shortlinkMatch?: RegExp;13
accountViewReplace?: (userId: string) => string;14
}15
16
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant17
const UrlReplacementRules: Record<string, URLReplacementRule> = {18
spotify: {19
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode|prerelease)\/(.+)(?:\?.+?)?$/,20
replace: (_, type, id) => `spotify:class="ts-cmt">//${type}/${id}`,21
description: "Open Spotify links in the Spotify app",22
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,23
accountViewReplace: userId => `spotify:user:${userId}`,24
},25
steam: {26
match: /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/,27
replace: match => `steam:class="ts-cmt">//openurl/${match}`,28
description: "Open Steam links in the Steam app",29
shortlinkMatch: /^https:\/\/s.team\/.+$/,30
accountViewReplace: userId => `steam:class="ts-cmt">//openurl/https://steamcommunity.com/profiles/${userId}`,31
},32
epic: {33
match: /^https:\/\/store\.epicgames\.com\/(.+)$/,34
replace: (_, id) => `com.epicgames.launcher:class="ts-cmt">//store/${id}`,35
description: "Open Epic Games links in the Epic Games Launcher",36
},37
tidal: {38
match: /^https:\/\/(?:listen\.)?tidal\.com\/(?:browse\/)?(track|album|artist|playlist|user|video|mix)\/([a-f0-9-]+).*/,39
replace: (_, type, id) => `tidal:class="ts-cmt">//${type}/${id}`,40
description: "Open Tidal links in the Tidal app",41
},42
itunes: {43
match: /^https:\/\/(?:geo\.)?music\.apple\.com\/([a-z]{2}\/)?(album|artist|playlist|song|curator)\/([^/?#]+)\/?([^/?#]+)?(?:\?.*)?(?:#.*)?$/,44
replace: (_, lang, type, name, id) => id ? `itunes:class="ts-cmt">//music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`,45
displayName: "iTunes",46
description: "Open Apple Music links in the iTunes app"47
},48
};49
50
const pluginSettings = definePluginSettings(51
Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => {52
acc[key] = {53
type: OptionType.BOOLEAN,54
displayName: rule.displayName,55
description: rule.description,56
default: true,57
};58
return acc;59
}, {} as SettingsDefinition)60
);61
62
63
const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>;64
65
export default definePlugin({66
name: "OpenInApp",67
description: "Open links in their respective apps instead of your browser",68
tags: ["Utility"],69
authors: [Devs.Ven, Devs.surgedevs],70
settings: pluginSettings,71
72
patches: [73
{74
find: "trackAnnouncementMessageLinkClicked({",75
replacement: {76
match: /function (\i\(\i,\i\)\{)(?=.{0,150}trusted:)/,77
replace: "async function $1 if(await $self.handleLink(...arguments)) return;"78
}79
},80
{81
find: "no artist ids in metadata",82
predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify,83
replacement: [84
{85
match: /\i\.\i\.isProtocolRegistered\(\)/g,86
replace: "true"87
},88
{89
match: /\(0,\i\.isDesktop\)\(\)/,90
replace: "true"91
}92
]93
},94
95
// User Profile Modal & User Profile Modal v296
...[".__invalid_connectedAccountOpenIconContainer", ".BLUESKY||"].map(find => ({97
find,98
replacement: {99
match: /(?<=onClick:(\i)=>\{)(?=.{0,100}\.CONNECTED_ACCOUNT_VIEWED)(?<==(\i)\.metadata.+?)/,100
replace: "if($self.handleAccountView($1,$2.type,$2.id)) return;"101
}102
}))103
],104
105
async handleLink(data: { href: string; }, event?: MouseEvent) {106
if (!data) return false;107
108
let url = data.href;109
if (!url) return false;110
111
for (const [key, rule] of Object.entries(UrlReplacementRules)) {112
if (!pluginSettings.store[key]) continue;113
114
if (rule.shortlinkMatch?.test(url)) {115
event?.preventDefault();116
url = await Native.resolveRedirect(url);117
}118
119
if (rule.match.test(url)) {120
showToast("Opened link in native app", Toasts.Type.SUCCESS);121
122
const newUrl = url.replace(rule.match, rule.replace);123
VencordNative.native.openExternal(newUrl);124
125
event?.preventDefault();126
return true;127
}128
}129
130
// in case short url didn't end up being something we can handle131
if (event?.defaultPrevented) {132
window.open(url, "_blank");133
return true;134
}135
136
return false;137
},138
139
handleAccountView(e: MouseEvent, platformType: string, userId: string) {140
const rule = UrlReplacementRules[platformType];141
if (rule?.accountViewReplace && pluginSettings.store[platformType]) {142
VencordNative.native.openExternal(rule.accountViewReplace(userId));143
e.preventDefault();144
return true;145
}146
}147
});148