diff --git a/src/components/native/Titlebar.tsx b/src/components/native/Titlebar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..14a5c4769ede2d107dc4d0e91c3c0518cbc83cb4 --- /dev/null +++ b/src/components/native/Titlebar.tsx @@ -0,0 +1,72 @@ +import { X, Minus, Square } from "@styled-icons/boxicons-regular"; +import styled from "styled-components"; + +import wideSVG from "../../assets/wide.svg"; + +export const TITLEBAR_HEIGHT = "24px"; +export const USE_TITLEBAR = window.isNative && !window.native.getConfig().frame; + +const TitlebarBase = styled.div` + height: ${TITLEBAR_HEIGHT}; + + display: flex; + user-select: none; + align-items: center; + + .title { + flex-grow: 1; + -webkit-app-region: drag; + + font-size: 16px; + font-weight: 600; + margin-left: 4px; + + img { + width: 60px; + } + } + + .actions { + z-index: 100; + display: flex; + align-items: center; + + div { + width: 24px; + height: 24px; + + display: grid; + place-items: center; + transition: 0.2s ease background-color; + + &:hover { + background: var(--primary-background); + } + + &.error:hover { + background: var(--error); + } + } + } +`; + +export function Titlebar() { + return ( + <TitlebarBase> + <span class="title"> + <img src={wideSVG} /> + </span> + <div class="actions"> + <div onClick={window.native.min}> + <Minus size={20} /> + </div> + <div onClick={window.native.max}> + <Square size={14} /> + </div> + <div onClick={window.native.close} class="error"> + <X size={20} /> + </div> + </div> + </TitlebarBase> + ); +} diff --git a/src/globals.d.ts b/src/globals.d.ts index b6014534902a6b599b49abcb184c18d3ec03cd18..b8e76fe9d1ca87344b691ca451c3f0d4fc185d2e 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -7,12 +7,20 @@ type NativeConfig = { declare interface Window { isNative?: boolean; + nativeVersion: string; native: { + min(); + max(); close(); reload(); + relaunch(); getConfig(): NativeConfig; setFrame(frame: boolean); setBuild(build: Build); + + getAutoStart(): Promise<boolean>; + enableAutoStart(): Promise<void>; + disableAutoStart(): Promise<void>; }; } diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx index 330015d3b4dea55aa0cbb1533770427d7828be65..4af34dbdda734766963e79effcef95c503c7d941 100644 --- a/src/pages/RevoltApp.tsx +++ b/src/pages/RevoltApp.tsx @@ -10,6 +10,7 @@ import Notifications from "../context/revoltjs/Notifications"; import StateMonitor from "../context/revoltjs/StateMonitor"; import SyncManager from "../context/revoltjs/SyncManager"; +import { TITLEBAR_HEIGHT, USE_TITLEBAR } from "../components/native/Titlebar"; import BottomNavigation from "../components/navigation/BottomNavigation"; import LeftSidebar from "../components/navigation/LeftSidebar"; import RightSidebar from "../components/navigation/RightSidebar"; @@ -42,83 +43,89 @@ export default function App() { path.includes("/settings"); return ( - <OverlappingPanels - width="100vw" - height="var(--app-height)" - leftPanel={ - inSpecial - ? undefined - : { width: 292, component: <LeftSidebar /> } - } - rightPanel={ - !inSpecial && inChannel - ? { width: 240, component: <RightSidebar /> } - : undefined - } - bottomNav={{ - component: <BottomNavigation />, - showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left, - height: 50, - }} - docked={isTouchscreenDevice ? Docked.None : Docked.Left}> - <Routes> - <Switch> - <Route - path="/server/:server/channel/:channel/settings/:page" - component={ChannelSettings} - /> - <Route - path="/server/:server/channel/:channel/settings" - component={ChannelSettings} - /> - <Route - path="/server/:server/settings/:page" - component={ServerSettings} - /> - <Route - path="/server/:server/settings" - component={ServerSettings} - /> - <Route - path="/channel/:channel/settings/:page" - component={ChannelSettings} - /> - <Route - path="/channel/:channel/settings" - component={ChannelSettings} - /> + <> + <OverlappingPanels + width="100vw" + height={ + USE_TITLEBAR + ? `calc(var(--app-height) - ${TITLEBAR_HEIGHT})` + : "var(--app-height)" + } + leftPanel={ + inSpecial + ? undefined + : { width: 292, component: <LeftSidebar /> } + } + rightPanel={ + !inSpecial && inChannel + ? { width: 240, component: <RightSidebar /> } + : undefined + } + bottomNav={{ + component: <BottomNavigation />, + showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left, + height: 50, + }} + docked={isTouchscreenDevice ? Docked.None : Docked.Left}> + <Routes> + <Switch> + <Route + path="/server/:server/channel/:channel/settings/:page" + component={ChannelSettings} + /> + <Route + path="/server/:server/channel/:channel/settings" + component={ChannelSettings} + /> + <Route + path="/server/:server/settings/:page" + component={ServerSettings} + /> + <Route + path="/server/:server/settings" + component={ServerSettings} + /> + <Route + path="/channel/:channel/settings/:page" + component={ChannelSettings} + /> + <Route + path="/channel/:channel/settings" + component={ChannelSettings} + /> - <Route - path="/channel/:channel/:message" - component={Channel} - /> - <Route - path="/server/:server/channel/:channel/:message" - component={Channel} - /> + <Route + path="/channel/:channel/:message" + component={Channel} + /> + <Route + path="/server/:server/channel/:channel/:message" + component={Channel} + /> - <Route - path="/server/:server/channel/:channel" - component={Channel} - /> - <Route path="/server/:server" /> - <Route path="/channel/:channel" component={Channel} /> + <Route + path="/server/:server/channel/:channel" + component={Channel} + /> + <Route path="/server/:server" /> + <Route path="/channel/:channel" component={Channel} /> - <Route path="/settings/:page" component={Settings} /> - <Route path="/settings" component={Settings} /> + <Route path="/settings/:page" component={Settings} /> + <Route path="/settings" component={Settings} /> - <Route path="/dev" component={Developer} /> - <Route path="/friends" component={Friends} /> - <Route path="/open/:id" component={Open} /> - <Route path="/invite/:code" component={Invite} /> - <Route path="/" component={Home} /> - </Switch> - </Routes> - <ContextMenus /> - <Popovers /> - <Notifications /> - <StateMonitor /> - <SyncManager /> - </OverlappingPanels> + <Route path="/dev" component={Developer} /> + <Route path="/friends" component={Friends} /> + <Route path="/open/:id" component={Open} /> + <Route path="/invite/:code" component={Invite} /> + <Route path="/" component={Home} /> + </Switch> + </Routes> + <ContextMenus /> + <Popovers /> + <Notifications /> + <StateMonitor /> + <SyncManager /> + </OverlappingPanels> + </> ); } diff --git a/src/pages/app.tsx b/src/pages/app.tsx index 57badde4efa7f4d235d59ad187a247c16942f0fc..f66f6d16b69b99a023811883e64f637801b299b0 100644 --- a/src/pages/app.tsx +++ b/src/pages/app.tsx @@ -8,6 +8,8 @@ import { CheckAuth } from "../context/revoltjs/CheckAuth"; import Masks from "../components/ui/Masks"; import Preloader from "../components/ui/Preloader"; +import { Titlebar, USE_TITLEBAR } from "../components/native/Titlebar"; + const Login = lazy(() => import("./login/Login")); const RevoltApp = lazy(() => import("./RevoltApp")); @@ -15,6 +17,7 @@ export function App() { return ( <Context> <Masks /> + {USE_TITLEBAR && <Titlebar />} {/* // @ts-expect-error */} <Suspense fallback={<Preloader type="spinner" />}> diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 5cf7efe9676f7ea7fccef77e3c6ea0388d955f26..c5911dd4b4df0b42bac0ef9da3c82729ac7c82ba 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -210,6 +210,9 @@ export default function Settings() { {GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "} {APP_VERSION} </span> + {window.isNative && ( + <span>Native: {window.nativeVersion}</span> + )} <span> API: {client.configuration?.revolt ?? "N/A"} </span> diff --git a/src/pages/settings/panes/Native.tsx b/src/pages/settings/panes/Native.tsx index 52c472b01553261a79e03145b6f5eaab90799398..2d3b3c56b5e7b7abbd5efcc4817415af6c2bfdb2 100644 --- a/src/pages/settings/panes/Native.tsx +++ b/src/pages/settings/panes/Native.tsx @@ -1,5 +1,8 @@ +import { useEffect, useState } from "preact/hooks"; + import { SyncOptions } from "../../../redux/reducers/sync"; +import Button from "../../../components/ui/Button"; import Checkbox from "../../../components/ui/Checkbox"; interface Props { @@ -7,5 +10,126 @@ interface Props { } export function Native(props: Props) { - return <div></div>; + const [config, setConfig] = useState(window.native.getConfig()); + const [autoStart, setAutoStart] = useState<boolean | undefined>(); + const fetchValue = () => window.native.getAutoStart().then(setAutoStart); + + const [hintReload, setHintReload] = useState(false); + const [hintRelaunch, setHintRelaunch] = useState(false); + const [confirmDev, setConfirmDev] = useState(false); + + useEffect(() => { + fetchValue(); + }, []); + + return ( + <div> + <Checkbox + checked={autoStart ?? false} + disabled={typeof autoStart === "undefined"} + onChange={async (v) => { + if (v) { + await window.native.enableAutoStart(); + } else { + await window.native.disableAutoStart(); + } + + setAutoStart(v); + }} + description="Launch Revolt when you log into your computer."> + Start with computer + </Checkbox> + <Checkbox + checked={!config.frame} + onChange={(frame) => { + window.native.setFrame(!frame); + setHintRelaunch(true); + setConfig({ + ...config, + frame: !frame, + }); + }} + description={<>Let Revolt use its own window frame.</>}> + Custom window frame + </Checkbox> + <Checkbox + checked={config.build === "nightly"} + onChange={(nightly) => { + const build = nightly ? "nightly" : "stable"; + window.native.setBuild(build); + setHintReload(true); + setConfig({ + ...config, + build, + }); + }} + description={<>Use the beta branch of Revolt.</>}> + Revolt Nightly + </Checkbox> + <p style={{ display: "flex", gap: "8px" }}> + <Button + contrast + compact + disabled={!hintReload} + onClick={window.native.reload}> + Reload Page + </Button> + <Button + contrast + compact + disabled={!hintRelaunch} + onClick={window.native.relaunch}> + Reload App + </Button> + </p> + <h3 style={{ marginTop: "4em" }}>Local Development Mode</h3> + {config.build === "dev" ? ( + <p> + <Button + contrast + compact + onClick={() => { + window.native.setBuild("stable"); + window.native.reload(); + }}> + Exit Development Mode + </Button> + </p> + ) : ( + <> + <Checkbox + checked={confirmDev} + onChange={setConfirmDev} + description={ + <> + This will change the app to the 'dev' branch, + instead loading the app from a local server on + your machine. + <br /> + <b> + Without a server running,{" "} + <span style={{ color: "var(--error)" }}> + the app will not load! + </span> + </b> + </> + }> + I understand there's no going back. + </Checkbox> + <p> + <Button + error + compact + disabled={!confirmDev} + onClick={() => { + window.native.setBuild("dev"); + window.native.reload(); + }}> + Enter Development Mode + </Button> + </p> + </> + )} + </div> + ); }