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