diff --git a/src/Common/ContextProviders.luau b/src/Common/ContextProviders.luau index 430571ed..4d862c5e 100644 --- a/src/Common/ContextProviders.luau +++ b/src/Common/ContextProviders.luau @@ -2,6 +2,7 @@ local React = require("@pkg/React") local TreeView = require("@root/TreeView") local ContextStack = require("@root/Common/ContextStack") +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local NavigationContext = require("@root/Navigation/NavigationContext") local PluginContext = require("@root/Plugin/PluginContext") local SettingsContext = require("@root/UserSettings/SettingsContext") @@ -20,6 +21,7 @@ local function ContextProviders(props: Props) React.createElement(NavigationContext.Provider, { defaultScreen = "Home", }), + React.createElement(LocalStorageContext.Provider), React.createElement(SettingsContext.Provider), React.createElement(TreeView.TreeViewProvider), }, diff --git a/src/Plugin/LocalStorageContext.luau b/src/Plugin/LocalStorageContext.luau new file mode 100644 index 00000000..f1e1e84e --- /dev/null +++ b/src/Plugin/LocalStorageContext.luau @@ -0,0 +1,99 @@ +local HttpService = game:GetService("HttpService") + +local React = require("@pkg/React") +local Sift = require("@pkg/Sift") + +local PluginContext = require("@root/Plugin/PluginContext") +local usePrevious = require("@root/Common/usePrevious") + +local useCallback = React.useCallback +local useContext = React.useContext +local useEffect = React.useEffect +local useMemo = React.useMemo +local useState = React.useState + +export type LocalStorage = { + [string]: unknown, +} + +export type LocalStorageContext = { + get: (key: string) -> unknown, + set: (key: string, value: unknown) -> (), +} + +local LocalStorageContext = React.createContext(nil :: LocalStorageContext?) + +export type Props = { + storageKey: string?, + children: React.Node, +} + +local function LocalStorageProvider(props: Props) + local plugin = useContext(PluginContext.Context) + + local storageKey = useMemo(function() + return if props.storageKey then props.storageKey else `{plugin.Name}LocalStorage` + end, { props.storageKey, plugin } :: { unknown }) + + local loadFromDisk = useCallback(function(): LocalStorage + local data = plugin:GetSetting(storageKey) + if data then + local json = HttpService:JSONDecode(data) + if json then + return json + end + end + return {} + end, { plugin } :: { unknown }) + + local storage, setStorage = useState(loadFromDisk) + local prevStorage = usePrevious(storage) + + local saveToDisk = useCallback(function() + local data = HttpService:JSONEncode(storage) + if data then + plugin:SetSetting(storageKey, data) + end + end, { plugin, storageKey, storage } :: { unknown }) + + local get = useCallback(function(key: string) + return storage[key] + end, { storage }) + + local set = useCallback(function(key: string, value: any) + setStorage(function(prev) + return Sift.Dictionary.join(prev, { + [key] = if typeof(value) == "function" then value(prev[key]) else value, + }) + end) + end, { storage }) + + useEffect(function() + if storage and storage ~= prevStorage then + saveToDisk() + end + end, { storage, prevStorage, saveToDisk } :: { unknown }) + + local context: LocalStorageContext = { + get = get, + set = set, + } + + return React.createElement(LocalStorageContext.Provider, { + value = context, + }, props.children) +end + +local function useLocalStorage(): LocalStorageContext + local context = useContext(LocalStorageContext) + if not context then + local contextName = script.Name + error(`failed to use {contextName}, is \`{contextName}.Provider\` defined in the React hierarchy?`) + end + return context +end + +return { + Provider = LocalStorageProvider, + use = useLocalStorage, +} diff --git a/src/Storybook/useLastOpenedStory.luau b/src/Storybook/useLastOpenedStory.luau index bd9e1caa..45db8d0d 100644 --- a/src/Storybook/useLastOpenedStory.luau +++ b/src/Storybook/useLastOpenedStory.luau @@ -1,31 +1,33 @@ local React = require("@pkg/React") -local PluginContext = require("@root/Plugin/PluginContext") +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local SettingsContext = require("@root/UserSettings/SettingsContext") local getInstanceFromFullName = require("@root/Common/getInstanceFromFullName") -local useContext = React.useContext local useCallback = React.useCallback local useMemo = React.useMemo -local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) - local plugin = useContext(PluginContext.Context) +local REMEMBER_LAST_OPENED_STORY_KEY = "rememberLastOpenedStory" +local LAST_OPENED_STORY_PATH_KEY = "lastOpenedStoryPath" +local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript?) -> ()) + local localStorage = LocalStorageContext.use() local settingsContext = SettingsContext.use() - local rememberLastOpenedStory = settingsContext.getSetting("rememberLastOpenedStory") local setLastOpenedStory = useCallback(function(storyModule: ModuleScript?) - plugin:SetSetting("lastOpenedStoryPath", if storyModule then storyModule:GetFullName() else nil) - end, { plugin }) + localStorage.set(LAST_OPENED_STORY_PATH_KEY, if storyModule then storyModule:GetFullName() else nil) + end, { localStorage }) local lastOpenedStory = useMemo(function(): ModuleScript? + local rememberLastOpenedStory = settingsContext.getSetting(REMEMBER_LAST_OPENED_STORY_KEY) + if not rememberLastOpenedStory then return nil end - local lastOpenedStoryPath = plugin:GetSetting("lastOpenedStoryPath") + local lastOpenedStoryPath = localStorage.get(LAST_OPENED_STORY_PATH_KEY) - if lastOpenedStoryPath then + if lastOpenedStoryPath and typeof(lastOpenedStoryPath) == "string" then local instance = getInstanceFromFullName(lastOpenedStoryPath) if instance and instance:IsA("ModuleScript") then @@ -34,7 +36,7 @@ local function useLastOpenedStory(): (ModuleScript?, (storyModule: ModuleScript? end return nil - end, { rememberLastOpenedStory, plugin }) + end, { settingsContext, localStorage } :: { unknown }) return lastOpenedStory, setLastOpenedStory end diff --git a/src/UserSettings/SettingsContext.luau b/src/UserSettings/SettingsContext.luau index 231da6e5..4f76a857 100644 --- a/src/UserSettings/SettingsContext.luau +++ b/src/UserSettings/SettingsContext.luau @@ -1,15 +1,16 @@ local React = require("@pkg/React") local Sift = require("@pkg/Sift") -local PluginContext = require("@root/Plugin/PluginContext") +local LocalStorageContext = require("@root/Plugin/LocalStorageContext") local defaultSettings = require("./defaultSettings") local useCallback = React.useCallback -local useContext = React.useContext local useState = React.useState type Settings = defaultSettings.Settings +local USER_SETTINGS_KEY = "userSettings" + local Context = React.createContext({} :: SettingsContext) export type Props = { @@ -26,19 +27,23 @@ export type SettingsContext = { } local function Provider(props: Props) - local plugin = useContext(PluginContext.Context) + local localStorage = LocalStorageContext.use() + + local userSettings, setUserSettings = useState(function() + return localStorage.get(USER_SETTINGS_KEY) or {} + end) local loadSettingsFromDisk = useCallback(function() local settings = {} for settingName, setting in pairs(defaultSettings) do - local savedValue = plugin:GetSetting(settingName) + local savedValue = userSettings[settingName] settings[settingName] = Sift.Dictionary.join(setting, { value = savedValue, }) end return settings - end, { plugin }) + end, { userSettings }) local settings, setSettings = useState(function() return loadSettingsFromDisk() @@ -57,18 +62,24 @@ local function Provider(props: Props) local isSettingDefault = useCallback(function(settingName: string) local defaultValue = getSettingDefault(settingName) - local savedValue = plugin:GetSetting(settingName) + local savedValue = userSettings[settingName] return savedValue == nil or savedValue == defaultValue - end, { plugin, getSettingDefault } :: { unknown }) + end, { userSettings, getSettingDefault } :: { unknown }) local setSetting = useCallback(function(settingName: string, newValue: any) if newValue == nil then newValue = getSettingDefault(settingName) end - plugin:SetSetting(settingName, newValue) + + setUserSettings(function(prev) + return Sift.Dictionary.join(prev, { + [settingName] = newValue, + }) + end) + setSettings(loadSettingsFromDisk) - end, { plugin, loadSettingsFromDisk, getSettingDefault } :: { unknown }) + end, { loadSettingsFromDisk, getSettingDefault } :: { unknown }) local getSetting = useCallback(function(settingName: string) return settings[settingName].value