diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c7f1db..c034f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ * Removed "style-disabled" property and GetDisabledStyle function * Added GoogleFonts field to AppParams * Added functions: GetWhiteSpace, GetWordBreak, ScrollIntoViewIfNeeded -* Added Popups, PopupDefault, PopupDefaultsSeq, and SetPopupDefaults methods to Session interface +* Added ClientSession interface +* Added ClientSession, Popups, PopupDefault, PopupDefaultsSeq, and SetPopupDefaults methods to Session interface +* Removed ClientItem, SetClientItem, RemoveClientItem, and RemoveAllClientItems methods from Session interface * Added DismissWithoutAnimation add SetHotKey methods to Popup interface * Added "outside-color" add "outside-filter" properties to Popup interface * Added ViewSeq add ViewCount methods to ParentView interface diff --git a/app_scripts.js b/app_scripts.js index 229083e..54b4f76 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -56,6 +56,7 @@ function sessionInfo() { message += ",pixel-ratio=" + pixelRatio; } + /* if (localStorage.length > 0) { message += ",storage=" lead = "_{" @@ -73,7 +74,8 @@ function sessionInfo() { } message += "}" } - + */ + return message + "}"; } @@ -1984,6 +1986,38 @@ function getCanvasContext(elementId) { return null; } +function localStorageGet(request, keys) { + const key_set = keys.split(","); + var result = ""; + for (const key of key_set) { + result += key; + result += ":"; + try { + let value = localStorage.getItem(key); + if (value) { + result += value; + } + } catch (err) {} + result += ";"; + } + sendMessage("storageValues{session=" + sessionID + ", request=" + request + ", values=`" + result + "`}") +} + +function localStorageGetAll(request) { + var result = ""; + for (let i = 0; i < localStorage.length; i++) { + let key = localStorage.key(i); + let value = localStorage.getItem(key); + if (value) { + result += key; + result += ":"; + result += value; + result += ";"; + } + } + sendMessage("storageValues{session=" + sessionID + ", request=" + request + ", values=`" + result + "`}") +} + function localStorageSet(key, value) { try { localStorage.setItem(key, value); diff --git a/clientStorage.go b/clientStorage.go new file mode 100644 index 0000000..040c8e8 --- /dev/null +++ b/clientStorage.go @@ -0,0 +1,155 @@ +package rui + +import ( + "encoding/base64" + "strconv" + "strings" +) + +// ClientStorage is an interface for accessing client-side key-value storage. +type ClientStorage interface { + + // Request performs an asynchronous request to obtain key-value pairs from client-side storage. + // + // The first argument specifies the function that is called for each key-value pair + // (if the pair is not in storage, then the function is called with an empty string as its value). + // + // The second argument specifies the set of keys to request. + // If no key is specified, then a query is performed for all possible key-value pairs. + Request(result func(key, value string), key ...string) + + // Get returns a value by key from the client-side storage. + // + // If the key-value pair is not in the client-side storage, then the function returns an empty string. + Get(key string) string + + // Set stores a key-value pair in the client-side storage. + // + // An empty string as a value removes the key-value pair. + Set(key, value string) + + // RemoveAll removes all key-value pair from the client-side storage + RemoveAll() + + handleEvent(command string, data DataObject) +} + +type clientStorageData struct { + bridge bridge + getResult map[int]func(string, string) + lastRequest int +} + +func (session *sessionData) ClientStorage() ClientStorage { + if session.clientStorage == nil { + storage := new(clientStorageData) + storage.bridge = session.bridge + session.clientStorage = storage + } + return session.clientStorage +} + +func encodeClientStorageText(text string) string { + return base64.URLEncoding.EncodeToString([]byte(text)) +} + +func decodeClientStorageText(text string) string { + result, _ := base64.URLEncoding.DecodeString(text) + return string(result) +} + +func (storage *clientStorageData) Get(key string) string { + result := make(chan string) + defer close(result) + + storage.Request(func(key, value string) { + result <- value + }, key) + + return <-result +} + +func (storage *clientStorageData) Request(result func(key, value string), key ...string) { + if result == nil { + return + } + + storage.lastRequest++ + request := storage.lastRequest + storage.getResult[request] = result + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + for _, k := range key { + if k != "" { + if buffer.Len() > 0 { + buffer.WriteRune(',') + } + buffer.WriteString(encodeClientStorageText(k)) + } + } + + if buffer.Len() == 0 { + storage.bridge.callFunc("localStorageGetAll", request) + } else { + storage.bridge.callFunc("localStorageGet", request, buffer.String()) + } +} + +func (storage *clientStorageData) Set(key, value string) { + key = encodeClientStorageText(key) + if value != "" { + storage.bridge.callFunc("localStorageSet", key, encodeClientStorageText(value)) + } else { + storage.bridge.callFunc("localStorageRemove", key) + } +} + +func (storage *clientStorageData) RemoveAll() { + storage.bridge.callFunc("localStorageClear") +} + +func (storage *clientStorageData) handleEvent(command string, data DataObject) { + request := 0 + if text, ok := data.PropertyValue("request"); ok { + var err error + if request, err = strconv.Atoi(text); err != nil { + ErrorLog(err.Error()) + return + } + } else { + ErrorLog("'request' property not found (command: " + command + ")") + return + } + + switch command { + case "storageError": + if text, ok := data.PropertyValue("error"); ok { + ErrorLog(text) + } + + //case "storageSuccess": + + case "storageValues": + fn, ok := storage.getResult[request] + if !ok { + return + } + delete(storage.getResult, request) + + text, ok := data.PropertyValue("error") + if !ok { + ErrorLog("'values' property not found (command: storageValues)") + return + } + + for pair := range strings.SplitSeq(text, ";") { + if data := strings.Split(pair, ":"); len(data) == 2 { + key := decodeClientStorageText(data[0]) + value := decodeClientStorageText(data[1]) + fn(key, value) + } + } + } +} diff --git a/session.go b/session.go index 4c59e25..03d6d22 100644 --- a/session.go +++ b/session.go @@ -135,17 +135,8 @@ type Session interface { // OpenURL opens the url in the new browser tab OpenURL(url string) - // ClientItem reads value by key from the client-side storage - ClientItem(key string) (string, bool) - - // SetClientItem stores a key-value pair in the client-side storage - SetClientItem(key, value string) - - // RemoveClientItem removes a key-value pair in the client-side storage - RemoveClientItem(key string) - - // RemoveAllClientItems removes all key-value pair from the client-side storage - RemoveAllClientItems() + // ClientStorage returns an interface for accessing client-side key-value storage. + ClientStorage() ClientStorage // SetHotKey sets the function that will be called when the given hotkey is pressed. // Invoke SetHotKey(..., ..., nil) for remove hotkey function. @@ -248,12 +239,12 @@ type sessionData struct { animationCounter int animationCSS string updateScripts map[string]*strings.Builder - clientStorage map[string]string hotkeys map[string]func(Session) timers map[int]func(Session) nextTimerID int pauseTime int64 popupDefaults Params + clientStorage ClientStorage } func newSession(app Application, id int, customTheme string, params DataObject) Session { @@ -270,7 +261,6 @@ func newSession(app Application, id int, customTheme string, params DataObject) session.animationCounter = 0 session.animationCSS = "" session.updateScripts = map[string]*strings.Builder{} - session.clientStorage = map[string]string{} session.hotkeys = map[string]func(Session){} session.timers = map[int]func(Session){} session.nextTimerID = 1 @@ -781,16 +771,6 @@ func (session *sessionData) handleSessionInfo(params DataObject) { session.pixelRatio = f } } - - if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode { - if obj := node.Object(); obj != nil { - for element := range obj.Properties() { - if element.Type() == TextNode { - session.clientStorage[element.Tag()] = element.Text() - } - } - } - } } func (session *sessionData) handleEvent(command string, data DataObject) { @@ -826,9 +806,9 @@ func (session *sessionData) handleEvent(command string, data DataObject) { case "sessionInfo": session.handleSessionInfo(data) - case "storageError": - if text, ok := data.PropertyValue("error"); ok { - ErrorLog(text) + case "storageError", "storageSuccess", "storageValues": + if session.clientStorage != nil { + session.clientStorage.handleEvent(command, data) } default: @@ -932,26 +912,6 @@ func (session *sessionData) OpenURL(urlStr string) { session.callFunc("openURL", urlStr) } -func (session *sessionData) ClientItem(key string) (string, bool) { - value, ok := session.clientStorage[key] - return value, ok -} - -func (session *sessionData) SetClientItem(key, value string) { - session.clientStorage[key] = value - session.bridge.callFunc("localStorageSet", key, value) -} - -func (session *sessionData) RemoveClientItem(key string) { - delete(session.clientStorage, key) - session.bridge.callFunc("localStorageRemove", key) -} - -func (session *sessionData) RemoveAllClientItems() { - session.clientStorage = map[string]string{} - session.bridge.callFunc("localStorageClear") -} - func (session *sessionData) addToEventsQueue(data DataObject) { session.events <- data }