diff --git a/i18n/en-US.messages.d.ts b/i18n/en-US.messages.d.ts index a10b65d73..8ce78b212 100644 --- a/i18n/en-US.messages.d.ts +++ b/i18n/en-US.messages.d.ts @@ -1269,6 +1269,19 @@ export declare const messages: { * Missing translations: `bg`, `da`, `el`, `es-419`, `hi`, `hr`, `lt`, `no`, `ro`, `th` */ 'REPLUGGED_QUICKCSS_CHANGES_APPLY': TypedIntlMessageGetter<{}>, + /** + * Key: `NNLlfn` + * + * ### Definition + * ```text + * You've popped out the editor to another window. + * ``` + * + * ### Problems + * + * Missing translations: `bg`, `cs`, `da`, `de`, `el`, `en-GB`, `es-419`, `es-ES`, `fi`, `fr`, `hi`, `hr`, `hu`, `it`, `ja`, `ko`, `lt`, `nl`, `no`, `pl`, `pt-BR`, `ro`, `ru`, `sv-SE`, `th`, `tr`, `uk`, `vi`, `zh-CN`, `zh-TW` + */ + 'REPLUGGED_QUICKCSS_EDITOR_POPPED_OUT': TypedIntlMessageGetter<{}>, /** * Key: `Nk3LNj` * @@ -1643,7 +1656,7 @@ export declare const messages: { * * ### Problems * - * Missing translations: `bg`, `cs`, `da`, `de`, `el`, `en-GB`, `es-419`, `es-ES`, `fi`, `fr`, `hi`, `hr`, `hu`, `it`, `ja`, `ko`, `lt`, `nl`, `no`, `pl`, `pt-BR`, `ro`, `ru`, `sv-SE`, `th`, `tr`, `uk`, `vi`, `zh-CN`, `zh-TW` + * Missing translations: `bg`, `cs`, `da`, `de`, `el`, `en-GB`, `es-419`, `es-ES`, `fi`, `fr`, `hi`, `hr`, `hu`, `it`, `ja`, `ko`, `lt`, `no`, `pl`, `pt-BR`, `ro`, `ru`, `sv-SE`, `th`, `tr`, `uk`, `vi`, `zh-CN`, `zh-TW` */ 'REPLUGGED_SETTINGS_QUICKCSS_ENABLE': TypedIntlMessageGetter<{}>, /** @@ -1656,7 +1669,7 @@ export declare const messages: { * * ### Problems * - * Missing translations: `bg`, `cs`, `da`, `de`, `el`, `en-GB`, `es-419`, `es-ES`, `fi`, `fr`, `hi`, `hr`, `hu`, `it`, `ja`, `ko`, `lt`, `nl`, `no`, `pl`, `pt-BR`, `ro`, `ru`, `sv-SE`, `th`, `tr`, `uk`, `vi`, `zh-CN`, `zh-TW` + * Missing translations: `bg`, `cs`, `da`, `de`, `el`, `en-GB`, `es-419`, `es-ES`, `fi`, `fr`, `hi`, `hr`, `hu`, `it`, `ja`, `ko`, `lt`, `no`, `pl`, `pt-BR`, `ro`, `ru`, `sv-SE`, `th`, `tr`, `uk`, `vi`, `zh-CN`, `zh-TW` */ 'REPLUGGED_SETTINGS_QUICKCSS_ENABLE_DESC': TypedIntlMessageGetter<{}>, /** diff --git a/i18n/en-US.messages.js b/i18n/en-US.messages.js index d4d8bc85a..f34e273ce 100644 --- a/i18n/en-US.messages.js +++ b/i18n/en-US.messages.js @@ -239,5 +239,6 @@ export default defineMessages({ "REPLUGGED_SETTINGS_DISCORD_DEVTOOLS": "Enable Discord Internal DevTools", "REPLUGGED_SETTINGS_DISCORD_DEVTOOLS_DESC": "Replaces the help button in the title bar with Discord's internal developer tools (different from Chrome DevTools). This setting requires Discord experiments to be enabled first. **Requires restart**.", "REPLUGGED_SETTINGS_QUICKCSS_ENABLE": "Enable Quick CSS", - "REPLUGGED_SETTINGS_QUICKCSS_ENABLE_DESC": "Apply custom styles to Discord instantly. Change colors, layout, and appearance in real time without installing themes." -}); \ No newline at end of file + "REPLUGGED_SETTINGS_QUICKCSS_ENABLE_DESC": "Apply custom styles to Discord instantly. Change colors, layout, and appearance in real time without installing themes.", + "REPLUGGED_QUICKCSS_EDITOR_POPPED_OUT": "You've popped out the editor to another window." +}); diff --git a/src/globals.d.ts b/src/globals.d.ts index 95b0d8364..fb32bb450 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -30,6 +30,9 @@ declare global { relaunch: () => void; }; window: { + close(key: string): void; + maximize(key: string): void; + minimize(key: string): void; setDevtoolsCallbacks(onOpened?: (() => void) | null, onClosed?: (() => void) | null): void; focus(): void; }; diff --git a/src/main/index.ts b/src/main/index.ts index def2b7475..c4e227740 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -50,6 +50,27 @@ class BrowserWindow extends electron.BrowserWindow { super(opts); (this.webContents as RepluggedWebContents).originalPreload = originalPreload; + const defaultWindowOpenHandler = this.webContents.setWindowOpenHandler.bind(this.webContents); + this.webContents.setWindowOpenHandler = (cb) => { + defaultWindowOpenHandler(({ url, frameName, features, ...args }) => { + const ret = cb({ url, frameName, features, ...args }); + if (frameName.startsWith("DISCORD_REPLUGGED") && ret.action === "allow") { + const trafficLightPosition = features.split(",").reduce( + (pos, pair) => { + const [key, value] = pair.split("="); + if (key === "trafficLightPositionX") pos.x = Number(value); + if (key === "trafficLightPositionY") pos.y = Number(value); + return pos; + }, + { x: -1, y: -1 }, + ); + if (trafficLightPosition.x !== -1 && trafficLightPosition.y !== -1) { + ret.overrideBrowserWindowOptions!.trafficLightPosition = trafficLightPosition; + } + } + return ret; + }); + }; } } diff --git a/src/renderer/coremods/settings/icons/Close.tsx b/src/renderer/coremods/settings/icons/Close.tsx new file mode 100644 index 000000000..9b4e9ef59 --- /dev/null +++ b/src/renderer/coremods/settings/icons/Close.tsx @@ -0,0 +1,8 @@ +export default (): React.ReactElement => ( + + + +); diff --git a/src/renderer/coremods/settings/icons/Maximize.tsx b/src/renderer/coremods/settings/icons/Maximize.tsx new file mode 100644 index 000000000..ae4dd2902 --- /dev/null +++ b/src/renderer/coremods/settings/icons/Maximize.tsx @@ -0,0 +1,8 @@ +export default (): React.ReactElement => ( + + + +); diff --git a/src/renderer/coremods/settings/icons/Minimize.tsx b/src/renderer/coremods/settings/icons/Minimize.tsx new file mode 100644 index 000000000..a90b6d6d5 --- /dev/null +++ b/src/renderer/coremods/settings/icons/Minimize.tsx @@ -0,0 +1,8 @@ +export default (): React.ReactElement => ( + + + +); diff --git a/src/renderer/coremods/settings/icons/Pin.tsx b/src/renderer/coremods/settings/icons/Pin.tsx new file mode 100644 index 000000000..1eca1cf1d --- /dev/null +++ b/src/renderer/coremods/settings/icons/Pin.tsx @@ -0,0 +1,8 @@ +export default (): React.ReactElement => ( + + + +); diff --git a/src/renderer/coremods/settings/icons/Popout.tsx b/src/renderer/coremods/settings/icons/Popout.tsx new file mode 100644 index 000000000..84cd76f08 --- /dev/null +++ b/src/renderer/coremods/settings/icons/Popout.tsx @@ -0,0 +1,12 @@ +export default (): React.ReactElement => ( + + + + +); diff --git a/src/renderer/coremods/settings/icons/Preview.tsx b/src/renderer/coremods/settings/icons/Preview.tsx new file mode 100644 index 000000000..45e0ac5b8 --- /dev/null +++ b/src/renderer/coremods/settings/icons/Preview.tsx @@ -0,0 +1,7 @@ +// copied from discord popout +export default (): React.ReactElement => ( + + + + +); diff --git a/src/renderer/coremods/settings/icons/Unpin.tsx b/src/renderer/coremods/settings/icons/Unpin.tsx new file mode 100644 index 000000000..8bcab2e6a --- /dev/null +++ b/src/renderer/coremods/settings/icons/Unpin.tsx @@ -0,0 +1,5 @@ +export default (): React.ReactElement => ( + + + +); diff --git a/src/renderer/coremods/settings/icons/index.ts b/src/renderer/coremods/settings/icons/index.ts index e6c8be8d6..3afe05064 100644 --- a/src/renderer/coremods/settings/icons/index.ts +++ b/src/renderer/coremods/settings/icons/index.ts @@ -1,15 +1,29 @@ +import Close from "./Close"; import Discord from "./Discord"; import GitHub from "./GitHub"; import Link from "./Link"; +import Maximize from "./Maximize"; +import Minimize from "./Minimize"; +import Pin from "./Pin"; +import Popout from "./Popout"; +import Preview from "./Preview"; import Reload from "./Reload"; import Settings from "./Settings"; import Trash from "./Trash"; +import Unpin from "./Unpin"; export default { + Close, Discord, GitHub, Link, + Maximize, + Minimize, + Pin, + Popout, + Preview, Reload, Settings, Trash, + Unpin, }; diff --git a/src/renderer/coremods/settings/pages/QuickCSS.css b/src/renderer/coremods/settings/pages/QuickCSS.css index 2f904de30..8470acb7c 100644 --- a/src/renderer/coremods/settings/pages/QuickCSS.css +++ b/src/renderer/coremods/settings/pages/QuickCSS.css @@ -3,16 +3,100 @@ #replugged-quickcss-wrapper { height: 100%; } +#rp-quickcss-tab { + display: flex; + flex-direction: column; +} + +.platform-win #replugged-quickcss-wrapper { + max-height: calc(100% - var(--custom-app-top-bar-height) - 20px); +} + +#rp-quickcss-tab #replugged-quickcss-wrapper { + height: auto; +} +#replugged-quickcss-wrapper { + max-height: calc(100% - 20px); + background-color: var(--background-base-lower); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); +} + +#replugged-quickcss-wrapper .replugged-quickcss-header h2 { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.platform-osx + .replugged-quickcss-popout-root + #replugged-quickcss-wrapper + .replugged-quickcss-header + h2 { + opacity: 0; +} + +.platform-osx + .replugged-quickcss-popout-root + #replugged-quickcss-wrapper + .replugged-quickcss-popout-navigation-container + replugged-quickcss-popout-navigation-minimize, +.platform-osx + .replugged-quickcss-popout-root + #replugged-quickcss-wrapper + .replugged-quickcss-popout-navigation-container + replugged-quickcss-popout-navigation-maximize, +.platform-osx + .replugged-quickcss-popout-root + #replugged-quickcss-wrapper + .replugged-quickcss-popout-navigation-container + replugged-quickcss-popout-navigation-close { + display: none; +} + +.replugged-quickcss-popout-root { + height: 100%; + width: 100%; +} +.replugged-quickcss-popout-root #replugged-quickcss-wrapper { + max-height: 100%; + border-radius: unset; +} + +.replugged-quickcss-popout-root #replugged-quickcss-wrapper .replugged-quickcss-header { + -webkit-app-region: drag; +} +#replugged-quickcss-wrapper .replugged-quickcss-header { + margin: 0 var(--space-8) 0 var(--space-16); +} +#replugged-quickcss-wrapper .replugged-quickcss-header .replugged-quickcss-header-buttons { + overflow: hidden; + flex-flow: row-reverse; + flex-grow: unset !important; + margin: var(--space-6) 0 var(--space-6); + gap: 5px; + -webkit-app-region: no-drag; +} + #replugged-quickcss-wrapper .cm-editor { max-height: calc(100% - 20px); background: var(--bg-overlay-2, var(--background-base-lower)); - border: 1px solid var(--background-secondary-alt); - border-radius: 4px; + border-top: 1px solid var(--background-secondary-alt); + border-radius: 0 0 var(--radius-sm) var(--radius-sm); outline: none !important; } .platform-win #replugged-quickcss-wrapper .cm-editor { max-height: calc(100% - var(--custom-app-top-bar-height) - 20px); } + +.replugged-quickcss-popout-root #replugged-quickcss-wrapper .cm-editor { + height: 100%; + max-height: calc(100% - 25px); +} +.platform-win .replugged-quickcss-popout-root #replugged-quickcss-wrapper .cm-editor { + max-height: calc(100% - var(--custom-app-top-bar-height) - 25px); +} #replugged-quickcss-wrapper .cm-content { padding: 0px; } @@ -187,3 +271,39 @@ #replugged-quickcss-wrapper .cm-specialChar { color: var(--red-400); } + +#replugged-quickcss-wrapper .replugged-quickcss-popout-navigation-container { + display: flex; + gap: 5px; +} + +#replugged-quickcss-wrapper .replugged-quickcss-popout-navigation-button { + display: flex; + justify-content: center; + align-items: center; + color: var(--interactive-normal); + border: 1px solid var(--button-outline-primary-border); + border-radius: var(--radius-sm); + min-width: 38px; + min-height: 38px; +} +#replugged-quickcss-wrapper .replugged-quickcss-popout-navigation-button > svg { + width: 20px; + height: 20px; +} + +#replugged-quickcss-wrapper .replugged-quickcss-popout-navigation-button:hover { + color: var(--interactive-hover); +} + +#replugged-quickcss-wrapper + .replugged-quickcss-popout-navigation-button.replugged-quickcss-close-popout:active, +#replugged-quickcss-wrapper + .replugged-quickcss-popout-navigation-button.replugged-quickcss-close-popout:hover { + color: var(--status-danger); + border-color: var(--status-danger); +} + +#replugged-quickcss-wrapper .replugged-quickcss-popout-navigation-button:active { + color: var(--interactive-active); +} diff --git a/src/renderer/coremods/settings/pages/QuickCSS.tsx b/src/renderer/coremods/settings/pages/QuickCSS.tsx index 1edda9dcd..f1734be48 100644 --- a/src/renderer/coremods/settings/pages/QuickCSS.tsx +++ b/src/renderer/coremods/settings/pages/QuickCSS.tsx @@ -1,8 +1,8 @@ import { css } from "@codemirror/lang-css"; import { EditorState } from "@codemirror/state"; -import { React, toast } from "@common"; -import { intl } from "@common/i18n"; -import { Button, Divider, Flex, Text } from "@components"; +import { React, flux, fluxDispatcher, toast } from "@common"; +import { t as discordT, intl } from "@common/i18n"; +import { Button, Clickable, Flex, Text, Tooltip } from "@components"; import { webpack } from "@replugged"; import { EditorView, basicSetup } from "codemirror"; import { t } from "src/renderer/modules/i18n"; @@ -10,6 +10,8 @@ import { githubDark, githubLight } from "./codemirror-github"; import { generalSettings } from "./General"; import "./QuickCSS.css"; +import type { Store } from "@common/flux"; +import Icons from "../icons"; interface UseCodeMirrorOptions { value?: string; @@ -23,6 +25,28 @@ interface ThemeModule { removeChangeListener: (listener: () => unknown) => unknown; } +const PopoutWindowStore = await webpack.waitForStore< + { getWindowOpen: (key: string) => boolean; getIsAlwaysOnTop: (key: string) => boolean } & Store +>("PopoutWindowStore", { timeout: 10_000 }); + +const PopoutContext = await webpack + .waitForModule< + Record + >(webpack.filters.bySource("Missing guestWindow reference"), { timeout: 10_000 }) + .then( + (m) => + Object.values(m).find((c) => typeof c === "object") as React.MemoExoticComponent< + React.FC<{ + withTitleBar?: boolean; + windowKey: string; + title?: string; + children: React.ReactElement; + }> + >, + ); + +const WindowKey = "DISCORD_REPLUGGED_QUICKCSS"; + function useTheme(): "light" | "dark" { const [theme, setTheme] = React.useState<"light" | "dark">("dark"); @@ -110,14 +134,68 @@ function useCodeMirror({ value: initialValueParam, onChange, container }: UseCod return { value, setValue: customSetValue }; } -export const QuickCSS = (): React.ReactElement => { +const NavigationButtons = ({ windowKey }: { windowKey: string }): React.ReactElement => { + const isAlwaysOnTop = flux.useStateFromStores([PopoutWindowStore], () => + PopoutWindowStore.getIsAlwaysOnTop(WindowKey), + ); + return ( + + + { + fluxDispatcher.dispatch({ + type: "POPOUT_WINDOW_SET_ALWAYS_ON_TOP", + alwaysOnTop: !isAlwaysOnTop, + key: windowKey, + }); + }} + className="replugged-quickcss-popout-navigation-button"> + {isAlwaysOnTop ? : } + + + + { + DiscordNative.window.minimize(windowKey); + }} + className="replugged-quickcss-popout-navigation-button"> + + + + + { + DiscordNative.window.maximize(windowKey); + }} + className="replugged-quickcss-popout-navigation-button"> + + + + + { + DiscordNative.window.close(windowKey); + }} + className="replugged-quickcss-popout-navigation-button replugged-quickcss-close-popout"> + + + + + ); +}; + +const QuickCSSPanel = ({ isPopout }: { isPopout?: boolean }): React.ReactElement => { const ref = React.useRef(null); const { value, setValue } = useCodeMirror({ container: ref.current, }); const [ready, setReady] = React.useState(false); - const autoApply = generalSettings.get("autoApplyQuickCss"); + const autoApply = generalSettings.useValue("autoApplyQuickCss"); const reload = (): void => window.replugged.quickCSS.reload(); const reloadAndToast = (): void => { @@ -125,6 +203,32 @@ export const QuickCSS = (): React.ReactElement => { toast.toast(intl.string(t.REPLUGGED_TOAST_QUICKCSS_RELOAD)); }; + const openPopout = (): void => { + fluxDispatcher.dispatch({ + type: "POPOUT_WINDOW_OPEN", + key: WindowKey, + features: { + frame: false, + menubar: false, + toolbar: false, + location: false, + directories: false, + trafficLightPositionX: 16, + trafficLightPositionY: 20, + }, + render: () => ( + +
+ +
+
+ ), + }); + }; + React.useEffect(() => { void window.RepluggedNative.quickCSS.get().then((val: string) => { setValue(val); @@ -138,24 +242,24 @@ export const QuickCSS = (): React.ReactElement => { e.preventDefault(); reloadAndToast(); } + if (e.key === "Escape" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) { + fluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } }; - window.addEventListener("keydown", listener); + const toggleKeybinds = (enabled: boolean): void => { + fluxDispatcher.dispatch({ + type: "KEYBINDS_ENABLE_ALL_KEYBINDS", + enable: isPopout || enabled, + }); + }; - // This is the best way I could come up with to not show the sticker picker when CTRL + S is pressed - // We want it to only be active when this tab is active - const hideStickerPickerCss = ` - [class*="positionLayer-"] { - display: none; - } - `; - const style = document.createElement("style"); - style.innerText = hideStickerPickerCss; - document.head.appendChild(style); + window.addEventListener("keydown", listener); + toggleKeybinds(false); return () => { window.removeEventListener("keydown", listener); - document.head.removeChild(style); + toggleKeybinds(true); }; }, []); @@ -172,24 +276,53 @@ export const QuickCSS = (): React.ReactElement => { return ( <> - - {intl.string(t.REPLUGGED_QUICKCSS)} -
- {autoApply ? null : ( - - )} - -
+ {autoApply ? null : ( + + )} +
+ + + + ); +}; + +export const QuickCSS = (): React.ReactElement => { + const isPopoutOpen = flux.useStateFromStores([PopoutWindowStore], () => + PopoutWindowStore.getWindowOpen(WindowKey), + ); + return isPopoutOpen ? ( + <> + + {intl.string(t.REPLUGGED_QUICKCSS_EDITOR_POPPED_OUT)} - -
+ ) : ( + ); };