diff --git a/src/Router.tsx b/src/Router.tsx index 22b7e148d..a08c741f2 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -66,10 +66,10 @@ const router = createBrowserRouter([ + {!isTauri() && import.meta.env.PROD && } - {!isTauri() && import.meta.env.PROD && } ), children: [ diff --git a/src/components/DownloadAppBanner.tsx b/src/components/DownloadAppBanner.tsx index 26f3eef96..3e66e0992 100644 --- a/src/components/DownloadAppBanner.tsx +++ b/src/components/DownloadAppBanner.tsx @@ -1,12 +1,13 @@ import { Dialog } from '@headlessui/react' -import { useStore } from '../useStore' import { ActionButton } from './ActionButton' +import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useState } from 'react' const DownloadAppBanner = () => { - const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({ - isBannerDismissed: s.isBannerDismissed, - setBannerDismissed: s.setBannerDismissed, - })) + const { settings } = useSettingsAuthContext() + const [isBannerDismissed, setIsBannerDismissed] = useState( + settings.context.app.dismissWebBanner.current + ) return ( { setBannerDismissed(true)} + onClick={() => setIsBannerDismissed(true)} icon={{ icon: 'close', className: 'p-1', @@ -51,6 +52,24 @@ const DownloadAppBanner = () => { {' '} to download the app for the best experience.

+

+ If you're on Linux and the browser is your only way to use the app, + you can permanently dismiss this banner by{' '} + { + setIsBannerDismissed(true) + settings.send({ + type: 'set.app.dismissWebBanner', + data: { level: 'user', value: true }, + }) + }} + href="/" + className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline" + > + toggling the App > Dismiss Web Banner setting + + . +

diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index 7253fe90c..6a71cf089 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -150,7 +150,16 @@ function ProjectMenuPopover({ closePanel={close} /> ) : ( -
+
+

+ In the browser version of Modeling App you can only have one + part, and the code is stored in your browser's storage. +

+

+ Please save any code you want to keep more permanently, as + your browser's storage is not guaranteed to be permanent. +

+
)}
({ defaultValue: '', validate: (v) => typeof v === 'string', + hideOnPlatform: 'both', + }), + /** Permanently dismiss the banner warning to download the desktop app. */ + dismissWebBanner: new Setting({ + defaultValue: false, + description: + 'Permanently dismiss the banner warning to download the desktop app.', + validate: (v) => typeof v === 'boolean', + hideOnPlatform: 'desktop', }), projectDirectory: new Setting({ defaultValue: '', diff --git a/src/lib/settings/settingsTypes.ts b/src/lib/settings/settingsTypes.ts index 45d06d999..990c74719 100644 --- a/src/lib/settings/settingsTypes.ts +++ b/src/lib/settings/settingsTypes.ts @@ -86,7 +86,7 @@ export interface SettingProps { * Whether to hide the setting on a certain platform. * This will be applied in both the settings panel and the command bar. */ - hideOnPlatform?: 'web' | 'desktop' + hideOnPlatform?: 'web' | 'desktop' | 'both' /** * A React component to use for the setting in the settings panel. * If this is not provided but a commandConfig is, the `inputType` diff --git a/src/lib/settings/settingsUtils.ts b/src/lib/settings/settingsUtils.ts index 119547be9..de958e353 100644 --- a/src/lib/settings/settingsUtils.ts +++ b/src/lib/settings/settingsUtils.ts @@ -139,16 +139,14 @@ export function setSettingsAtLevel( Object.entries(newSettings).forEach(([category, settingsCategory]) => { const categoryKey = category as keyof typeof settings if (!allSettings[categoryKey]) return // ignore unrecognized categories - Object.entries(settingsCategory).forEach( - ([settingKey, settingValue]: [string, Setting]) => { - // TODO: How do you get a valid type for allSettings[categoryKey][settingKey]? - // it seems to always collapses to `never`, which is not correct - // @ts-ignore - if (!allSettings[categoryKey][settingKey]) return // ignore unrecognized settings - // @ts-ignore - allSettings[categoryKey][settingKey][level] = settingValue as unknown - } - ) + Object.entries(settingsCategory).forEach(([settingKey, settingValue]) => { + // TODO: How do you get a valid type for allSettings[categoryKey][settingKey]? + // it seems to always collapses to `never`, which is not correct + // @ts-ignore + if (!allSettings[categoryKey][settingKey]) return // ignore unrecognized settings + // @ts-ignore + allSettings[categoryKey][settingKey][level] = settingValue as unknown + }) }) return allSettings @@ -165,8 +163,42 @@ export function shouldHideSetting( ) { return ( setting.hideOnLevel === settingsLevel || + setting.hideOnPlatform === 'both' || (setting.hideOnPlatform && isTauri() ? setting.hideOnPlatform === 'desktop' : setting.hideOnPlatform === 'web') ) } + +/** + * Returns true if the setting meets the requirements + * to appear in the settings modal in this context + * based on its config, the current settings level, + * and the current platform + */ +export function shouldShowSettingInput( + setting: Setting, + settingsLevel: SettingsLevel +) { + return ( + !shouldHideSetting(setting, settingsLevel) && + (setting.Component || + ['string', 'boolean'].some((t) => typeof setting.default === t) || + (setting.commandConfig?.inputType && + ['string', 'options', 'boolean'].some( + (t) => setting.commandConfig?.inputType === t + ))) + ) +} + +/** + * Get the appropriate input type to show given a + * command's config. Highly dependent on the filtering logic from + * shouldShowSettingInput being applied + */ +export function getSettingInputType(setting: Setting) { + if (setting.Component) return 'component' + if (setting.commandConfig) + return setting.commandConfig.inputType as 'string' | 'options' | 'boolean' + return typeof setting.default as 'string' | 'boolean' +} diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 988d41605..78744ab42 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -31,7 +31,11 @@ import { Event } from 'xstate' import { Dialog, RadioGroup, Transition } from '@headlessui/react' import { CustomIcon, CustomIconName } from 'components/CustomIcon' import Tooltip from 'components/Tooltip' -import { shouldHideSetting } from 'lib/settings/settingsUtils' +import { + getSettingInputType, + shouldHideSetting, + shouldShowSettingInput, +} from 'lib/settings/settingsUtils' export const Settings = () => { const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown' @@ -235,9 +239,7 @@ export const Settings = () => { // Filter out settings that don't have a Component or inputType // or are hidden on the current level or the current platform (item: [string, Setting]) => - !shouldHideSetting(item[1], settingsLevel) && - (item[1].Component || - item[1].commandConfig?.inputType) + shouldShowSettingInput(item[1], settingsLevel) ) .map(([settingName, s]) => { const setting = s as Setting @@ -484,26 +486,28 @@ function GeneratedSetting({ ) : [] }, [setting, settingsLevel, context]) + const inputType = getSettingInputType(setting) - if (setting.Component) - return ( - { - if ('value' in e.target) { - send({ - type: `set.${category}.${settingName}`, - data: { - level: settingsLevel, - value: e.target.value, - }, - } as unknown as Event) - } - }} - /> - ) - - switch (setting.commandConfig?.inputType) { + switch (inputType) { + case 'component': + return ( + setting.Component && ( + { + if ('value' in e.target) { + send({ + type: `set.${category}.${settingName}`, + data: { + level: settingsLevel, + value: e.target.value, + }, + } as unknown as Event) + } + }} + /> + ) + ) case 'boolean': return ( void - isBannerDismissed: boolean - setBannerDismissed: (isBannerDismissed: boolean) => void openPanes: PaneType[] setOpenPanes: (panes: PaneType[]) => void homeMenuItems: { @@ -150,8 +148,6 @@ export const useStore = create()( defaultDir: { dir: '', }, - isBannerDismissed: false, - setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }), openPanes: ['code'], setOpenPanes: (openPanes) => set({ openPanes }), showHomeMenu: true,