Fix bugs
This commit is contained in:
Binary file not shown.
@@ -8874,6 +8874,52 @@
|
||||
"filePath": "src/nightcordplugins/platformSpoofer",
|
||||
"dirName": "platformSpoofer"
|
||||
},
|
||||
{
|
||||
"hasPatches": false,
|
||||
"hasCommands": false,
|
||||
"enabledByDefault": true,
|
||||
"required": false,
|
||||
"isModified": false,
|
||||
"tags": [
|
||||
|
||||
],
|
||||
"name": "PrevNames",
|
||||
"description": "Shows the username history of a user. Right-click -> PrevNames.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nightcord",
|
||||
"id": "0"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
"ContextMenuAPI"
|
||||
],
|
||||
"filePath": "src/nightcordplugins/PrevNames",
|
||||
"dirName": "PrevNames"
|
||||
},
|
||||
{
|
||||
"hasPatches": false,
|
||||
"hasCommands": false,
|
||||
"enabledByDefault": true,
|
||||
"required": false,
|
||||
"isModified": false,
|
||||
"tags": [
|
||||
|
||||
],
|
||||
"name": "PrevNames",
|
||||
"description": "Shows the username history of a user. Right-click -> PrevNames.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nightcord",
|
||||
"id": "0"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
"ContextMenuAPI"
|
||||
],
|
||||
"filePath": "src/nightcordplugins/PrevNames",
|
||||
"dirName": "PrevNames"
|
||||
},
|
||||
{
|
||||
"hasPatches": true,
|
||||
"hasCommands": false,
|
||||
|
||||
@@ -353,7 +353,12 @@ function buildBrowserWindowOptions(): BrowserWindowConstructorOptions {
|
||||
// disable renderer backgrounding to prevent the app from unloading when in the background
|
||||
backgroundThrottling: false
|
||||
},
|
||||
frame: !noFrame,
|
||||
// FIX 1: quand customTitleBar ou frameless (noFrame=true), utiliser titleBarStyle:"hidden"
|
||||
// sur Windows/Linux. frame:false seul supprime toute zone draggable OS -> fenetre immobile.
|
||||
// titleBarStyle:"hidden" garde la zone de drag native invisible mais fonctionnelle.
|
||||
...(noFrame && process.platform !== "darwin"
|
||||
? { frame: false, titleBarStyle: "hidden" }
|
||||
: { frame: !noFrame }),
|
||||
autoHideMenuBar: enableMenu,
|
||||
...getWindowBoundsOptions()
|
||||
};
|
||||
@@ -424,11 +429,14 @@ function createMainWindow() {
|
||||
|
||||
initWindowBoundsListeners(win);
|
||||
|
||||
win.on("enter-html-full-screen", () => {
|
||||
win.setFullScreen(true);
|
||||
});
|
||||
// FIX 2: on ne force plus setFullScreen(true) sur enter-html-full-screen.
|
||||
// L'ancien code causait un blocage : quand Discord passait une video en plein ecran HTML5,
|
||||
// Electron forcait le fullscreen natif de l'OS -> les utilisateurs ne pouvaient plus
|
||||
// sortir du fullscreen ou redimensionner la fenetre normalement.
|
||||
// On laisse Discord gerer son propre fullscreen HTML sans toucher a la fenetre Electron.
|
||||
win.on("leave-html-full-screen", () => {
|
||||
win.setFullScreen(false);
|
||||
// S'assurer que le fullscreen natif est bien quitte si Discord sort du mode HTML FS
|
||||
if (win.isFullScreen()) win.setFullScreen(false);
|
||||
});
|
||||
|
||||
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin")
|
||||
@@ -499,7 +507,13 @@ export async function createWindows() {
|
||||
|
||||
if (!startMinimized) {
|
||||
if (splash) mainWin?.show();
|
||||
if (State.store.maximized && !isDeckGameMode) mainWin?.maximize();
|
||||
// FIX 3: on ne maximize que si maximized=true ET windowBounds absent.
|
||||
// L'ancien code maximizait systematiquement si maximized=true, meme quand l'utilisateur
|
||||
// avait redimensionne la fenetre -> toujours bloque en plein ecran au demarrage.
|
||||
const shouldMaximize = State.store.maximized === true
|
||||
&& !isDeckGameMode
|
||||
&& !State.store.windowBounds;
|
||||
if (shouldMaximize) mainWin?.maximize();
|
||||
}
|
||||
|
||||
if (isDeckGameMode) {
|
||||
@@ -510,9 +524,11 @@ export async function createWindows() {
|
||||
}
|
||||
|
||||
mainWin.once("show", () => {
|
||||
if (State.store.maximized && !mainWin?.isMaximized() && !isDeckGameMode) {
|
||||
mainWin?.maximize();
|
||||
}
|
||||
const shouldMaximize = State.store.maximized === true
|
||||
&& !mainWin?.isMaximized()
|
||||
&& !isDeckGameMode
|
||||
&& !State.store.windowBounds;
|
||||
if (shouldMaximize) mainWin?.maximize();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -127,10 +127,59 @@ export function makeLinksOpenExternally(win: BrowserWindow) {
|
||||
// Drop the static temp page Discord web loads for the connections popout
|
||||
if (frameName === "authorize" && searchParams.get("loading") === "true") return { action: "deny" };
|
||||
|
||||
// Allow captcha popups to open inside Electron (hCaptcha / reCaptcha)
|
||||
// Discord opens them via window.open() — they must stay in-process or the
|
||||
// captcha iframe can never communicate back to Discord.
|
||||
if (
|
||||
hostname.includes("hcaptcha.com") ||
|
||||
hostname.includes("recaptcha.net") ||
|
||||
hostname.includes("google.com") && pathname.startsWith("/recaptcha") ||
|
||||
hostname.includes("discord.com") && pathname.startsWith("/cdn-cgi/") ||
|
||||
// Discord sometimes opens its own captcha flow on discord.com
|
||||
(DISCORD_HOSTNAMES.includes(hostname) && (pathname.includes("captcha") || searchParams.has("captcha")))
|
||||
) {
|
||||
return {
|
||||
action: "allow",
|
||||
overrideBrowserWindowOptions: {
|
||||
width: 500,
|
||||
height: 600,
|
||||
frame: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return handleExternalUrl(url, protocol);
|
||||
});
|
||||
|
||||
win.webContents.on("did-create-window", (childWin, { frameName, options, url }: any) => {
|
||||
// Detect captcha windows and handle them gracefully
|
||||
let isCaptcha = false;
|
||||
if (url) {
|
||||
try {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
isCaptcha =
|
||||
hostname.includes("hcaptcha.com") ||
|
||||
hostname.includes("recaptcha.net") ||
|
||||
(hostname.includes("google.com") && pathname.startsWith("/recaptcha")) ||
|
||||
(hostname.includes("discord.com") && pathname.startsWith("/cdn-cgi/")) ||
|
||||
(DISCORD_HOSTNAMES.includes(hostname) && (pathname.includes("captcha")));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (isCaptcha) {
|
||||
childWin.setMenuBarVisibility(false);
|
||||
// Allow the captcha window to open links externally if needed
|
||||
childWin.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url));
|
||||
childWin.once("closed", () => childWin.removeAllListeners());
|
||||
return;
|
||||
}
|
||||
|
||||
let isPopout = frameName.startsWith("DISCORD_");
|
||||
|
||||
if (!isPopout) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,13 +32,40 @@ const settings = definePluginSettings({
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Tick hook — forces a re-render every second ─────────────────────────────
|
||||
// ─── Global tick — UN seul setInterval partagé par tous les composants ─────────
|
||||
// BUGFIX: l'ancienne implémentation créait un setInterval PAR composant timestamp
|
||||
// rendu (50+ messages = 50+ intervals), forçant 50+ re-renders React par seconde
|
||||
// → freeze complet de Discord. Un seul interval global notifie tous les abonnés.
|
||||
|
||||
const tickListeners = new Set<() => void>();
|
||||
let globalTickInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startGlobalTick() {
|
||||
if (globalTickInterval !== null) return;
|
||||
globalTickInterval = setInterval(() => {
|
||||
for (const fn of tickListeners) {
|
||||
try { fn(); } catch { }
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopGlobalTick() {
|
||||
if (tickListeners.size > 0) return; // still has subscribers
|
||||
if (globalTickInterval !== null) {
|
||||
clearInterval(globalTickInterval);
|
||||
globalTickInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function useSecondTick() {
|
||||
const [, tick] = useReducer((n: number) => n + 1, 0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
tickListeners.add(tick);
|
||||
startGlobalTick();
|
||||
return () => {
|
||||
tickListeners.delete(tick);
|
||||
stopGlobalTick();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -79,6 +106,15 @@ export default definePlugin({
|
||||
|
||||
renderTimestamp,
|
||||
|
||||
stop() {
|
||||
// Cleanup global tick on plugin disable
|
||||
tickListeners.clear();
|
||||
if (globalTickInterval !== null) {
|
||||
clearInterval(globalTickInterval);
|
||||
globalTickInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
patches: [
|
||||
// ─── Main Timestamp component (cozy + compact messages + hover tooltip) ─
|
||||
{
|
||||
|
||||
@@ -80,7 +80,6 @@ async function transcribe(blob: Blob): Promise<string> {
|
||||
form.append("file", blob, "audio.webm");
|
||||
form.append("model", "whisper-large-v3-turbo");
|
||||
form.append("response_format", "text");
|
||||
// Prompt pour orienter Whisper vers du français et éviter les hallucinations "Thank you"
|
||||
form.append("prompt", "Ceci est une dictée vocale en français. Ne pas traduire en anglais. Ne pas générer de texte si il n'y a que du silence.");
|
||||
if (language) form.append("language", language);
|
||||
|
||||
@@ -124,16 +123,30 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
setErrorMsg(null);
|
||||
activeRef.current = true;
|
||||
|
||||
// Helper to map Discord input device to real WebAudio device
|
||||
async function getRealInputDeviceId(discordId: string): Promise<string> {
|
||||
if (!discordId || discordId === "default") return "default";
|
||||
try {
|
||||
const devs = MediaEngineStore.getInputDevices();
|
||||
<<<<<<< HEAD
|
||||
const selected = devs[discordId];
|
||||
if (!selected || !selected.name) return "default";
|
||||
|
||||
=======
|
||||
let targetName = "";
|
||||
|
||||
if (devs && typeof devs === "object") {
|
||||
if (Array.isArray(devs)) {
|
||||
const d = devs.find(item => item.id === discordId);
|
||||
if (d) targetName = d.name;
|
||||
} else if (devs[discordId]) {
|
||||
targetName = devs[discordId].name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetName) return "default";
|
||||
|
||||
>>>>>>> 5ab15b59 (fix bugs)
|
||||
let webDevs = await navigator.mediaDevices.enumerateDevices();
|
||||
// trigger permissions if empty labels
|
||||
if (webDevs.some(d => d.kind === "audioinput" && !d.label)) {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
@@ -143,9 +156,15 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
let match = webDevs.find(d => d.kind === "audioinput" && d.deviceId === discordId);
|
||||
|
||||
if (!match) {
|
||||
<<<<<<< HEAD
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
const normSelected = normalize(selected.name);
|
||||
|
||||
=======
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const normSelected = normalize(targetName);
|
||||
|
||||
>>>>>>> 5ab15b59 (fix bugs)
|
||||
match = webDevs.find(d => {
|
||||
if (d.kind !== "audioinput" || !d.label) return false;
|
||||
const normLabel = normalize(d.label);
|
||||
@@ -154,11 +173,11 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
}
|
||||
|
||||
if (match) {
|
||||
console.log(`[VoiceDictation] Mapped Discord device "${selected.name}" to WebAudio deviceId "${match.deviceId}"`);
|
||||
showToast(`Dictation: Using mic "${match.label || selected.name}"`, Toasts.Type.SUCCESS);
|
||||
console.log(`[VoiceDictation] Mapped Discord device "${targetName}" to WebAudio deviceId "${match.deviceId}"`);
|
||||
showToast(`Dictation: Using mic "${match.label || targetName}"`, Toasts.Type.SUCCESS);
|
||||
return match.deviceId;
|
||||
} else {
|
||||
showToast(`Dictation: Could not map "${selected.name}", using default`, Toasts.Type.FAILURE);
|
||||
showToast(`Dictation: Could not map "${targetName}", using default`, Toasts.Type.FAILURE);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[VoiceDictation] Error mapping device ID:", err);
|
||||
@@ -167,14 +186,12 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
return "default";
|
||||
}
|
||||
|
||||
// Open microphone
|
||||
let stream: MediaStream;
|
||||
try {
|
||||
const discordDeviceId = MediaEngineStore.getInputDeviceId();
|
||||
const realDeviceId = await getRealInputDeviceId(discordDeviceId);
|
||||
|
||||
try {
|
||||
// Premier essai : avec le deviceId spécifique (fonctionne si permission ok)
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: realDeviceId && realDeviceId !== "default"
|
||||
? { deviceId: { exact: realDeviceId } }
|
||||
@@ -182,7 +199,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
});
|
||||
} catch (firstErr: any) {
|
||||
if (firstErr.name === "NotAllowedError" || firstErr.name === "PermissionDeniedError") {
|
||||
// Fallback: permission pas encore accordée, demander sans deviceId
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
} else {
|
||||
throw firstErr;
|
||||
@@ -198,7 +214,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Choose audio format
|
||||
const mimeType = ["audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4"]
|
||||
.find(m => MediaRecorder.isTypeSupported(m)) ?? "";
|
||||
|
||||
@@ -213,7 +228,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
recorder.start();
|
||||
setRecording(true);
|
||||
|
||||
// Flush and transcribe at regular intervals
|
||||
const chunkMs = (settings.store.chunkSeconds ?? 5) * 1000;
|
||||
timerRef.current = setInterval(() => flushAndTranscribe(), chunkMs);
|
||||
}
|
||||
@@ -221,7 +235,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
async function flushAndTranscribe() {
|
||||
if (!recorderRef.current || recorderRef.current.state !== "recording") return;
|
||||
|
||||
// Stop briefly to get a complete blob
|
||||
recorderRef.current.stop();
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
@@ -232,7 +245,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
chunksRef.current = [];
|
||||
|
||||
if (chunks.length === 0 || !activeRef.current) {
|
||||
// Restart if still active
|
||||
if (activeRef.current && streamRef.current) restartRecorder();
|
||||
return;
|
||||
}
|
||||
@@ -240,36 +252,26 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
const mimeType = recorderRef.current?.mimeType || "audio/webm";
|
||||
const blob = new Blob(chunks, { type: mimeType });
|
||||
|
||||
// Debug log
|
||||
console.log("[VoiceDictation] Blob size:", blob.size);
|
||||
|
||||
if (blob.size < 500) {
|
||||
if (activeRef.current) restartRecorder();
|
||||
return; // Silence / too short
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const text = await transcribe(blob);
|
||||
// Debug log
|
||||
console.log("[VoiceDictation] Transcribed text:", text);
|
||||
if (text && text.length > 0) {
|
||||
// Filter common Whisper hallucinations (silence → phantom text)
|
||||
const t = text.trim();
|
||||
const isHallucination =
|
||||
// Simple exact patterns
|
||||
/^(merci|thanks?|thank you|music|♪|🎵|\.\.\.|\.\s*)+$/i.test(t) ||
|
||||
// Subtitling / subtitles
|
||||
/sous[- ]?titr/i.test(t) ||
|
||||
// Radio-Canada, SRC, etc.
|
||||
/radio[- ]?canada|société radio/i.test(t) ||
|
||||
// "Thanks for watching", etc.
|
||||
/merci .*(regard|écouter|suivi)|thanks? .*watch/i.test(t) ||
|
||||
// "Transcription by...", "Transcribed by..."
|
||||
/transcri(ption|t)\s*(par|by)/i.test(t) ||
|
||||
// Repetitive text (same word/syllable 3+ times)
|
||||
/^(.{1,15})\1{2,}$/i.test(t.replace(/\s+/g, "")) ||
|
||||
// Too short and punctuation only
|
||||
/^[\s.,!?…\-–—]+$/.test(t);
|
||||
if (!isHallucination) insertText(text + " ");
|
||||
}
|
||||
@@ -324,7 +326,6 @@ const VoiceDictationButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
|
||||
function toggle() {
|
||||
if (recording) {
|
||||
// Flush immediate transcription on stop
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
@@ -365,4 +366,4 @@ export default definePlugin({
|
||||
icon: DictationIcon as any,
|
||||
render: VoiceDictationButton,
|
||||
},
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
declare module "*.png";
|
||||
@@ -0,0 +1,9 @@
|
||||
import "./style.css";
|
||||
|
||||
export const Box = () => {
|
||||
return (
|
||||
<div className="box">
|
||||
<div className="pill" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 507 B |
Binary file not shown.
|
After Width: | Height: | Size: 507 B |
@@ -0,0 +1,9 @@
|
||||
.box {
|
||||
display: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.box .pill {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginNative } from "@utils/types";
|
||||
import { Button, MediaEngineStore, showToast, Toasts, useState } from "@webpack/common";
|
||||
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { MouseEvent } from "react";
|
||||
import { useTimer } from "@utils/react";
|
||||
|
||||
import { cl, VoiceMessage } from "..";
|
||||
@@ -32,12 +15,14 @@ export type VoicePreviewOptions = {
|
||||
src?: string;
|
||||
waveform: string;
|
||||
recording?: boolean;
|
||||
onDownload?: () => void;
|
||||
};
|
||||
|
||||
export const VoicePreview = ({
|
||||
src,
|
||||
waveform,
|
||||
recording,
|
||||
onDownload,
|
||||
}: VoicePreviewOptions) => {
|
||||
const durationMs = useTimer({
|
||||
deps: [recording]
|
||||
@@ -46,8 +31,26 @@ export const VoicePreview = ({
|
||||
const durationSeconds = recording ? Math.floor(durationMs / 1000) : 0;
|
||||
const durationDisplay = Math.floor(durationSeconds / 60) + ":" + (durationSeconds % 60).toString().padStart(2, "0");
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||
if (!onDownload) return;
|
||||
const target = event.target as HTMLElement;
|
||||
const link = target.closest('a[href^="blob:"]') as HTMLAnchorElement | null;
|
||||
|
||||
if (link) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onDownload();
|
||||
}
|
||||
};
|
||||
|
||||
if (src && !recording)
|
||||
return <VoiceMessage key={src} src={src} waveform={waveform} />;
|
||||
return (
|
||||
<div className={cl("preview", "preview-playback")} onClick={handleClick}>
|
||||
<div className={cl("preview-message")}>
|
||||
<VoiceMessage key={src} src={src} waveform={waveform} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cl("preview", recording ? "preview-recording" : [])}>
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Button, MediaEngineStore, useState } from "@webpack/common";
|
||||
|
||||
import { settings, type VoiceRecorder } from "..";
|
||||
@@ -72,21 +54,8 @@ export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChang
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={toggleRecording}>
|
||||
{recording ? "Stop" : "Start"} recording
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!recording}
|
||||
onClick={() => {
|
||||
setPaused(!paused);
|
||||
if (paused) recorder?.resume();
|
||||
else recorder?.pause();
|
||||
}}
|
||||
>
|
||||
{paused ? "Resume" : "Pause"} recording
|
||||
</Button>
|
||||
</>
|
||||
<Button onClick={toggleRecording}>
|
||||
{recording ? "Stop" : "Start"} recording
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import "./styles.css";
|
||||
import "./FIGMAUI/style.css";
|
||||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
@@ -24,16 +9,15 @@ import { Card } from "@components/Card";
|
||||
import { Microphone } from "@components/Icons";
|
||||
import { Link } from "@components/Link";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { lastState as silentMessageEnabled } from "@plugins/silentMessageToggle";
|
||||
import { lastState as silentMessageEnabled } from "@nightcordplugins/silentMessageToggle";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { chooseFile } from "@utils/web";
|
||||
import { RenderModalProps } from "@vencord/discord-types";
|
||||
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
|
||||
import { Button, CloudUploader, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, Modal, openModal, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
||||
import { Button, CloudUploader, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, Modal, openModal, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState, useRef } from "@webpack/common";
|
||||
import { ComponentType } from "react";
|
||||
|
||||
import { VoiceRecorderDesktop } from "./components/DesktopRecorder";
|
||||
@@ -79,7 +63,9 @@ export const settings = definePluginSettings({
|
||||
});
|
||||
|
||||
const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||
if (!props || !props.channel) return;
|
||||
if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return;
|
||||
if (!Menu || !Menu.MenuItem) return;
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
@@ -205,7 +191,7 @@ function useObjectUrl() {
|
||||
return [url, setWithFree] as const;
|
||||
}
|
||||
|
||||
function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
|
||||
function VoiceMessageModal({ modalProps }: { modalProps: any; }) {
|
||||
const [isRecording, setRecording] = useState(false);
|
||||
const [blob, setBlob] = useState<Blob>();
|
||||
const [blobUrl, setBlobUrl] = useObjectUrl();
|
||||
@@ -234,9 +220,44 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
|
||||
|
||||
const isUnsupportedFormat = blob && (!blob.type.startsWith("audio/ogg") || blob.type.includes("codecs") && !blob.type.includes("opus"));
|
||||
|
||||
const downloadBlob = () => {
|
||||
if (!blob) return;
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = "voice-message.ogg";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
};
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
try {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (!target) return;
|
||||
if (!target.closest || !document.body.querySelector) return;
|
||||
const modalRoot = target.closest('.vc-vmsg-figma-ui') || target.closest('.vc-vmsg-figma-ui-content');
|
||||
if (!modalRoot) return;
|
||||
const anchor = target.closest('a[href^="blob:"]') as HTMLAnchorElement | null;
|
||||
if (anchor) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
downloadBlob();
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handler, true);
|
||||
return () => document.removeEventListener('click', handler, true);
|
||||
}, [blob]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...modalProps}
|
||||
className={cl("modal")}
|
||||
title="Record Voice Message"
|
||||
actions={[{
|
||||
text: "Send",
|
||||
@@ -249,8 +270,10 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
|
||||
disabled: !blob
|
||||
}]}
|
||||
>
|
||||
<div className={cl("buttons")}>
|
||||
<VoiceRecorder
|
||||
<div className={cl("figma-ui")}>
|
||||
<div className={cl("figma-ui-content")}>
|
||||
<div className={cl("buttons")}>
|
||||
<VoiceRecorder
|
||||
setAudioBlob={blob => {
|
||||
setBlob(blob);
|
||||
setBlobUrl(blob);
|
||||
@@ -275,11 +298,34 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
|
||||
{metaError
|
||||
? <Paragraph className={cl("error")}>Failed to parse selected audio file: {metaError.message}</Paragraph>
|
||||
: (
|
||||
<VoicePreview
|
||||
src={blobUrl}
|
||||
waveform={meta.waveform}
|
||||
recording={isRecording}
|
||||
/>
|
||||
<>
|
||||
<div className={cl("preview-container")}>
|
||||
<VoicePreview
|
||||
src={blobUrl}
|
||||
waveform={meta.waveform}
|
||||
recording={isRecording}
|
||||
onDownload={downloadBlob}
|
||||
/>
|
||||
</div>
|
||||
{blob && (
|
||||
<div className={cl("send-row")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
try {
|
||||
sendAudio(blob, meta ?? EMPTY_META);
|
||||
modalProps.onClose();
|
||||
showToast("Now sending voice message... Please be patient", Toasts.Type.MESSAGE);
|
||||
} catch (e) {
|
||||
showToast("Failed to send voice message", Toasts.Type.FAILURE);
|
||||
}
|
||||
}}
|
||||
disabled={!blob}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isUnsupportedFormat && (
|
||||
@@ -291,6 +337,8 @@ function VoiceMessageModal({ modalProps }: { modalProps: RenderModalProps; }) {
|
||||
</Forms.FormText>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { readFile, rm } from "fs/promises";
|
||||
import { basename, normalize } from "path";
|
||||
|
||||
@@ -1,23 +1,153 @@
|
||||
.vc-vmsg-modal {
|
||||
padding: 1em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.vc-vmsg-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5em;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
max-width: min(520px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: auto;
|
||||
max-width: 520px;
|
||||
padding: 28px 24px 26px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background: #1f2126;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.vc-vmsg-preview-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui-content .buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui-content .buttons button,
|
||||
.vc-vmsg-figma-ui-content .buttons [role="button"] {
|
||||
height: 44px !important;
|
||||
flex: 1;
|
||||
min-width: 150px !important;
|
||||
max-width: 230px !important;
|
||||
padding: 0 26px !important;
|
||||
border-radius: 14px !important;
|
||||
background: #4f535b !important;
|
||||
color: #ebedef !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 14px !important;
|
||||
letter-spacing: 0.02em !important;
|
||||
border: 1px solid rgba(255,255,255,0.09) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui-content .buttons button:hover,
|
||||
.vc-vmsg-figma-ui-content .buttons [role="button"]:hover {
|
||||
background: #5f646d !important;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui-content .buttons button[disabled],
|
||||
.vc-vmsg-figma-ui-content .buttons [role="button"][aria-disabled="true"] {
|
||||
opacity: 0.55 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.vc-vmsg-preview {
|
||||
color: var(--text-default);
|
||||
border-radius: 24px;
|
||||
background-color: var(--background-base-lower);
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
justify-content: center;
|
||||
width: min(100%, 520px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.vc-vmsg-preview-playback {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-vmsg-preview-message {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vc-vmsg-download-icon {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-vmsg-download-icon svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vc-vmsg-download-icon:hover {
|
||||
background: rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.vc-vmsg-download-wrap {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.vc-vmsg-preview-indicator {
|
||||
@@ -37,18 +167,27 @@
|
||||
margin: 0 0.5em;
|
||||
font-size: 80%;
|
||||
|
||||
/* monospace so different digits have same size */
|
||||
|
||||
font-family: var(--font-code);
|
||||
}
|
||||
|
||||
.vc-vmsg-preview-label {
|
||||
opacity: 0.5;
|
||||
letter-spacing: 0.125em;
|
||||
opacity: 0.6;
|
||||
letter-spacing: 0.12em;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vc-vmsg-figma-ui-content h4,
|
||||
.vc-vmsg-figma-ui-content h5,
|
||||
.vc-vmsg-figma-ui-content .formTitle {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: #ecedef;
|
||||
}
|
||||
|
||||
.vc-vmsg-error {
|
||||
color: var(--text-feedback-critical, #FF5C5C);
|
||||
}
|
||||
Reference in New Issue
Block a user