diff --git a/FR/README_FR.md b/FR/README_FR.md new file mode 100644 index 00000000..2f9f903 --- /dev/null +++ b/FR/README_FR.md @@ -0,0 +1,95 @@ +# Vencord Installer + +L'installateur Vencord vous permet d'installer [Vencord, le plus cute des clients moddés Discord ](https://github.com/Vendicated/Vencord) + +![image](https://user-images.githubusercontent.com/45497981/226734476-5fb42420-844d-4e27-ae06-4799118e086e.png) + +## Usage + +### Windows + +> **ATTENTION** +**NE PAS** Executer en temps qu'administrateur + +Télécharger [VencordInstaller.exe](https://github.com/Vencord/Installer/releases/latest/download/VencordInstaller.exe) et l'executer. + +Si l'installateur graphique ne s'ouvre pas, par exemple si vous utilisez Windows 7, 32bits, ou que votre Carte Graphique est ancienne, vous pouvez utiliser notre installateur en ligne de commande. +Pour se faire, ouvrez Powershell, executez la commande suivante, puis suivez les instructions. + + +```ps1 +iwr "https://raw.githubusercontent.com/Vencord/Installer/main/install.ps1" -UseBasicParsing | iex +``` + +### Linux + +Executez la commande suivante depuis votre terminal, puis suivez les instructions. + + +```sh +sh -c "$(curl -sS https://raw.githubusercontent.com/Vendicated/VencordInstaller/main/install.sh)" +``` + +### MacOs + +Téléchargez la dernière version du [MacOs build](https://github.com/Vencord/Installer/releases/latest/download/VencordInstaller.MacOS.zip), dé-zippez le, puis executez `VencordInstaller.app` + +Si vous obtenez l'erreur `VencordInstaller can't be opened`, faites un clic-droit sur `VencordInstaller.app` and cliquez sur ouvrir. + +Cet avertissement s'affiche car l'application n'est pas signée, car je ne suis pas prêt à payer 100$ par an pour une licence de développeur Apple. + +___ + +## Compilation depuis les sources + +### Pre-requis + +Vous devez installer le compilateur [du language de programmation Go](https://go.dev/doc/install) et GCC, le GNU Compiler COllection (MinGW sous Windows) + +
+Sous linux, il faut aussi installer les dépendances suivantes + +#### Dépendances de base +```sh +apt install -y pkg-config libsdl2-dev libglx-dev libgl1-mesa-dev +``` + +#### Dépendances X11 +```sh +apt install -y xorg-dev +``` + +#### Dépendances Wayland +```sh +apt install -y libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules +``` + +
+ +### Compilation + +#### Installation des dépendances + +```sh +go mod tidy +``` + +#### Compilation du GUI + +##### Windows / Mac / Linux X11 +```sh +go build +``` + +##### Linux Wayland +```sh +go build --tags wayland +``` + +#### Compilation du CLI +``` +go build --tags cli +``` + +Pour obtenir une meilleure compilation, vous pouvez utiliser des flags différents. +Référez vous au [Github du projet](https://github.com/Vendicated/VencordInstaller/blob/main/.github/workflows/release.yml) pour voir les flags utilisés pour les builds officiels. diff --git a/FR/cli_FR.go b/FR/cli_FR.go new file mode 100644 index 00000000..6c8fc22 --- /dev/null +++ b/FR/cli_FR.go @@ -0,0 +1,110 @@ +//go:build cli +/* + * SPDX-License-Identifier: GPL-3.0 + * Vencord Installer, a cross platform gui/cli app for installing Vencord + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +package main + +import ( + "errors" + "flag" + "fmt" +) + +var discords []any + +func main() { + InitGithubDownloader() + discords = FindDiscords() + + var installFlag = flag.Bool("install", false, "Installer Vencord pour une installation Discord") + var uninstallFlag = flag.Bool("uninstall", false, "Désinstaller Vencord pour une installation Discord") + var installOpenAsar = flag.Bool("install-openasar", false, "Installer OpenAsar pour une installation Discord") + var uninstallOpenAsar = flag.Bool("uninstall-openasar", false, "Désinstaller OpenAsar pour une installation Discord") + var updateFlag = flag.Bool("update", false, "Mettre à jour vos fichiers locaux Vencord") + flag.Parse() + + if *installFlag || *updateFlag { + if !<-GithubDoneChan { + fmt.Println("Not", Ternary(*installFlag, "installing", "updating"), "as fetching release data failed") + return + } + } + + fmt.Println("Vencord Installer cli", InstallerTag, "("+InstallerGitHash+")") + + var err error + if *installFlag { + _ = PromptDiscord("patch").patch() + } else if *uninstallFlag { + _ = PromptDiscord("unpatch").unpatch() + } else if *updateFlag { + _ = installLatestBuilds() + } else if *installOpenAsar { + discord := PromptDiscord("patch") + if !discord.IsOpenAsar() { + err = discord.InstallOpenAsar() + } else { + err = errors.New("OpenAsar déjà installé") + } + } else if *uninstallOpenAsar { + discord := PromptDiscord("patch") + if discord.IsOpenAsar() { + err = discord.UninstallOpenAsar() + } else { + err = errors.New("OpenAsar n'est pas installé") + } + } else { + flag.Usage() + } + + if err != nil { + fmt.Println(err) + } +} + +func PromptDiscord(action string) *DiscordInstall { + fmt.Println("Merci de choisir l'installation Discord à patcher", action) + for i, discord := range discords { + install := discord.(*DiscordInstall) + fmt.Printf("[%d] %s%s (%s)\n", i+1, Ternary(install.isPatched, "(PATCHED) ", ""), install.path, install.branch) + } + fmt.Printf("[%d] Emplacement personnalisée\n", len(discords)+1) + + var choice int + for { + fmt.Printf("> ") + if _, err := fmt.Scan(&choice); err != nil { + fmt.Println("Choix non valide") + continue + } + + choice-- + if choice >= 0 && choice < len(discords) { + return discords[choice].(*DiscordInstall) + } + + if choice == len(discords) { + var custom string + fmt.Print("Installation personnalisée: ") + if _, err := fmt.Scan(&custom); err == nil { + if discord := ParseDiscord(custom, ""); discord != nil { + return discord + } + } + } + + fmt.Println("Choix non valide") + } +} + +func InstallLatestBuilds() error { + return installLatestBuilds() +} + +func HandleScuffedInstall() { + fmt.Println("Attention!") + fmt.Println("Votre installation Discord est corrompue\nMerci de réinstaller Discord avant de re-essayer!\nSinon, Vencord ne fonctionnera pas correctement!") +} diff --git a/FR/find_discord_windows_FR.go b/FR/find_discord_windows_FR.go new file mode 100644 index 00000000..7f5bb5b --- /dev/null +++ b/FR/find_discord_windows_FR.go @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vencord Installer, a cross platform gui/cli app for installing Vencord + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +package main + +import ( + "errors" + "fmt" + "os" + path "path/filepath" + "strings" +) + +var windowsNames = map[string]string{ + "stable": "Discord", + "ptb": "DiscordPTB", + "canary": "DiscordCanary", + "dev": "DiscordDevelopment", +} + +func ParseDiscord(p, branch string) *DiscordInstall { + entries, err := os.ReadDir(p) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + fmt.Println("Erreur lors de la lecture du dossier "+p+":", err) + } + return nil + } + + isPatched := false + var versions []string + for _, dir := range entries { + if dir.IsDir() && strings.HasPrefix(dir.Name(), "app-") { + resources := path.Join(p, dir.Name(), "resources") + if !ExistsFile(resources) { + continue + } + app := path.Join(resources, "app") + versions = append(versions, app) + isPatched = isPatched || ExistsFile(app) || IsDirectory(path.Join(resources, "app.asar")) + } + } + + if len(versions) == 0 { + return nil + } + + if branch == "" { + branch = GetBranch(p) + } + + return &DiscordInstall{ + path: p, + branch: branch, + versions: versions, + isPatched: isPatched, + isFlatpak: false, + isSystemElectron: false, + } +} + +func FindDiscords() []any { + var discords []any + + appData := os.Getenv("LOCALAPPDATA") + if appData == "" { + fmt.Println("%LOCALAPPDATA% is empty???????") + return discords + } + + for branch, dirname := range windowsNames { + p := path.Join(appData, dirname) + if discord := ParseDiscord(p, branch); discord != nil { + fmt.Println("Installation trouvée ici ", p) + discords = append(discords, discord) + } + } + return discords +} + +func FixOwnership(_ string) error { + return nil +} + +// https://github.com/Vencord/Installer/issues/9 + +func CheckScuffedInstall() bool { + username := os.Getenv("USERNAME") + programData := os.Getenv("PROGRAMDATA") + for _, discordName := range windowsNames { + if ExistsFile(path.Join(programData, username, discordName)) || ExistsFile(path.Join(programData, username, discordName)) { + HandleScuffedInstall() + return true + } + } + return false +} diff --git a/FR/gui_FR.go b/FR/gui_FR.go new file mode 100644 index 00000000..425ec36 --- /dev/null +++ b/FR/gui_FR.go @@ -0,0 +1,596 @@ +//go:build gui || (!gui && !cli) +/* + * SPDX-License-Identifier: GPL-3.0 + * Vencord Installer, a cross platform gui/cli app for installing Vencord + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +package main + +import ( + "bytes" + _ "embed" + "errors" + "fmt" + g "github.com/AllenDang/giu" + "github.com/AllenDang/imgui-go" + "image" + "image/color" + // png decoder for icon + _ "image/png" + "os" + path "path/filepath" + "runtime" + "strconv" + "strings" +) + +var ( + discords []any + radioIdx int + customChoiceIdx int + + customDir string + autoCompleteDir string + autoCompleteFile string + autoCompleteCandidates []string + autoCompleteIdx int + lastAutoComplete string + didAutoComplete bool + + modalId = 0 + modalTitle = "Oh No :(" + modalMessage = "You should never see this" + + acceptedOpenAsar bool + + win *g.MasterWindow +) + +//go:embed winres/icon.png +var iconBytes []byte + +func main() { + InitGithubDownloader() + discords = FindDiscords() + + customChoiceIdx = len(discords) + + go func() { + <-GithubDoneChan + g.Update() + }() + + go func() { + CheckSelfUpdate() + g.Update() + }() + + win = g.NewMasterWindow("Installateur Vencord", 1200, 800, 0) + + icon, _, err := image.Decode(bytes.NewReader(iconBytes)) + if err != nil { + fmt.Println("Erreur lors du chargement de l'icone", err) + fmt.Println(iconBytes, len(iconBytes)) + } else { + win.SetIcon([]image.Image{icon}) + } + win.Run(loop) +} + +type CondWidget struct { + predicate bool + ifWidget func() g.Widget + elseWidget func() g.Widget +} + +func (w *CondWidget) Build() { + if w.predicate { + w.ifWidget().Build() + } else if w.elseWidget != nil { + w.elseWidget().Build() + } +} + +func getChosenInstall() *DiscordInstall { + var choice *DiscordInstall + if radioIdx == customChoiceIdx { + choice = ParseDiscord(customDir, "") + if choice == nil { + ShowModal("Alors...", "Cela ne ressemble pas a une installation Discord...\nMerci de vérifier que vous avez selectionné le dossier de base, par exemple\n(blah/Discord, et pas blah/Discord/resources/app)") + } + } else { + choice = discords[radioIdx].(*DiscordInstall) + } + return choice +} + +func InstallLatestBuilds() (err error) { + if IsDevInstall { + return + } + + err = installLatestBuilds() + if err != nil { + ShowModal("Aie! ", "Erreur lors de l'installation des derniers builds de Vencord depuis Github :\n"+err.Error()) + } + return +} + +func handlePatch() { + choice := getChosenInstall() + if choice != nil { + choice.Patch() + } +} + +func handleUnpatch() { + choice := getChosenInstall() + if choice != nil { + choice.Unpatch() + } +} + +func handleOpenAsar() { + if acceptedOpenAsar || getChosenInstall().IsOpenAsar() { + handleOpenAsarConfirmed() + return + } + + g.OpenPopup("#openasar-confirm") +} + +func handleOpenAsarConfirmed() { + choice := getChosenInstall() + if choice != nil { + if choice.IsOpenAsar() { + if err := choice.UninstallOpenAsar(); err != nil { + handleErr(err, "désinstaller OpenAsar de") + } else { + g.OpenPopup("#openasar-unpatched") + g.Update() + } + } else { + if err := choice.InstallOpenAsar(); err != nil { + handleErr(err, "installer OpenAsar dans") + } else { + g.OpenPopup("#openasar-patched") + g.Update() + } + } + } +} + +func handleErr(err error, action string) { + if errors.Is(err, os.ErrPermission) { + switch os := runtime.GOOS; os { + case "windows": + err = errors.New("Permission refusée. Essayez de fermer Discord depuis le gestionnaire des tâches et de réessayer.") + case "darwin": + err = errors.New("Permission refusée. Merci de donner les permissions nécessaires à l'application, en autorisant l'accès complet au disque dans les paramètres de votre système (page sécuritée).") + default: + err = errors.New("Permission refusée. Essayez de lancer l'application en tant qu'administrateur.") + } + + ShowModal("Erreur lors de "+action+" de cette installation", err.Error()) +} + +func HandleScuffedInstall() { + g.OpenPopup("#scuffed-install") +} + +func (di *DiscordInstall) Patch() { + if CheckScuffedInstall() { + return + } + if err := di.patch(); err != nil { + handleErr(err, "patch") + } else { + g.OpenPopup("#patched") + } +} + +func (di *DiscordInstall) Unpatch() { + if err := di.unpatch(); err != nil { + handleErr(err, "unpatch") + } else { + g.OpenPopup("#unpatched") + } +} + +func onCustomInputChanged() { + p := customDir + if len(p) != 0 { + // Select the custom option for people + radioIdx = customChoiceIdx + } + + dir := path.Dir(p) + + isNewDir := strings.HasSuffix(p, "/") + wentUpADir := !isNewDir && dir != autoCompleteDir + + if isNewDir || wentUpADir { + autoCompleteDir = dir + // reset all the funnies + autoCompleteIdx = 0 + lastAutoComplete = "" + autoCompleteFile = "" + autoCompleteCandidates = nil + + // Generate autocomplete items + files, err := os.ReadDir(dir) + if err == nil { + for _, file := range files { + autoCompleteCandidates = append(autoCompleteCandidates, file.Name()) + } + } + } else if !didAutoComplete { + // reset auto complete and update our file + autoCompleteFile = path.Base(p) + lastAutoComplete = "" + } + + if wentUpADir { + autoCompleteFile = path.Base(p) + } + + didAutoComplete = false +} + +// go can you give me []any? +// to pass to giu RangeBuilder? +// yeeeeees +// actually returns []string like a boss +func makeAutoComplete() []any { + input := strings.ToLower(autoCompleteFile) + + var candidates []any + for _, e := range autoCompleteCandidates { + file := strings.ToLower(e) + if autoCompleteFile == "" || strings.HasPrefix(file, input) { + candidates = append(candidates, e) + } + } + return candidates +} + +func makeRadioOnChange(i int) func() { + return func() { + radioIdx = i + } +} + +func renderFilesDirErr() g.Widget { + return g.Layout{ + g.Dummy(0, 50), + g.Style(). + SetColor(g.StyleColorText, DiscordRed). + SetFontSize(30). + To( + g.Align(g.AlignCenter).To( + g.Label("Erreur lors de la création de : "+FilesDirErr.Error()), + g.Label("Merci de régler le problème et de réessayer."), + ), + ), + } +} + +func Tooltip(label string) g.Widget { + return g.Style(). + SetStyle(g.StyleVarWindowPadding, 10, 8). + SetStyleFloat(g.StyleVarWindowRounding, 8). + To( + g.Tooltip(label), + ) +} + +func InfoModal(id, title, description string) g.Widget { + return RawInfoModal(id, title, description, false) +} + +func RawInfoModal(id, title, description string, isOpenAsar bool) g.Widget { + isDynamic := strings.HasPrefix(id, "#modal") + return g.Style(). + SetStyle(g.StyleVarWindowPadding, 30, 30). + SetStyleFloat(g.StyleVarWindowRounding, 12). + To( + g.PopupModal(id). + Flags(g.WindowFlagsNoTitleBar | Ternary(isDynamic, g.WindowFlagsAlwaysAutoResize, 0)). + Layout( + g.Align(g.AlignCenter).To( + g.Style().SetFontSize(30).To( + g.Label(title), + ), + g.Style().SetFontSize(20).To( + g.Label(description).Wrapped(isDynamic), + ), + &CondWidget{id == "#scuffed-install", func() g.Widget { + return g.Column( + g.Dummy(0, 10), + g.Button("Take me there!").OnClick(func() { + // this issue only exists on windows so using Windows specific path is oki + username := os.Getenv("USERNAME") + programData := os.Getenv("PROGRAMDATA") + g.OpenURL("file://" + path.Join(programData, username)) + }).Size(200, 30), + ) + }, nil}, + g.Dummy(0, 20), + &CondWidget{isOpenAsar, + func() g.Widget { + return g.Row( + g.Button("Accepter"). + OnClick(func() { + acceptedOpenAsar = true + g.CloseCurrentPopup() + }). + Size(100, 30), + g.Button("Annuler"). + OnClick(func() { + g.CloseCurrentPopup() + }). + Size(100, 30), + ) + }, + func() g.Widget { + return g.Button("Ok"). + OnClick(func() { + g.CloseCurrentPopup() + }). + Size(100, 30) + }, + }, + ), + ), + ) +} + +func ShowModal(title, desc string) { + modalTitle = title + modalMessage = desc + modalId++ + g.OpenPopup("#modal" + strconv.Itoa(modalId)) +} + +func renderInstaller() g.Widget { + candidates := makeAutoComplete() + wi, _ := win.GetSize() + w := float32(wi) - 96 + + var currentDiscord *DiscordInstall + if radioIdx != customChoiceIdx { + currentDiscord = discords[radioIdx].(*DiscordInstall) + } + var isOpenAsar = currentDiscord != nil && currentDiscord.IsOpenAsar() + + layout := g.Layout{ + g.Dummy(0, 20), + g.Separator(), + g.Dummy(0, 5), + + g.Style().SetFontSize(30).To( + g.Label("Merci de sélectionner votre installation de Discord"), + ), + + g.Style().SetFontSize(20).To( + g.RangeBuilder("Discords", discords, func(i int, v any) g.Widget { + d := v.(*DiscordInstall) + text := d.path + " (" + d.branch + ")" + if d.isPatched { + text = "[PATCHÉE] " + text + } + return g.RadioButton(text, radioIdx == i). + OnChange(makeRadioOnChange(i)) + }), + + g.RadioButton("Emplacement personnalisé", radioIdx == customChoiceIdx). + OnChange(makeRadioOnChange(customChoiceIdx)), + ), + + g.Dummy(0, 5), + g.Style(). + SetStyle(g.StyleVarFramePadding, 16, 16). + SetFontSize(20). + To( + g.InputText(&customDir).Hint("L'emplacement personnalisé"). + Size(w - 16). + Flags(g.InputTextFlagsCallbackCompletion). + OnChange(onCustomInputChanged). + // this library has its own autocomplete but it's broken + Callback( + func(data imgui.InputTextCallbackData) int32 { + if len(candidates) == 0 { + return 0 + } + // just wrap around + if autoCompleteIdx >= len(candidates) { + autoCompleteIdx = 0 + } + + // used by change handler + didAutoComplete = true + + start := len(customDir) + // Delete previous auto complete + if lastAutoComplete != "" { + start -= len(lastAutoComplete) + data.DeleteBytes(start, len(lastAutoComplete)) + } else if autoCompleteFile != "" { // delete partial input + start -= len(autoCompleteFile) + data.DeleteBytes(start, len(autoCompleteFile)) + } + + // Insert auto complete + lastAutoComplete = candidates[autoCompleteIdx].(string) + data.InsertBytes(start, []byte(lastAutoComplete)) + autoCompleteIdx++ + + return 0 + }, + ), + ), + g.RangeBuilder("AutoComplete", candidates, func(i int, v any) g.Widget { + dir := v.(string) + return g.Label(dir) + }), + + g.Dummy(0, 20), + + g.Style().SetFontSize(20).To( + g.Row( + g.Style(). + SetColor(g.StyleColorButton, DiscordGreen). + SetDisabled(GithubError != nil). + To( + g.Button("Installer"). + OnClick(handlePatch). + Size((w-40)/4, 50), + Tooltip("Patcher l'installation de Discord sélectionnée"), + ), + g.Style(). + SetColor(g.StyleColorButton, Ternary(isOpenAsar, DiscordRed, DiscordGreen)). + To( + g.Button(Ternary(isOpenAsar, "Désinstaller OpenAsar", Ternary(currentDiscord != nil, "Installer OpenAsar", "(Dé-)Install OpenAsar"))). + OnClick(handleOpenAsar). + Size((w-40)/4, 50), + Tooltip("Modifier OpenAsar"), + ), + g.Style(). + SetColor(g.StyleColorButton, DiscordRed). + To( + g.Button("Désinstaller"). + OnClick(handleUnpatch). + Size((w-40)/4, 50), + Tooltip("Désinstaller Vencord de l'installation de Discord sélectionnée"), + ), + g.Style(). + SetColor(g.StyleColorButton, DiscordBlue). + SetDisabled(IsDevInstall || GithubError != nil). + To( + g.Button(Ternary(GithubError == nil && LatestHash == InstalledHash, "Re-Télécharger Vencord", "Mise à jour")). + OnClick(func() { + if err := InstallLatestBuilds(); err == nil { + g.OpenPopup("téléchargé") + } + }). + Size((w-40)/4, 50), + Tooltip("Mettre à jour Vencord"), + ), + ), + ), + + InfoModal("#downloaded", "Téléchargement réussi", "Vencord a été téléchargé avec succès !"), + InfoModal("#patched", "Patch réussi ! ", "Merci de quitter Discord depuis la barre des tâches\n"+ + "Ensuite, relancez-le pour voir les changements, en allant dans Paramètres depuis les paramètres Discord, et en cherchant Vencord"), + InfoModal("#unpatched", "Dé-Patch réussi !", "Merci de quitter Discord depuis la barre des tâches\n"+ + InfoModal("#scuffed-install", "Aie ! ", "Votre installation Discord est problématique...\n"+ + "Parfois Discord décide de s'installer dans le mauvais dossier sans raison...\n"+ + "Avant de poursuivre, vous devez régler ce problème, sans quoi Vencord ne marchera certainement pas...\n\n"+ + "Cliquez sur le bouton ci dessous pour vous y rendre, et supprimez le dossier Discord et/ou Squirrel.\n"+ + "Si le dossier est maintenant vide, supprimez complétement le dossier.\n"+ + "Vérifiez que Discord ne s'éxécute pas, et réinstallez-le".), + RawInfoModal("#openasar-confirm", "OpenAsar", "OpenAsar est une alternative open-source au Luncher Discord app.asar.\n"+ + "Vencord est en aucun cas affilié avec OpenAsar.\n"+ + "Vous installer OpenAsar à vos propres risques \n"+ + "Si vous rencontrez des problèmes avec OpenAsar, rendez vous sur leur serveur pour demandez de l'aide!\n\n"+ + "Pour installer OpenAsar, cliquez sur Acceptez, puis sur 'Install OpenAsar' again.", true), + InfoModal("#openasar-patched", "OpenAsar installé ! ", "Redémarrez Discord depuis le gestionnaire des tâches pour voir les changements"), + InfoModal("#openasar-unpatched", "OpenAsar désinstallé ! ", "Redémarrez Discord depuis le gestionnaire des tâches"), + InfoModal("#modal"+strconv.Itoa(modalId), modalTitle, modalMessage), + } + + return layout +} + +func renderErrorCard(col color.Color, message string) g.Widget { + return g.Style(). + SetColor(g.StyleColorChildBg, col). + SetStyleFloat(g.StyleVarAlpha, 0.9). + SetStyle(g.StyleVarWindowPadding, 10, 10). + SetStyleFloat(g.StyleVarChildRounding, 5). + To( + g.Child(). + Size(g.Auto, 40). + Layout( + g.Row( + g.Style().SetColor(g.StyleColorText, color.Black).To( + g.Markdown(&message), + ), + ), + ), + ) +} + +func loop() { + g.PushWindowPadding(48, 48) + + g.SingleWindow(). + RegisterKeyboardShortcuts( + g.WindowShortcut{Key: g.KeyUp, Callback: func() { + if radioIdx > 0 { + radioIdx-- + } + }}, + g.WindowShortcut{Key: g.KeyDown, Callback: func() { + if radioIdx < customChoiceIdx { + radioIdx++ + } + }}, + ). + Layout( + g.Align(g.AlignCenter).To( + g.Style().SetFontSize(40).To( + g.Label("Installateur Vencord"), + ), + ), + + g.Dummy(0, 20), + + g.Style().SetFontSize(20).To( + g.Row( + g.Label(Ternary(IsDevInstall, "Dev Install: ", "Les fichiers vont être installés ici: ")+FilesDir), + g.Style(). + SetColor(g.StyleColorButton, DiscordBlue). + SetStyle(g.StyleVarFramePadding, 4, 4). + To( + g.Button("Ouvrir le dossier").OnClick(func() { + g.OpenURL("file://" + FilesDir) + }), + ), + ), + &CondWidget{!IsDevInstall, func() g.Widget { + return g.Label("Pour changer l'emplacement, modifiez la variable d'environnement 'VENCORD_USER_DATA_DIR' et redémarrez l'installateur").Wrapped(true) + }, nil}, + g.Dummy(0, 10), + g.Label("Emplacement de l'installation: "+InstallerTag+" ("+InstallerGitHash+")"+Ternary(IsInstallerOutdated, " - PLUS À JOUR", "")), + g.Label("Version Locale de Vencord "+InstalledHash), + &CondWidget{ + GithubError == nil, + func() g.Widget { + if IsDevInstall { + return g.Label("Pas de mise à jour pour les installations de développement") + } + return g.Label("Dernière Version de Vencord " + LatestHash) + }, func() g.Widget { + return renderErrorCard(DiscordRed, "Erreur lors de la récupération des donnéees depuis Github "+GithubError.Error()) + }, + }, + &CondWidget{ + IsInstallerOutdated, + func() g.Widget { + return renderErrorCard(DiscordYellow, "Cet installateur n'est pas à jour! "+GetInstallerDownloadMarkdown()) + }, + nil, + }, + ), + + &CondWidget{ + predicate: FilesDirErr != nil, + ifWidget: renderFilesDirErr, + elseWidget: renderInstaller, + }, + ) + + g.PopStyle() +} diff --git a/FR/patcher_FR.go b/FR/patcher_FR.go new file mode 100644 index 00000000..a1625a4 --- /dev/null +++ b/FR/patcher_FR.go @@ -0,0 +1,308 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vencord Installer, a cross platform gui/cli app for installing Vencord + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/ProtonMail/go-appdir" + "os" + "os/exec" + path "path/filepath" + "strings" +) + +var BaseDir string +var FilesDir string +var FilesDirErr error +var Patcher string + +var PackageJson = []byte(`{ + "name": "discord", + "main": "index.js" +} +`) + +func init() { + if dir := os.Getenv("VENCORD_USER_DATA_DIR"); dir != "" { + fmt.Println("Using VENCORD_USER_DATA_DIR") + BaseDir = dir + } else if dir = os.Getenv("DISCORD_USER_DATA_DIR"); dir != "" { + fmt.Println("Using DISCORD_USER_DATA_DIR/../VencordData") + BaseDir = path.Join(dir, "..", "VencordData") + } else { + fmt.Println("Using UserConfig") + BaseDir = appdir.New("Vencord").UserConfig() + } + FilesDir = path.Join(BaseDir, "dist") + if !ExistsFile(FilesDir) { + FilesDirErr = os.MkdirAll(FilesDir, 0755) + if FilesDirErr != nil { + fmt.Println("Impossible de créér", FilesDir, FilesDirErr) + } else { + FilesDirErr = FixOwnership(BaseDir) + } + } + Patcher = path.Join(FilesDir, "patcher.js") +} + +type DiscordInstall struct { + path string // the base path + branch string // canary / stable / ... + versions []string // List of paths to folders to patch, 1 on linux/mac, might be more on Windows + isPatched bool + isFlatpak bool + isSystemElectron bool // Needs special care https://aur.archlinux.org/packages/discord_arch_electron + isOpenAsar *bool +} + +// IsSafeToDelete returns nil if path is safe to delete. +// In other cases, the returned error should give more info +func IsSafeToDelete(path string) error { + files, err := os.ReadDir(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = nil + } + return err + } + for _, file := range files { + name := file.Name() + if name != "package.json" && name != "index.js" { + return errors.New("Ce fichier: '" + name + "' n'appartient pas à Vencord") + } + } + return nil +} + +func writeFiles(dir string) error { + if err := os.RemoveAll(dir); err != nil { + return err + } + + if err := os.Mkdir(dir, 0755); err != nil { + return err + } + + if err := os.WriteFile(path.Join(dir, "package.json"), PackageJson, 0644); err != nil { + return err + } + + patcherPath, _ := json.Marshal(Patcher) + return os.WriteFile(path.Join(dir, "index.js"), []byte("require("+string(patcherPath)+")"), 0644) +} + +func patchRenames(dir string, isSystemElectron bool) (err error) { + appAsar := path.Join(dir, "app.asar") + _appAsar := path.Join(dir, "_app.asar") + + var renamesDone [][]string + defer func() { + if err != nil && len(renamesDone) > 0 { + fmt.Println("Erreur lors du patch, on annule les changements") + for _, rename := range renamesDone { + if innerErr := os.Rename(rename[1], rename[0]); innerErr != nil { + fmt.Println("Erreur lors de l'annulation des changements. Merci de réinstaller", innerErr) + } else { + fmt.Println("Changements annulées.") + } + } + } + }() + + fmt.Println("On renomme", appAsar, "en", _appAsar) + if err := os.Rename(appAsar, _appAsar); err != nil { + err = CheckIfErrIsCauseItsBusyRn(err) + fmt.Println(err) + return err + } + renamesDone = append(renamesDone, []string{appAsar, _appAsar}) + + if isSystemElectron { + from, to := appAsar+".unpacked", _appAsar+".unpacked" + fmt.Println("On renomme", from, "en", to) + err := os.Rename(from, to) + if err != nil { + return err + } + renamesDone = append(renamesDone, []string{from, to}) + } + + fmt.Println("On écrit ici:", appAsar) + if err := writeFiles(appAsar); err != nil { + return err + } + + return nil +} + +func (di *DiscordInstall) patch() error { + fmt.Println("On Patch " + di.path + "...") + if LatestHash != InstalledHash { + if err := InstallLatestBuilds(); err != nil { + return nil // already shown dialog so don't return same error again + } + } + + if di.isPatched { + fmt.Println(di.path, "est déjà patché, on le dépatch") + if err := di.unpatch(); err != nil { + if errors.Is(err, os.ErrPermission) { + return err + } + return errors.New("erreur lors du dépatch dans ce dossier '" + di.path + "':\n" + err.Error()) + } + } + + if di.isSystemElectron { + if err := patchRenames(di.path, true); err != nil { + return err + } + } else { + for _, version := range di.versions { + if err := patchRenames(path.Join(version, ".."), false); err != nil { + return err + } + } + } + fmt.Println("Patché avec succès", di.path) + di.isPatched = true + + if di.isFlatpak { + pathElements := strings.Split(di.path, "/") + var name string + for _, e := range pathElements { + if strings.HasPrefix(e, "com.discordapp") { + name = e + break + } + } + + fmt.Println("C'est un flatpak. On essaye de donner l'accès à Flatpak à", FilesDir+"...") + + isSystemFlatpak := strings.HasPrefix(di.path, "/var") + var args []string + if !isSystemFlatpak { + args = append(args, "--user") + } + args = append(args, "override", name, "--filesystem="+FilesDir) + fullCmd := "flatpak " + strings.Join(args, " ") + + fmt.Println("Running", fullCmd) + + var err error + if !isSystemFlatpak && os.Getuid() == 0 { + // We are operating on a user flatpak but are root + actualUser := os.Getenv("SUDO_USER") + fmt.Println("Ceci est une install personnelle, mais nous somme le root du système. Essayez de lancer la commande su pour lancer le programme en temps que", actualUser) + cmd := exec.Command("su", "-", actualUser, "-c", "sh", "-c", fullCmd) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + } else { + cmd := exec.Command("flatpak", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + } + if err != nil { + return errors.New("Impossible de donner à DIscord Flatpak les permissions pour ce dossier: " + FilesDir + ": " + err.Error()) + } + } + return nil +} + +func unpatchRenames(dir string, isSystemElectron bool) (errOut error) { + appAsar := path.Join(dir, "app.asar") + appAsarTmp := path.Join(dir, "app.asar.tmp") + _appAsar := path.Join(dir, "_app.asar") + + var renamesDone [][]string + defer func() { + if errOut != nil && len(renamesDone) > 0 { + fmt.Println("Erreur lors du dépatch, on annule les changements") + for _, rename := range renamesDone { + if innerErr := os.Rename(rename[1], rename[0]); innerErr != nil { + fmt.Println("Erreur lors de l'annulation des changements. Merci de réinstaller", innerErr) + } else { + fmt.Println("Changements annulées.") + } + } + } else if errOut == nil { + if innerErr := os.RemoveAll(appAsarTmp); innerErr != nil { + fmt.Println("Erreur lors de la supprésion de app.asar backup. Ce n'est pas obligé mais pouvez le supprimer manuellement", innerErr) + } + } + }() + + fmt.Println("On supprime", appAsar) + if err := os.Rename(appAsar, appAsarTmp); err != nil { + err = CheckIfErrIsCauseItsBusyRn(err) + fmt.Println(err) + errOut = err + } else { + renamesDone = append(renamesDone, []string{appAsar, appAsarTmp}) + } + + fmt.Println("On renomme", _appAsar, "en", appAsar) + if err := os.Rename(_appAsar, appAsar); err != nil { + err = CheckIfErrIsCauseItsBusyRn(err) + fmt.Println(err) + errOut = err + } else { + renamesDone = append(renamesDone, []string{_appAsar, appAsar}) + } + + if isSystemElectron { + fmt.Println("On renomme", _appAsar+".unpacked", "en", appAsar+".unpacked") + if err := os.Rename(_appAsar+".unpacked", appAsar+".unpacked"); err != nil { + fmt.Println(err) + errOut = err + } + } + return +} + +func (di *DiscordInstall) unpatch() error { + fmt.Println("On dépatch " + di.path + "...") + + if di.isSystemElectron { + fmt.Println("Installation Electron système, on dépatche les renames") + // See comment in Patch + if err := unpatchRenames(di.path, true); err != nil { + return err + } + } else { + for _, version := range di.versions { + isCanaryHack := IsDirectory(path.Join(version, "..", "app.asar")) + if isCanaryHack { + if err := unpatchRenames(path.Join(version, ".."), false); err != nil { + return err + } + } else { + err := IsSafeToDelete(version) + if errors.Is(err, os.ErrPermission) { + fmt.Println("Permission de lire", version, "refusée") + return err + } + fmt.Println("On vérifie si ", version, "est safe à supprimer", Ternary(err == nil, "Oui", "Non")) + if err != nil { + return errors.New("Supprimer le dossier du patch version : '" + version + "' est potentiellement dangereux, merci de le faire manuellement " + err.Error()) + } + fmt.Println("On supprime", version) + err = os.RemoveAll(version) + if err != nil { + return err + } + } + } + } + fmt.Println("Le Patch a été supprimé avec succès!", di.path) + di.isPatched = false + return nil +} diff --git a/FR/self_updater_FR.go b/FR/self_updater_FR.go new file mode 100644 index 00000000..910ffe6 --- /dev/null +++ b/FR/self_updater_FR.go @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vencord Installer, a cross platform gui/cli app for installing Vencord + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +package main + +import ( + "fmt" + "runtime" +) + +var IsInstallerOutdated = false + +func CheckSelfUpdate() { + fmt.Println("On cherche des mises à jour pour l'Installateur...") + + res, err := GetGithubRelease(InstallerReleaseUrl) + if err == nil { + IsInstallerOutdated = res.TagName != InstallerTag + } +} + +func GetInstallerDownloadLink() string { + switch runtime.GOOS { + case "windows": + return "https://github.com/Vencord/Installer/releases/latest/download/VencordInstaller.exe" + case "darwin": + return "https://github.com/Vencord/Installer/releases/latest/download/VencordInstaller.MacOS.zip" + default: + return "" + } +} + +func GetInstallerDownloadMarkdown() string { + link := GetInstallerDownloadLink() + if link == "" { + return "" + } + return " [Télécharger la dernière version de l'installeur](" + link + ")" +} diff --git a/FR/util_FR.go b/FR/util_FR.go new file mode 100644 index 00000000..2c19511 --- /dev/null +++ b/FR/util_FR.go @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vencord Installer, a cross platform gui/cli app for installing Vencord + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +package main + +import ( + "errors" + "fmt" + "os" + "runtime" + "strings" + "syscall" +) + +func ArrayIncludes[T comparable](arr []T, v T) bool { + for _, e := range arr { + if e == v { + return true + } + } + return false +} + +func ExistsFile(path string) bool { + _, err := os.Stat(path) + fmt.Println("On vérifie si ", path, "existe:", Ternary(err == nil, "Oui", "Non")) + return err == nil +} + +func IsDirectory(path string) bool { + s, err := os.Stat(path) + if err != nil { + fmt.Println("Erreur lors de la vérification du dossier", path, "est un dossier:", err) + return false + } + fmt.Println("On vérifie si ", path, "est un dossier", Ternary(s.IsDir(), "Oui", "Non")) + return s.IsDir() +} + +func Ternary[T any](b bool, ifTrue, ifFalse T) T { + if b { + return ifTrue + } + return ifFalse +} + +var branches = []string{"canary", "development", "ptb"} + +func GetBranch(name string) string { + name = strings.ToLower(name) + for _, branch := range branches { + if strings.HasSuffix(name, branch) { + return branch + } + } + return "stable" +} + +func Ptr[T any](v T) *T { + return &v +} + +func CheckIfErrIsCauseItsBusyRn(err error) error { + if runtime.GOOS != "windows" { + return err + } + + // bruhhhh + if linkError, ok := err.(*os.LinkError); ok { + if errno, ok := linkError.Err.(syscall.Errno); ok && errno == 32 /* ERROR_SHARING_VIOLATION */ { + return errors.New( + "Impossible de patcher Discord car il est ouvert!" + + "\nMerci de fermer Discord depuis le gestionnaire des tâches et réessayer", + ) + } + } + + return err +} diff --git a/gui.go b/gui.go index e7a281b..f2b42b3 100644 --- a/gui.go +++ b/gui.go @@ -5,593 +5,594 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ -package main - -import ( - "bytes" - _ "embed" - "errors" - "fmt" - g "github.com/AllenDang/giu" - "github.com/AllenDang/imgui-go" - "image" - "image/color" - // png decoder for icon - _ "image/png" - "os" - path "path/filepath" - "runtime" - "strconv" - "strings" -) - -var ( - discords []any - radioIdx int - customChoiceIdx int - - customDir string - autoCompleteDir string - autoCompleteFile string - autoCompleteCandidates []string - autoCompleteIdx int - lastAutoComplete string - didAutoComplete bool - - modalId = 0 - modalTitle = "Oh No :(" - modalMessage = "You should never see this" - - acceptedOpenAsar bool - - win *g.MasterWindow -) - -//go:embed winres/icon.png -var iconBytes []byte - -func main() { - InitGithubDownloader() - discords = FindDiscords() - - customChoiceIdx = len(discords) - - go func() { - <-GithubDoneChan - g.Update() - }() - - go func() { - CheckSelfUpdate() - g.Update() - }() - - win = g.NewMasterWindow("Vencord Installer", 1200, 800, 0) - - icon, _, err := image.Decode(bytes.NewReader(iconBytes)) - if err != nil { - fmt.Println("Failed to load application icon", err) - fmt.Println(iconBytes, len(iconBytes)) - } else { - win.SetIcon([]image.Image{icon}) - } - win.Run(loop) -} - -type CondWidget struct { - predicate bool - ifWidget func() g.Widget - elseWidget func() g.Widget -} - -func (w *CondWidget) Build() { - if w.predicate { - w.ifWidget().Build() - } else if w.elseWidget != nil { - w.elseWidget().Build() - } -} - -func getChosenInstall() *DiscordInstall { - var choice *DiscordInstall - if radioIdx == customChoiceIdx { - choice = ParseDiscord(customDir, "") - if choice == nil { - ShowModal("Hey now...", "That doesn't seem to be a Discord install.\nPlease make sure you select the base folder\n(blah/Discord, not blah/Discord/resources/app)") - } - } else { - choice = discords[radioIdx].(*DiscordInstall) - } - return choice -} - -func InstallLatestBuilds() (err error) { - if IsDevInstall { - return - } - - err = installLatestBuilds() - if err != nil { - ShowModal("Uh Oh!", "Failed to install the latest Vencord builds from GitHub:\n"+err.Error()) - } - return -} - -func handlePatch() { - choice := getChosenInstall() - if choice != nil { - choice.Patch() - } -} - -func handleUnpatch() { - choice := getChosenInstall() - if choice != nil { - choice.Unpatch() - } -} - -func handleOpenAsar() { - if acceptedOpenAsar || getChosenInstall().IsOpenAsar() { - handleOpenAsarConfirmed() - return - } - - g.OpenPopup("#openasar-confirm") -} - -func handleOpenAsarConfirmed() { - choice := getChosenInstall() - if choice != nil { - if choice.IsOpenAsar() { - if err := choice.UninstallOpenAsar(); err != nil { - handleErr(err, "uninstall OpenAsar from") - } else { - g.OpenPopup("#openasar-unpatched") - g.Update() - } - } else { - if err := choice.InstallOpenAsar(); err != nil { - handleErr(err, "install OpenAsar on") - } else { - g.OpenPopup("#openasar-patched") - g.Update() - } - } - } -} - -func handleErr(err error, action string) { - if errors.Is(err, os.ErrPermission) { - switch os := runtime.GOOS; os { - case "windows": - err = errors.New("Permission denied. Make sure your Discord is fully closed (from the tray)!") - case "darwin": - err = errors.New("Permission denied. Please grant the installer Full Disk Access in the system settings (privacy & security page).") - default: - err = errors.New("Permission denied. Maybe try running me as Administrator/Root?") - } - } - - ShowModal("Failed to "+action+" this Install", err.Error()) -} - -func HandleScuffedInstall() { - g.OpenPopup("#scuffed-install") -} - -func (di *DiscordInstall) Patch() { - if CheckScuffedInstall() { - return - } - if err := di.patch(); err != nil { - handleErr(err, "patch") - } else { - g.OpenPopup("#patched") - } -} - -func (di *DiscordInstall) Unpatch() { - if err := di.unpatch(); err != nil { - handleErr(err, "unpatch") - } else { - g.OpenPopup("#unpatched") - } -} - -func onCustomInputChanged() { - p := customDir - if len(p) != 0 { - // Select the custom option for people - radioIdx = customChoiceIdx - } - - dir := path.Dir(p) - - isNewDir := strings.HasSuffix(p, "/") - wentUpADir := !isNewDir && dir != autoCompleteDir - - if isNewDir || wentUpADir { - autoCompleteDir = dir - // reset all the funnies - autoCompleteIdx = 0 - lastAutoComplete = "" - autoCompleteFile = "" - autoCompleteCandidates = nil - - // Generate autocomplete items - files, err := os.ReadDir(dir) - if err == nil { - for _, file := range files { - autoCompleteCandidates = append(autoCompleteCandidates, file.Name()) - } - } - } else if !didAutoComplete { - // reset auto complete and update our file - autoCompleteFile = path.Base(p) - lastAutoComplete = "" - } - - if wentUpADir { - autoCompleteFile = path.Base(p) - } - - didAutoComplete = false -} - -// go can you give me []any? -// to pass to giu RangeBuilder? -// yeeeeees -// actually returns []string like a boss -func makeAutoComplete() []any { - input := strings.ToLower(autoCompleteFile) - - var candidates []any - for _, e := range autoCompleteCandidates { - file := strings.ToLower(e) - if autoCompleteFile == "" || strings.HasPrefix(file, input) { - candidates = append(candidates, e) - } - } - return candidates -} - -func makeRadioOnChange(i int) func() { - return func() { - radioIdx = i - } -} - -func renderFilesDirErr() g.Widget { - return g.Layout{ - g.Dummy(0, 50), - g.Style(). - SetColor(g.StyleColorText, DiscordRed). - SetFontSize(30). - To( - g.Align(g.AlignCenter).To( - g.Label("Error: Failed to create: "+FilesDirErr.Error()), - g.Label("Resolve this error, then restart me!"), - ), - ), - } -} - -func Tooltip(label string) g.Widget { - return g.Style(). - SetStyle(g.StyleVarWindowPadding, 10, 8). - SetStyleFloat(g.StyleVarWindowRounding, 8). - To( - g.Tooltip(label), - ) -} - -func InfoModal(id, title, description string) g.Widget { - return RawInfoModal(id, title, description, false) -} - -func RawInfoModal(id, title, description string, isOpenAsar bool) g.Widget { - isDynamic := strings.HasPrefix(id, "#modal") - return g.Style(). - SetStyle(g.StyleVarWindowPadding, 30, 30). - SetStyleFloat(g.StyleVarWindowRounding, 12). - To( - g.PopupModal(id). - Flags(g.WindowFlagsNoTitleBar | Ternary(isDynamic, g.WindowFlagsAlwaysAutoResize, 0)). - Layout( - g.Align(g.AlignCenter).To( - g.Style().SetFontSize(30).To( - g.Label(title), - ), - g.Style().SetFontSize(20).To( - g.Label(description).Wrapped(isDynamic), - ), - &CondWidget{id == "#scuffed-install", func() g.Widget { - return g.Column( - g.Dummy(0, 10), - g.Button("Take me there!").OnClick(func() { - // this issue only exists on windows so using Windows specific path is oki - username := os.Getenv("USERNAME") - programData := os.Getenv("PROGRAMDATA") - g.OpenURL("file://" + path.Join(programData, username)) - }).Size(200, 30), - ) - }, nil}, - g.Dummy(0, 20), - &CondWidget{isOpenAsar, - func() g.Widget { - return g.Row( - g.Button("Accept"). - OnClick(func() { - acceptedOpenAsar = true - g.CloseCurrentPopup() - }). - Size(100, 30), - g.Button("Cancel"). - OnClick(func() { - g.CloseCurrentPopup() - }). - Size(100, 30), - ) - }, - func() g.Widget { - return g.Button("Ok"). - OnClick(func() { - g.CloseCurrentPopup() - }). - Size(100, 30) - }, - }, - ), - ), - ) -} - -func ShowModal(title, desc string) { - modalTitle = title - modalMessage = desc - modalId++ - g.OpenPopup("#modal" + strconv.Itoa(modalId)) -} - -func renderInstaller() g.Widget { - candidates := makeAutoComplete() - wi, _ := win.GetSize() - w := float32(wi) - 96 - - var currentDiscord *DiscordInstall - if radioIdx != customChoiceIdx { - currentDiscord = discords[radioIdx].(*DiscordInstall) - } - var isOpenAsar = currentDiscord != nil && currentDiscord.IsOpenAsar() - - layout := g.Layout{ - g.Dummy(0, 20), - g.Separator(), - g.Dummy(0, 5), - - g.Style().SetFontSize(30).To( - g.Label("Please select an install to patch"), - ), - - g.Style().SetFontSize(20).To( - g.RangeBuilder("Discords", discords, func(i int, v any) g.Widget { - d := v.(*DiscordInstall) - text := d.path + " (" + d.branch + ")" - if d.isPatched { - text = "[PATCHED] " + text - } - return g.RadioButton(text, radioIdx == i). - OnChange(makeRadioOnChange(i)) - }), - - g.RadioButton("Custom Install Location", radioIdx == customChoiceIdx). - OnChange(makeRadioOnChange(customChoiceIdx)), - ), - - g.Dummy(0, 5), - g.Style(). - SetStyle(g.StyleVarFramePadding, 16, 16). - SetFontSize(20). - To( - g.InputText(&customDir).Hint("The custom location"). - Size(w - 16). - Flags(g.InputTextFlagsCallbackCompletion). - OnChange(onCustomInputChanged). - // this library has its own autocomplete but it's broken - Callback( - func(data imgui.InputTextCallbackData) int32 { - if len(candidates) == 0 { - return 0 - } - // just wrap around - if autoCompleteIdx >= len(candidates) { - autoCompleteIdx = 0 - } - - // used by change handler - didAutoComplete = true - - start := len(customDir) - // Delete previous auto complete - if lastAutoComplete != "" { - start -= len(lastAutoComplete) - data.DeleteBytes(start, len(lastAutoComplete)) - } else if autoCompleteFile != "" { // delete partial input - start -= len(autoCompleteFile) - data.DeleteBytes(start, len(autoCompleteFile)) - } - - // Insert auto complete - lastAutoComplete = candidates[autoCompleteIdx].(string) - data.InsertBytes(start, []byte(lastAutoComplete)) - autoCompleteIdx++ - - return 0 - }, - ), - ), - g.RangeBuilder("AutoComplete", candidates, func(i int, v any) g.Widget { - dir := v.(string) - return g.Label(dir) - }), - - g.Dummy(0, 20), - - g.Style().SetFontSize(20).To( - g.Row( - g.Style(). - SetColor(g.StyleColorButton, DiscordGreen). - SetDisabled(GithubError != nil). - To( - g.Button("Install"). - OnClick(handlePatch). - Size((w-40)/4, 50), - Tooltip("Patch the selected Discord Install"), - ), - g.Style(). - SetColor(g.StyleColorButton, Ternary(isOpenAsar, DiscordRed, DiscordGreen)). - To( - g.Button(Ternary(isOpenAsar, "Uninstall OpenAsar", Ternary(currentDiscord != nil, "Install OpenAsar", "(Un-)Install OpenAsar"))). - OnClick(handleOpenAsar). - Size((w-40)/4, 50), - Tooltip("Manage OpenAsar"), - ), - g.Style(). - SetColor(g.StyleColorButton, DiscordRed). - To( - g.Button("Uninstall"). - OnClick(handleUnpatch). - Size((w-40)/4, 50), - Tooltip("Unpatch the selected Discord Install"), - ), - g.Style(). - SetColor(g.StyleColorButton, DiscordBlue). - SetDisabled(IsDevInstall || GithubError != nil). - To( - g.Button(Ternary(GithubError == nil && LatestHash == InstalledHash, "Re-Download Vencord", "Update")). - OnClick(func() { - if err := InstallLatestBuilds(); err == nil { - g.OpenPopup("#downloaded") - } - }). - Size((w-40)/4, 50), - Tooltip("Update your local Vencord files"), - ), - ), - ), - - InfoModal("#downloaded", "Successfully Downloaded", "The Vencord files were successfully downloaded!"), - InfoModal("#patched", "Successfully Patched", "You must now fully close Discord (from the tray).\n"+ - "Then, verify Vencord installed successfully by looking for its category in Discord Settings"), - InfoModal("#unpatched", "Successfully Unpatched", "You must now fully close Discord (from the tray)"), - InfoModal("#scuffed-install", "Hold On!", "You have a broken Discord Install.\n"+ - "Sometimes Discord decides to install to the wrong location for some reason!\n"+ - "You need to fix this before patching, otherwise Vencord will likely not work.\n\n"+ - "Use the below button to jump there and delete any folder called Discord or Squirrel.\n"+ - "If the folder is now empty, feel free to go back a step and delete that folder too.\n"+ - "Then see if Discord still starts. If not, reinstall it"), - RawInfoModal("#openasar-confirm", "OpenAsar", "OpenAsar is an open-source alternative of Discord desktop's app.asar.\n"+ - "Vencord is in no way affiliated with OpenAsar.\n"+ - "You're installing OpenAsar at your own risk. If you run into issues with OpenAsar,\n"+ - "no support will be provided, join the OpenAsar Server instead!\n\n"+ - "To install OpenAsar, press Accept and click 'Install OpenAsar' again.", true), - InfoModal("#openasar-patched", "Successfully Installed OpenAsar", "You must now fully close Discord (from the tray)"), - InfoModal("#openasar-unpatched", "Successfully Uninstalled OpenAsar", "You must now fully close Discord (from the tray)"), - InfoModal("#modal"+strconv.Itoa(modalId), modalTitle, modalMessage), - } - - return layout -} - -func renderErrorCard(col color.Color, message string) g.Widget { - return g.Style(). - SetColor(g.StyleColorChildBg, col). - SetStyleFloat(g.StyleVarAlpha, 0.9). - SetStyle(g.StyleVarWindowPadding, 10, 10). - SetStyleFloat(g.StyleVarChildRounding, 5). - To( - g.Child(). - Size(g.Auto, 40). - Layout( - g.Row( - g.Style().SetColor(g.StyleColorText, color.Black).To( - g.Markdown(&message), - ), - ), - ), - ) -} - -func loop() { - g.PushWindowPadding(48, 48) - - g.SingleWindow(). - RegisterKeyboardShortcuts( - g.WindowShortcut{Key: g.KeyUp, Callback: func() { - if radioIdx > 0 { - radioIdx-- - } - }}, - g.WindowShortcut{Key: g.KeyDown, Callback: func() { - if radioIdx < customChoiceIdx { - radioIdx++ - } - }}, - ). - Layout( - g.Align(g.AlignCenter).To( - g.Style().SetFontSize(40).To( - g.Label("Vencord Installer"), - ), - ), - - g.Dummy(0, 20), - - g.Style().SetFontSize(20).To( - g.Row( - g.Label(Ternary(IsDevInstall, "Dev Install: ", "Files will be downloaded to: ")+FilesDir), - g.Style(). - SetColor(g.StyleColorButton, DiscordBlue). - SetStyle(g.StyleVarFramePadding, 4, 4). - To( - g.Button("Open Directory").OnClick(func() { - g.OpenURL("file://" + FilesDir) - }), - ), - ), - &CondWidget{!IsDevInstall, func() g.Widget { - return g.Label("To customise this location, set the environment variable 'VENCORD_USER_DATA_DIR' and restart me").Wrapped(true) - }, nil}, - g.Dummy(0, 10), - g.Label("Installer Version: "+InstallerTag+" ("+InstallerGitHash+")"+Ternary(IsInstallerOutdated, " - OUTDATED", "")), - g.Label("Local Vencord Version: "+InstalledHash), - &CondWidget{ - GithubError == nil, - func() g.Widget { - if IsDevInstall { - return g.Label("Not updating Vencord due to being in DevMode") - } - return g.Label("Latest Vencord Version: " + LatestHash) - }, func() g.Widget { - return renderErrorCard(DiscordRed, "Failed to fetch Info from GitHub: "+GithubError.Error()) - }, - }, - &CondWidget{ - IsInstallerOutdated, - func() g.Widget { - return renderErrorCard(DiscordYellow, "This Installer is outdated!"+GetInstallerDownloadMarkdown()) - }, - nil, - }, - ), - - &CondWidget{ - predicate: FilesDirErr != nil, - ifWidget: renderFilesDirErr, - elseWidget: renderInstaller, - }, - ) - - g.PopStyle() -} + package main + + import ( + "bytes" + _ "embed" + "errors" + "fmt" + g "github.com/AllenDang/giu" + "github.com/AllenDang/imgui-go" + "image" + "image/color" + // png decoder for icon + _ "image/png" + "os" + path "path/filepath" + "runtime" + "strconv" + "strings" + ) + + var ( + discords []any + radioIdx int + customChoiceIdx int + + customDir string + autoCompleteDir string + autoCompleteFile string + autoCompleteCandidates []string + autoCompleteIdx int + lastAutoComplete string + didAutoComplete bool + + modalId = 0 + modalTitle = "Oh No :(" + modalMessage = "You should never see this" + + acceptedOpenAsar bool + + win *g.MasterWindow + ) + + //go:embed winres/icon.png + var iconBytes []byte + + func main() { + InitGithubDownloader() + discords = FindDiscords() + + customChoiceIdx = len(discords) + + go func() { + <-GithubDoneChan + g.Update() + }() + + go func() { + CheckSelfUpdate() + g.Update() + }() + + win = g.NewMasterWindow("Vencord Installer", 1200, 800, 0) + + icon, _, err := image.Decode(bytes.NewReader(iconBytes)) + if err != nil { + fmt.Println("Failed to load application icon", err) + fmt.Println(iconBytes, len(iconBytes)) + } else { + win.SetIcon([]image.Image{icon}) + } + win.Run(loop) + } + + type CondWidget struct { + predicate bool + ifWidget func() g.Widget + elseWidget func() g.Widget + } + + func (w *CondWidget) Build() { + if w.predicate { + w.ifWidget().Build() + } else if w.elseWidget != nil { + w.elseWidget().Build() + } + } + + func getChosenInstall() *DiscordInstall { + var choice *DiscordInstall + if radioIdx == customChoiceIdx { + choice = ParseDiscord(customDir, "") + if choice == nil { + ShowModal("Hey now...", "That doesn't seem to be a Discord install.\nPlease make sure you select the base folder\n(blah/Discord, not blah/Discord/resources/app)") + } + } else { + choice = discords[radioIdx].(*DiscordInstall) + } + return choice + } + + func InstallLatestBuilds() (err error) { + if IsDevInstall { + return + } + + err = installLatestBuilds() + if err != nil { + ShowModal("Uh Oh!", "Failed to install the latest Vencord builds from GitHub:\n"+err.Error()) + } + return + } + + func handlePatch() { + choice := getChosenInstall() + if choice != nil { + choice.Patch() + } + } + + func handleUnpatch() { + choice := getChosenInstall() + if choice != nil { + choice.Unpatch() + } + } + + func handleOpenAsar() { + if acceptedOpenAsar || getChosenInstall().IsOpenAsar() { + handleOpenAsarConfirmed() + return + } + + g.OpenPopup("#openasar-confirm") + } + + func handleOpenAsarConfirmed() { + choice := getChosenInstall() + if choice != nil { + if choice.IsOpenAsar() { + if err := choice.UninstallOpenAsar(); err != nil { + handleErr(err, "uninstall OpenAsar from") + } else { + g.OpenPopup("#openasar-unpatched") + g.Update() + } + } else { + if err := choice.InstallOpenAsar(); err != nil { + handleErr(err, "install OpenAsar on") + } else { + g.OpenPopup("#openasar-patched") + g.Update() + } + } + } + } + + func handleErr(err error, action string) { + if errors.Is(err, os.ErrPermission) { + switch os := runtime.GOOS; os { + case "windows": + err = errors.New("Permission denied. Make sure your Discord is fully closed (from the tray)!") + case "darwin": + err = errors.New("Permission denied. Please grant the installer Full Disk Access in the system settings (privacy & security page).") + default: + err = errors.New("Permission denied. Maybe try running me as Administrator/Root?") + } + } + + ShowModal("Failed to "+action+" this Install", err.Error()) + } + + func HandleScuffedInstall() { + g.OpenPopup("#scuffed-install") + } + + func (di *DiscordInstall) Patch() { + if CheckScuffedInstall() { + return + } + if err := di.patch(); err != nil { + handleErr(err, "patch") + } else { + g.OpenPopup("#patched") + } + } + + func (di *DiscordInstall) Unpatch() { + if err := di.unpatch(); err != nil { + handleErr(err, "unpatch") + } else { + g.OpenPopup("#unpatched") + } + } + + func onCustomInputChanged() { + p := customDir + if len(p) != 0 { + // Select the custom option for people + radioIdx = customChoiceIdx + } + + dir := path.Dir(p) + + isNewDir := strings.HasSuffix(p, "/") + wentUpADir := !isNewDir && dir != autoCompleteDir + + if isNewDir || wentUpADir { + autoCompleteDir = dir + // reset all the funnies + autoCompleteIdx = 0 + lastAutoComplete = "" + autoCompleteFile = "" + autoCompleteCandidates = nil + + // Generate autocomplete items + files, err := os.ReadDir(dir) + if err == nil { + for _, file := range files { + autoCompleteCandidates = append(autoCompleteCandidates, file.Name()) + } + } + } else if !didAutoComplete { + // reset auto complete and update our file + autoCompleteFile = path.Base(p) + lastAutoComplete = "" + } + + if wentUpADir { + autoCompleteFile = path.Base(p) + } + + didAutoComplete = false + } + + // go can you give me []any? + // to pass to giu RangeBuilder? + // yeeeeees + // actually returns []string like a boss + func makeAutoComplete() []any { + input := strings.ToLower(autoCompleteFile) + + var candidates []any + for _, e := range autoCompleteCandidates { + file := strings.ToLower(e) + if autoCompleteFile == "" || strings.HasPrefix(file, input) { + candidates = append(candidates, e) + } + } + return candidates + } + + func makeRadioOnChange(i int) func() { + return func() { + radioIdx = i + } + } + + func renderFilesDirErr() g.Widget { + return g.Layout{ + g.Dummy(0, 50), + g.Style(). + SetColor(g.StyleColorText, DiscordRed). + SetFontSize(30). + To( + g.Align(g.AlignCenter).To( + g.Label("Error: Failed to create: "+FilesDirErr.Error()), + g.Label("Resolve this error, then restart me!"), + ), + ), + } + } + + func Tooltip(label string) g.Widget { + return g.Style(). + SetStyle(g.StyleVarWindowPadding, 10, 8). + SetStyleFloat(g.StyleVarWindowRounding, 8). + To( + g.Tooltip(label), + ) + } + + func InfoModal(id, title, description string) g.Widget { + return RawInfoModal(id, title, description, false) + } + + func RawInfoModal(id, title, description string, isOpenAsar bool) g.Widget { + isDynamic := strings.HasPrefix(id, "#modal") + return g.Style(). + SetStyle(g.StyleVarWindowPadding, 30, 30). + SetStyleFloat(g.StyleVarWindowRounding, 12). + To( + g.PopupModal(id). + Flags(g.WindowFlagsNoTitleBar | Ternary(isDynamic, g.WindowFlagsAlwaysAutoResize, 0)). + Layout( + g.Align(g.AlignCenter).To( + g.Style().SetFontSize(30).To( + g.Label(title), + ), + g.Style().SetFontSize(20).To( + g.Label(description).Wrapped(isDynamic), + ), + &CondWidget{id == "#scuffed-install", func() g.Widget { + return g.Column( + g.Dummy(0, 10), + g.Button("Take me there!").OnClick(func() { + // this issue only exists on windows so using Windows specific path is oki + username := os.Getenv("USERNAME") + programData := os.Getenv("PROGRAMDATA") + g.OpenURL("file://" + path.Join(programData, username)) + }).Size(200, 30), + ) + }, nil}, + g.Dummy(0, 20), + &CondWidget{isOpenAsar, + func() g.Widget { + return g.Row( + g.Button("Accept"). + OnClick(func() { + acceptedOpenAsar = true + g.CloseCurrentPopup() + }). + Size(100, 30), + g.Button("Cancel"). + OnClick(func() { + g.CloseCurrentPopup() + }). + Size(100, 30), + ) + }, + func() g.Widget { + return g.Button("Ok"). + OnClick(func() { + g.CloseCurrentPopup() + }). + Size(100, 30) + }, + }, + ), + ), + ) + } + + func ShowModal(title, desc string) { + modalTitle = title + modalMessage = desc + modalId++ + g.OpenPopup("#modal" + strconv.Itoa(modalId)) + } + + func renderInstaller() g.Widget { + candidates := makeAutoComplete() + wi, _ := win.GetSize() + w := float32(wi) - 96 + + var currentDiscord *DiscordInstall + if radioIdx != customChoiceIdx { + currentDiscord = discords[radioIdx].(*DiscordInstall) + } + var isOpenAsar = currentDiscord != nil && currentDiscord.IsOpenAsar() + + layout := g.Layout{ + g.Dummy(0, 20), + g.Separator(), + g.Dummy(0, 5), + + g.Style().SetFontSize(30).To( + g.Label("Please select an install to patch"), + ), + + g.Style().SetFontSize(20).To( + g.RangeBuilder("Discords", discords, func(i int, v any) g.Widget { + d := v.(*DiscordInstall) + text := d.path + " (" + d.branch + ")" + if d.isPatched { + text = "[PATCHED] " + text + } + return g.RadioButton(text, radioIdx == i). + OnChange(makeRadioOnChange(i)) + }), + + g.RadioButton("Custom Install Location", radioIdx == customChoiceIdx). + OnChange(makeRadioOnChange(customChoiceIdx)), + ), + + g.Dummy(0, 5), + g.Style(). + SetStyle(g.StyleVarFramePadding, 16, 16). + SetFontSize(20). + To( + g.InputText(&customDir).Hint("The custom location"). + Size(w - 16). + Flags(g.InputTextFlagsCallbackCompletion). + OnChange(onCustomInputChanged). + // this library has its own autocomplete but it's broken + Callback( + func(data imgui.InputTextCallbackData) int32 { + if len(candidates) == 0 { + return 0 + } + // just wrap around + if autoCompleteIdx >= len(candidates) { + autoCompleteIdx = 0 + } + + // used by change handler + didAutoComplete = true + + start := len(customDir) + // Delete previous auto complete + if lastAutoComplete != "" { + start -= len(lastAutoComplete) + data.DeleteBytes(start, len(lastAutoComplete)) + } else if autoCompleteFile != "" { // delete partial input + start -= len(autoCompleteFile) + data.DeleteBytes(start, len(autoCompleteFile)) + } + + // Insert auto complete + lastAutoComplete = candidates[autoCompleteIdx].(string) + data.InsertBytes(start, []byte(lastAutoComplete)) + autoCompleteIdx++ + + return 0 + }, + ), + ), + g.RangeBuilder("AutoComplete", candidates, func(i int, v any) g.Widget { + dir := v.(string) + return g.Label(dir) + }), + + g.Dummy(0, 20), + + g.Style().SetFontSize(20).To( + g.Row( + g.Style(). + SetColor(g.StyleColorButton, DiscordGreen). + SetDisabled(GithubError != nil). + To( + g.Button("Install"). + OnClick(handlePatch). + Size((w-40)/4, 50), + Tooltip("Patch the selected Discord Install"), + ), + g.Style(). + SetColor(g.StyleColorButton, Ternary(isOpenAsar, DiscordRed, DiscordGreen)). + To( + g.Button(Ternary(isOpenAsar, "Uninstall OpenAsar", Ternary(currentDiscord != nil, "Install OpenAsar", "(Un-)Install OpenAsar"))). + OnClick(handleOpenAsar). + Size((w-40)/4, 50), + Tooltip("Manage OpenAsar"), + ), + g.Style(). + SetColor(g.StyleColorButton, DiscordRed). + To( + g.Button("Uninstall"). + OnClick(handleUnpatch). + Size((w-40)/4, 50), + Tooltip("Unpatch the selected Discord Install"), + ), + g.Style(). + SetColor(g.StyleColorButton, DiscordBlue). + SetDisabled(IsDevInstall || GithubError != nil). + To( + g.Button(Ternary(GithubError == nil && LatestHash == InstalledHash, "Re-Download Vencord", "Update")). + OnClick(func() { + if err := InstallLatestBuilds(); err == nil { + g.OpenPopup("#downloaded") + } + }). + Size((w-40)/4, 50), + Tooltip("Update your local Vencord files"), + ), + ), + ), + + InfoModal("#downloaded", "Successfully Downloaded", "The Vencord files were successfully downloaded!"), + InfoModal("#patched", "Successfully Patched", "You must now fully close Discord (from the tray).\n"+ + "Then, verify Vencord installed successfully by looking for its category in Discord Settings"), + InfoModal("#unpatched", "Successfully Unpatched", "You must now fully close Discord (from the tray)"), + InfoModal("#scuffed-install", "Hold On!", "You have a broken Discord Install.\n"+ + "Sometimes Discord decides to install to the wrong location for some reason!\n"+ + "You need to fix this before patching, otherwise Vencord will likely not work.\n\n"+ + "Use the below button to jump there and delete any folder called Discord or Squirrel.\n"+ + "If the folder is now empty, feel free to go back a step and delete that folder too.\n"+ + "Then see if Discord still starts. If not, reinstall it"), + RawInfoModal("#openasar-confirm", "OpenAsar", "OpenAsar is an open-source alternative of Discord desktop's app.asar.\n"+ + "Vencord is in no way affiliated with OpenAsar.\n"+ + "You're installing OpenAsar at your own risk. If you run into issues with OpenAsar,\n"+ + "no support will be provided, join the OpenAsar Server instead!\n\n"+ + "To install OpenAsar, press Accept and click 'Install OpenAsar' again.", true), + InfoModal("#openasar-patched", "Successfully Installed OpenAsar", "You must now fully close Discord (from the tray)"), + InfoModal("#openasar-unpatched", "Successfully Uninstalled OpenAsar", "You must now fully close Discord (from the tray)"), + InfoModal("#modal"+strconv.Itoa(modalId), modalTitle, modalMessage), + } + + return layout + } + + func renderErrorCard(col color.Color, message string) g.Widget { + return g.Style(). + SetColor(g.StyleColorChildBg, col). + SetStyleFloat(g.StyleVarAlpha, 0.9). + SetStyle(g.StyleVarWindowPadding, 10, 10). + SetStyleFloat(g.StyleVarChildRounding, 5). + To( + g.Child(). + Size(g.Auto, 40). + Layout( + g.Row( + g.Style().SetColor(g.StyleColorText, color.Black).To( + g.Markdown(&message), + ), + ), + ), + ) + } + + func loop() { + g.PushWindowPadding(48, 48) + + g.SingleWindow(). + RegisterKeyboardShortcuts( + g.WindowShortcut{Key: g.KeyUp, Callback: func() { + if radioIdx > 0 { + radioIdx-- + } + }}, + g.WindowShortcut{Key: g.KeyDown, Callback: func() { + if radioIdx < customChoiceIdx { + radioIdx++ + } + }}, + ). + Layout( + g.Align(g.AlignCenter).To( + g.Style().SetFontSize(40).To( + g.Label("Vencord Installer"), + ), + ), + + g.Dummy(0, 20), + + g.Style().SetFontSize(20).To( + g.Row( + g.Label(Ternary(IsDevInstall, "Dev Install: ", "Files will be downloaded to: ")+FilesDir), + g.Style(). + SetColor(g.StyleColorButton, DiscordBlue). + SetStyle(g.StyleVarFramePadding, 4, 4). + To( + g.Button("Open Directory").OnClick(func() { + g.OpenURL("file://" + FilesDir) + }), + ), + ), + &CondWidget{!IsDevInstall, func() g.Widget { + return g.Label("To customise this location, set the environment variable 'VENCORD_USER_DATA_DIR' and restart me").Wrapped(true) + }, nil}, + g.Dummy(0, 10), + g.Label("Installer Version: "+InstallerTag+" ("+InstallerGitHash+")"+Ternary(IsInstallerOutdated, " - OUTDATED", "")), + g.Label("Local Vencord Version: "+InstalledHash), + &CondWidget{ + GithubError == nil, + func() g.Widget { + if IsDevInstall { + return g.Label("Not updating Vencord due to being in DevMode") + } + return g.Label("Latest Vencord Version: " + LatestHash) + }, func() g.Widget { + return renderErrorCard(DiscordRed, "Failed to fetch Info from GitHub: "+GithubError.Error()) + }, + }, + &CondWidget{ + IsInstallerOutdated, + func() g.Widget { + return renderErrorCard(DiscordYellow, "This Installer is outdated!"+GetInstallerDownloadMarkdown()) + }, + nil, + }, + ), + + &CondWidget{ + predicate: FilesDirErr != nil, + ifWidget: renderFilesDirErr, + elseWidget: renderInstaller, + }, + ) + + g.PopStyle() + } + \ No newline at end of file