diff --git a/src/equicordplugins/loginWithQR/index.tsx b/src/equicordplugins/loginWithQR/index.tsx index 198784872..71af39c5d 100644 --- a/src/equicordplugins/loginWithQR/index.tsx +++ b/src/equicordplugins/loginWithQR/index.tsx @@ -7,41 +7,43 @@ import { plugins } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { Paragraph } from "@components/Paragraph"; +import SettingsPlugin, { settingsSectionMap } from "@plugins/_core/settings"; import { EquicordDevs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, Menu } from "@webpack/common"; -import { ReactElement } from "react"; +import { Button } from "@webpack/common"; import { preload, unload } from "./images"; -import { cl } from "./ui"; +import { QrCodeIcon } from "./ui"; import openQrModal from "./ui/modals/QrModal"; +const settings = definePluginSettings({ + scanQr: { + type: OptionType.COMPONENT, + description: "Scan a QR code", + component() { + if (!plugins.LoginWithQR.started) + return ( + + Enable the plugin and restart your client to scan a login QR code + + ); + + return ( + + ); + }, + }, +}); + export default definePlugin({ name: "LoginWithQR", description: "Allows you to login to another device by scanning a login QR code, just like on mobile!", authors: [EquicordDevs.nexpid], - settings: definePluginSettings({ - scanQr: { - type: OptionType.COMPONENT, - description: "Scan a QR code", - component() { - if (!plugins.LoginWithQR.started) - return ( - - Enable the plugin and restart your client to scan a login QR code - - ); - - return ( - - ); - }, - }, - }), + settings, patches: [ // Prevent paste event from firing when the QRModal is open @@ -52,69 +54,30 @@ export default definePlugin({ replace: "handleGlobalPaste:(...args)=>!$self.qrModalOpen&&$1(...args)", }, }, - // Insert a Scan QR Code button in the My Account tab - /* broken - { - find: "UserSettingsAccountProfileCard", - replacement: { - // Find the Edit User Profile button and insert our custom button. - // A bit jank, but whatever - match: /,(\(.{1,90}#{intl::USER_SETTINGS_EDIT_USER_PROFILE}\),onClick:\i\}\))/, - replace: ",$self.insertScanQrButton($1)", - }, - }, - */ - // Insert a Scan QR Code MenuItem in the Swith Accounts popout - { - find: 'id:"manage-accounts"', - replacement: { - match: /(id:"manage-accounts",.*?)}\)\)(,\i)/, - replace: "$1}),$self.ScanQrMenuItem)$2" - } - }, - - // Insert a Scan QR Code button in the Settings sheet - { - find: ".isInputProfileCustom()", - replacement: { - match: /\.CONNECTIONS/, - replace: "$&,\"SCAN_QR_CODE\"" - } - }, - // Insert a Scan QR Code button in the Settings sheet (part 2) - { - find: ".PRIVACY_ENCRYPTION_VERIFIED_DEVICES_V2]", - replacement: { - match: /\.PRIVACY_SAFETY_ALERTS_V2]:{.*?},/, - replace: "$&\"SCAN_QR_CODE\":$self.ScanQrSettingsSheet," - } - } ], qrModalOpen: false, - insertScanQrButton: (button: ReactElement) => ( -
- - {button} -
- ), - get ScanQrMenuItem() { - return ; - }, - get ScanQrSettingsSheet() { - return { - section: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE"), - onClick: openQrModal, - searchableTitles: [getIntlMessage("USER_SETTINGS_SCAN_QR_CODE")], - label: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE"), - ariaLabel: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE") - }; - }, - start() { + const { customEntries, customSections } = SettingsPlugin; + + customEntries.push({ + key: "equicord_login_with_qr", + title: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE"), + Component: openQrModal, + Icon: QrCodeIcon + }); + + customSections.push(() => ({ + section: "EquicordLoginWithQR", + label: getIntlMessage("USER_SETTINGS_SCAN_QR_CODE"), + searchableTitles: [getIntlMessage("USER_SETTINGS_SCAN_QR_CODE")], + element: openQrModal, + id: "LoginWithQR", + })); + + settingsSectionMap.push(["EquicordLoginWithQR", "equicord_login_with_qr"]); + preload(); }, diff --git a/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx b/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx index a3bc0473c..2f7ce90fc 100644 --- a/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx +++ b/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx @@ -5,20 +5,14 @@ */ import { BaseText } from "@components/BaseText"; +import { wrapTab } from "@components/settings"; import loginWithQR from "@equicordplugins/loginWithQR"; import { images } from "@equicordplugins/loginWithQR/images"; -import { getIntlMessage } from "@utils/discord"; import { - ModalContent, - ModalHeader, ModalProps, - ModalRoot, - ModalSize, - openModal, } from "@utils/modal"; import { findByPropsLazy } from "@webpack"; import { - Button, RestAPI, useEffect, useRef, @@ -33,7 +27,6 @@ import openVerifyModal from "./VerifyModal"; enum LoginStateType { Idle, Loading, - Camera, } interface Preview { @@ -51,7 +44,6 @@ interface QrModalProps { media: HTMLImageElement | HTMLVideoElement | null, location?: QRCode["location"] ) => Promise; - closeMain: () => void; } type QrModalPropsRef = MutableRefObject; @@ -92,7 +84,6 @@ const verifyUrl = async ( body: { handshake_token: handshake }, }); }, - modalProps.closeMain ); }; @@ -245,7 +236,6 @@ function QrModal(props: ModalProps) { setTimeout(res, 500 + 300 + 300); } }), - closeMain: props.onClose, }); useEffect(() => { @@ -284,243 +274,123 @@ function QrModal(props: ModalProps) { return () => document.removeEventListener("paste", callback); }, [state]); - useEffect(() => { - if (state !== LoginStateType.Camera) return; - - const video = document.createElement("video"); - video.width = 1280; - video.height = 720; - const canvas = document.createElement("canvas"); - canvas.width = video.width; - canvas.height = video.height; - const ctx = canvas.getContext("2d", { willReadFrequently: true })!; - - let stream: MediaStream; - let snapshotTimeout: number; - let stopped = false; - - const stop = (stream: MediaStream) => ( - stream.getTracks().forEach(track => track.stop()), - modalProps.current.exit(null) - ); - - const snapshot = () => { - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - const { data, width, height } = ctx.getImageData( - 0, - 0, - canvas.width, - canvas.height - ); - const code = jsQR(data, width, height); - - const token = code?.data.match(tokenRegex)?.[1]; - if (token) { - const img = new Image(); - img.addEventListener("load", () => - modalProps.current - .setPreview(img, code.location) - .then( - () => ( - img.remove(), - verifyUrl(token, modalProps).catch(e => - modalProps.current.exit(e?.message) - ) - ) - ) - ); - canvas.toBlob(blob => blob && (img.src = URL.createObjectURL(blob))); - } else snapshotTimeout = setTimeout(snapshot, 1e3) as any; - }; - - navigator.mediaDevices - .getUserMedia({ - audio: false, - video: { - deviceId: getVideoDeviceId(), - width: canvas.width, - height: canvas.height, - frameRate: 30, - }, - }) - .then(str => { - if (stopped) return stop(str); - - stream = str; - video.srcObject = str; - video.addEventListener("loadedmetadata", () => { - if (stopped) return stop(str); - - video.play(); - modalProps.current.setPreview(video); - snapshot(); - }); - }) - .catch(() => setState(LoginStateType.Idle)); - - return () => { - stopped = true; - clearTimeout(snapshotTimeout); - if (stream) stop(stream); - - video.remove(); - canvas.remove(); - }; - }, [state]); - return ( - - - - {getIntlMessage("USER_SETTINGS_SCAN_QR_CODE")} - - - -
- state === LoginStateType.Idle && inputRef.current?.click() - } - onDragEnter={e => - e.currentTarget.classList.add(cl("modal-filepaste-drop")) - } - onDragLeave={e => - e.currentTarget.classList.remove(cl("modal-filepaste-drop")) - } - onDrop={e => { - e.preventDefault(); - e.currentTarget.classList.remove(cl("modal-filepaste-drop")); +
+
+ state === LoginStateType.Idle && inputRef.current?.click() + } + onDragEnter={e => + e.currentTarget.classList.add(cl("modal-filepaste-drop")) + } + onDragLeave={e => + e.currentTarget.classList.remove(cl("modal-filepaste-drop")) + } + onDrop={e => { + e.preventDefault(); + e.currentTarget.classList.remove(cl("modal-filepaste-drop")); - if (state !== LoginStateType.Idle) return; + if (state !== LoginStateType.Idle) return; - for (const item of e.dataTransfer.files) { - if (item.type.startsWith("image/")) { - setState(LoginStateType.Loading); - handleProcessImage(item, modalProps); - break; - } + for (const item of e.dataTransfer.files) { + if (item.type.startsWith("image/")) { + setState(LoginStateType.Loading); + handleProcessImage(item, modalProps); + break; } - }} - role="button" - style={ - preview?.size - ? { - width: `${preview.size.width}rem`, - height: `${preview.size.height}rem`, - } - : {} } - > - {preview?.source ? ( -
i + x, 0) / - preview.crosses.length - - 50)}%` - : undefined, - ["--offset-y" as any]: preview.crosses - ? `${-(preview.crosses.reduce((i, { y }) => i + y, 0) / - preview.crosses.length - - 50)}%` - : undefined, - }} - className={cl(preview?.crosses && "preview-crosses")} - > - {preview.source} - {preview.crosses?.map(({ x, y, rot, size }) => ( - - - - ))} -
- ) : state === LoginStateType.Loading ? ( - - ) : error.current ? ( - - {error.current} - - ) : state === LoginStateType.Camera ? ( + }} + role="button" + style={ + preview?.size + ? { + width: `${preview.size.width}rem`, + height: `${preview.size.height}rem`, + } + : {} + } + > + {preview?.source ? ( +
i + x, 0) / + preview.crosses.length - + 50)}%` + : undefined, + ["--offset-y" as any]: preview.crosses + ? `${-(preview.crosses.reduce((i, { y }) => i + y, 0) / + preview.crosses.length - + 50)}%` + : undefined, + }} + className={cl(preview?.crosses && "preview-crosses")} + > + {preview.source} + {preview.crosses?.map(({ x, y, rot, size }) => ( + + + + ))} +
+ ) : state === LoginStateType.Loading ? ( + + ) : error.current ? ( + + {error.current} + + ) : ( + <> - Scanning... + Drag and drop an image here, or click to select an image - ) : ( - <> - - Drag and drop an image here, or click to select an image - - - Or paste an image from your clipboard! - -
- - - )} -
- { - if (!e.target.files || state !== LoginStateType.Idle) return; + + Or paste an image from your clipboard! + +
+ + + )} +
+ { + if (!e.target.files || state !== LoginStateType.Idle) return; - for (const item of e.target.files) { - if (item.type.startsWith("image/")) { - setState(LoginStateType.Loading); - handleProcessImage(item, modalProps); - break; - } + for (const item of e.target.files) { + if (item.type.startsWith("image/")) { + setState(LoginStateType.Loading); + handleProcessImage(item, modalProps); + break; } - }} - ref={inputRef} - style={{ display: "none" }} - /> - - - + } + }} + ref={inputRef} + style={{ display: "none" }} + /> +
); } -export default function openQrModal() { - return openModal(props => ); -} +export default wrapTab(QrModal, "Scan QR Code"); diff --git a/src/equicordplugins/loginWithQR/ui/modals/VerifyModal.tsx b/src/equicordplugins/loginWithQR/ui/modals/VerifyModal.tsx index 77acc84ef..f988367a5 100644 --- a/src/equicordplugins/loginWithQR/ui/modals/VerifyModal.tsx +++ b/src/equicordplugins/loginWithQR/ui/modals/VerifyModal.tsx @@ -37,12 +37,10 @@ enum VerifyState { function VerifyModal({ token, onAbort, - closeMain, ...props }: { token: string | null; onAbort: () => void; - closeMain: () => void; } & ModalProps) { const [state, setState] = useState( !token ? VerifyState.NotFound : VerifyState.Verifying @@ -77,7 +75,6 @@ function VerifyModal({ }, }) .then(() => { - closeMain(); setState(VerifyState.LoggedIn); }) .catch(() => setState(VerifyState.NotFound)) @@ -229,14 +226,12 @@ function VerifyModal({ export default function openVerifyModal( token: string | null, onAbort: () => void, - closeMain: () => void ) { return openModal(props => ( )); } diff --git a/src/equicordplugins/loginWithQR/ui/styles.css b/src/equicordplugins/loginWithQR/ui/styles.css index c449eb057..50b6cee9c 100644 --- a/src/equicordplugins/loginWithQR/ui/styles.css +++ b/src/equicordplugins/loginWithQR/ui/styles.css @@ -1,7 +1,3 @@ -.qrlogin-modal-header { - padding: 16px; -} - .qrlogin-modal-filepaste { width: 30rem; height: 13rem; @@ -67,11 +63,6 @@ opacity: 0; } -.qrlogin-modal-button { - margin: 1rem 0; - width: 100%; -} - .qrlogin-device-content { padding: 24px; display: flex; @@ -111,6 +102,10 @@ gap: 8px; } +.qrlogin-modal-container { + margin-left: 100px; +} + [data-tab-id="Scan QR Code"]::before { /* stylelint-disable-next-line property-no-vendor-prefix */ -webkit-mask: var(--si-scan-qr-code) center/contain no-repeat !important; @@ -133,4 +128,4 @@ to { transform: translate(var(--offset-x), var(--offset-y)) scale(var(--scale)); } -} \ No newline at end of file +}