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 (
+
+ 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,