Added FilePicker

This commit is contained in:
anoshenko 2021-11-04 14:59:25 +03:00
parent f64f6d2bca
commit 04bbd47f66
14 changed files with 595 additions and 64 deletions

View File

@ -45,8 +45,11 @@ func DebugLogF(format string, a ...interface{}) {
} }
} }
var lastError = ""
// ErrorLog print the text to the error log // ErrorLog print the text to the error log
func ErrorLog(text string) { func ErrorLog(text string) {
lastError = text
if errorLogFunc != nil { if errorLogFunc != nil {
errorLogFunc(text) errorLogFunc(text)
errorStack() errorStack()
@ -55,12 +58,18 @@ func ErrorLog(text string) {
// ErrorLogF print the text to the error log // ErrorLogF print the text to the error log
func ErrorLogF(format string, a ...interface{}) { func ErrorLogF(format string, a ...interface{}) {
lastError = fmt.Sprintf(format, a...)
if errorLogFunc != nil { if errorLogFunc != nil {
errorLogFunc(fmt.Sprintf(format, a...)) errorLogFunc(lastError)
errorStack() errorStack()
} }
} }
// LastError returns the last error text
func LastError() string {
return lastError
}
func errorStack() { func errorStack() {
if errorLogFunc != nil { if errorLogFunc != nil {
skip := 2 skip := 2

View File

@ -989,6 +989,50 @@ function setInputValue(elementId, text) {
} }
} }
function fileSelectedEvent(element) {
var files = element.files;
if (files) {
var message = "fileSelected{session=" + sessionID + ",id=" + element.id + ",files=[";
for(var i = 0; i < files.length; i++) {
if (i > 0) {
message += ",";
}
message += "_{name=\"" + files[i].name +
"\",last-modified=" + files[i].lastModified +
",size=" + files[i].size +
",mime-type=\"" + files[i].type + "\"}";
}
sendMessage(message + "]}");
}
}
function loadSelectedFile(elementId, index) {
var element = document.getElementById(elementId);
if (element) {
var files = element.files;
if (files && index >= 0 && index < files.length) {
const reader = new FileReader();
reader.onload = function() {
sendMessage("fileLoaded{session=" + sessionID + ",id=" + element.id +
",index=" + index +
",name=\"" + files[index].name +
"\",last-modified=" + files[index].lastModified +
",size=" + files[index].size +
",mime-type=\"" + files[index].type +
"\",data=`" + reader.result + "`}");
}
reader.onerror = function(error) {
sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",index=" + index + ",error=`" + error + "`}");
}
reader.readAsDataURL(files[index]);
} else {
sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",index=" + index + ",error=`File not found`}");
}
} else {
sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",index=" + index + ",error=`Invalid FilePicker id`}");
}
}
function startResize(element, mx, my, event) { function startResize(element, mx, my, event) {
var view = element.parentNode; var view = element.parentNode;
if (!view) { if (!view) {

39
demo/filePickerDemo.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"github.com/anoshenko/rui"
)
const filePickerDemoText = `
GridLayout {
width = 100%, height = 100%, cell-height = "auto, 1fr",
content = [
FilePicker {
id = filePicker, accept = "txt, html"
},
EditView {
id = selectedFileData, row = 1, type = multiline, read-only = true, wrap = true,
}
]
}
`
func createFilePickerDemo(session rui.Session) rui.View {
view := rui.CreateViewFromText(session, filePickerDemoText)
if view == nil {
return nil
}
rui.Set(view, "filePicker", rui.FileSelectedEvent, func(picker rui.FilePicker, files []rui.FileInfo) {
if len(files) > 0 {
picker.LoadFile(files[0], func(file rui.FileInfo, data []byte) {
if data != nil {
rui.Set(view, "selectedFileData", rui.Text, string(data))
} else {
rui.Set(view, "selectedFileData", rui.Text, rui.LastError())
}
})
}
})
return view
}

View File

@ -82,6 +82,7 @@ func createDemo(session rui.Session) rui.SessionContent {
{"ListView", createListViewDemo, nil}, {"ListView", createListViewDemo, nil},
{"Checkbox", createCheckboxDemo, nil}, {"Checkbox", createCheckboxDemo, nil},
{"Controls", createControlsDemo, nil}, {"Controls", createControlsDemo, nil},
{"FilePicker", createFilePickerDemo, nil},
{"TableView", createTableViewDemo, nil}, {"TableView", createTableViewDemo, nil},
{"EditView", createEditDemo, nil}, {"EditView", createEditDemo, nil},
{"ImageView", createImageViewDemo, nil}, {"ImageView", createImageViewDemo, nil},

View File

@ -1,26 +0,0 @@
package main
/*
import (
"github.com/anoshenko/rui"
)
const splitViewDemoText = `
SplitView {
width = 100%, height = 100%, orientation = vertical, anchor = bottom, padding = 2px,
view1 = GridLayout { width = 100%, height = 75%, content = ["View 1"], cell-vertical-align = center, cell-horizontal-align = center,
border = _{ width = 1px, style = solid, color = #FF000000 }, radius = 8px,},
view2 = GridLayout { width = 100%, height = 25%, content = ["View 2"], cell-align = center,
border = _{ width = 1px, style = solid, color = #FF000000 }, radius = 8px,},
}
`
func createSplitViewDemo(session rui.Session) rui.View {
view := rui.CreateViewFromText(session, splitViewDemoText)
if view == nil {
return nil
}
return view
}
*/

414
filePicker.go Normal file
View File

@ -0,0 +1,414 @@
package rui
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
)
const (
// FileSelectedEvent is the constant for "file-selected-event" property tag.
// The "file-selected-event" is fired when user selects file(s) in the FilePicker.
FileSelectedEvent = "file-selected-event"
// Accept is the constant for "accept" property tag.
// The "accept" property of the FilePicker sets the list of allowed file extensions or MIME types.
Accept = "accept"
// Multiple is the constant for "multiple" property tag.
// The "multiple" bool property of the FilePicker sets whether multiple files can be selected
Multiple = "multiple"
)
// FileInfo describes a file which selected in the FilePicker view
type FileInfo struct {
// Name - the file's name.
Name string
// LastModified specifying the date and time at which the file was last modified
LastModified time.Time
// Size - the size of the file in bytes.
Size int64
// MimeType - the file's MIME type.
MimeType string
}
// FilePicker - the control view for the files selecting
type FilePicker interface {
View
// Files returns the list of selected files.
// If there are no files selected then an empty slice is returned (the result is always not nil)
Files() []FileInfo
// LoadFile loads the content of the selected file. This function is asynchronous.
// The "result" function will be called after loading the data.
LoadFile(file FileInfo, result func(FileInfo, []byte))
}
type filePickerData struct {
viewData
files []FileInfo
fileSelectedListeners []func(FilePicker, []FileInfo)
loader map[int]func(FileInfo, []byte)
}
func (file *FileInfo) initBy(node DataValue) {
if obj := node.Object(); obj != nil {
file.Name, _ = obj.PropertyValue("name")
file.MimeType, _ = obj.PropertyValue("mime-type")
if size, ok := obj.PropertyValue("size"); ok {
if n, err := strconv.ParseInt(size, 10, 64); err == nil {
file.Size = n
}
}
if value, ok := obj.PropertyValue("last-modified"); ok {
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
file.LastModified = time.UnixMilli(n)
}
}
}
}
// NewFilePicker create new FilePicker object and return it
func NewFilePicker(session Session, params Params) FilePicker {
view := new(filePickerData)
view.Init(session)
setInitParams(view, params)
return view
}
func newFilePicker(session Session) View {
return NewFilePicker(session, nil)
}
func (picker *filePickerData) Init(session Session) {
picker.viewData.Init(session)
picker.tag = "FilePicker"
picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
}
func (picker *filePickerData) Files() []FileInfo {
return picker.files
}
func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result == nil {
return
}
for i, info := range picker.files {
if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified {
picker.loader[i] = result
picker.Session().runScript(fmt.Sprintf(`loadSelectedFile("%s", %d)`, picker.htmlID(), i))
return
}
}
}
func (picker *filePickerData) Remove(tag string) {
picker.remove(strings.ToLower(tag))
}
func (picker *filePickerData) remove(tag string) {
switch tag {
case FileSelectedEvent:
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
default:
picker.viewData.remove(tag)
}
}
func (picker *filePickerData) Set(tag string, value interface{}) bool {
return picker.set(strings.ToLower(tag), value)
}
func (picker *filePickerData) set(tag string, value interface{}) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag {
case FileSelectedEvent:
switch value := value.(type) {
case func(FilePicker, []FileInfo):
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){value}
case func([]FileInfo):
fn := func(view FilePicker, files []FileInfo) {
value(files)
}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){fn}
case []func(FilePicker, []FileInfo):
picker.fileSelectedListeners = value
case []func([]FileInfo):
listeners := make([]func(FilePicker, []FileInfo), len(value))
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
listeners[i] = func(view FilePicker, files []FileInfo) {
val(files)
}
}
picker.fileSelectedListeners = listeners
case []interface{}:
listeners := make([]func(FilePicker, []FileInfo), len(value))
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
switch val := val.(type) {
case func(FilePicker, []FileInfo):
listeners[i] = val
case func([]FileInfo):
listeners[i] = func(view FilePicker, files []FileInfo) {
val(files)
}
default:
notCompatibleType(tag, val)
return false
}
}
picker.fileSelectedListeners = listeners
}
return true
case Accept:
switch value := value.(type) {
case string:
value = strings.Trim(value, " \t\n")
if value == "" {
picker.remove(Accept)
} else {
picker.properties[Accept] = value
}
case []string:
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, val := range value {
val = strings.Trim(val, " \t\n")
if val != "" {
if buffer.Len() > 0 {
buffer.WriteRune(',')
}
buffer.WriteString(val)
}
}
if buffer.Len() == 0 {
picker.remove(Accept)
} else {
picker.properties[Accept] = buffer.String()
}
default:
notCompatibleType(tag, value)
return false
}
return true
default:
return picker.viewData.set(tag, value)
}
}
func (picker *filePickerData) htmlTag() string {
return "input"
}
func (picker *filePickerData) acceptCSS() string {
accept, ok := stringProperty(picker, Accept, picker.Session())
if !ok {
accept, ok = valueFromStyle(picker, Accept)
}
if ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, value := range strings.Split(accept, ",") {
if value = strings.Trim(value, " \t\n"); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
if value[0] != '.' && !strings.Contains(value, "/") {
buffer.WriteRune('.')
}
buffer.WriteString(value)
}
}
return buffer.String()
}
return ""
}
func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer)
if accept := picker.acceptCSS(); accept != "" {
buffer.WriteString(` accept="`)
buffer.WriteString(accept)
buffer.WriteRune('"')
}
buffer.WriteString(` type="file"`)
if multiple, ok := boolStyledProperty(picker, Multiple); ok && multiple {
buffer.WriteString(` multiple`)
}
buffer.WriteString(` oninput="fileSelectedEvent(this)"`)
}
func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "fileSelected":
if node := data.PropertyWithTag("files"); node != nil && node.Type() == ArrayNode {
count := node.ArraySize()
files := make([]FileInfo, count)
for i := 0; i < count; i++ {
if value := node.ArrayElement(i); value != nil {
files[i].initBy(value)
}
}
picker.files = files
for _, listener := range picker.fileSelectedListeners {
listener(picker, files)
}
}
return true
case "fileLoaded":
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
var file FileInfo
file.initBy(data)
var fileData []byte = nil
if base64Data, ok := data.PropertyValue("data"); ok {
if index := strings.LastIndex(base64Data, ","); index >= 0 {
base64Data = base64Data[index+1:]
}
decode, err := base64.StdEncoding.DecodeString(base64Data)
if err == nil {
fileData = decode
} else {
ErrorLog(err.Error())
}
}
result(file, fileData)
delete(picker.loader, index)
}
}
return true
case "fileLoadingError":
if error, ok := data.PropertyValue("error"); ok {
ErrorLog(error)
}
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
if index >= 0 && index < len(picker.files) {
result(picker.files[index], nil)
} else {
result(FileInfo{}, nil)
}
delete(picker.loader, index)
}
}
return true
}
return picker.viewData.handleCommand(self, command, data)
}
// GetFilePickerFiles returns the list of FilePicker selected files
// If there are no files selected then an empty slice is returned (the result is always not nil)
// If the second argument (subviewID) is "" then selected files of the first argument (view) is returned
func GetFilePickerFiles(view View, subviewID string) []FileInfo {
if picker := FilePickerByID(view, subviewID); picker != nil {
return picker.Files()
}
return []FileInfo{}
}
// LoadFilePickerFile loads the content of the selected file. This function is asynchronous.
// The "result" function will be called after loading the data.
// If the second argument (subviewID) is "" then the file from the first argument (view) is loaded
func LoadFilePickerFile(view View, subviewID string, file FileInfo, result func(FileInfo, []byte)) {
if picker := FilePickerByID(view, subviewID); picker != nil {
picker.LoadFile(file, result)
}
}
// IsMultipleFilePicker returns "true" if multiple files can be selected in the FilePicker, "false" otherwise.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func IsMultipleFilePicker(view View, subviewID string) bool {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := boolStyledProperty(view, Multiple); ok {
return result
}
}
return false
}
// GetFilePickerAccept returns sets the list of allowed file extensions or MIME types.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetFilePickerAccept(view View, subviewID string) []string {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
accept, ok := stringProperty(view, Accept, view.Session())
if !ok {
accept, ok = valueFromStyle(view, Accept)
}
if ok {
result := strings.Split(accept, ",")
for i := 0; i < len(result); i++ {
result[i] = strings.Trim(result[i], " \t\n")
}
return result
}
}
return []string{}
}
// GetFileSelectedListeners returns the "file-selected-event" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetFileSelectedListeners(view View, subviewID string) []func(FilePicker, []FileInfo) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(FileSelectedEvent); value != nil {
if result, ok := value.([]func(FilePicker, []FileInfo)); ok {
return result
}
}
}
return []func(FilePicker, []FileInfo){}
}

View File

@ -844,7 +844,7 @@ func (player *mediaPlayerData) handleCommand(self View, command string, data Dat
case PlayerErrorEvent: case PlayerErrorEvent:
if value := player.getRaw(command); value != nil { if value := player.getRaw(command); value != nil {
if listeners, ok := value.([]func(MediaPlayer, int, string)); ok { if listeners, ok := value.([]func(MediaPlayer, int, string)); ok {
code := dataIntProperty(data, "code") code, _ := dataIntProperty(data, "code")
message, _ := data.PropertyValue("message") message, _ := data.PropertyValue("message")
for _, listener := range listeners { for _, listener := range listeners {
listener(player, code, message) listener(player, code, message)

View File

@ -328,8 +328,8 @@ func getTimeStamp(data DataObject) uint64 {
func (event *MouseEvent) init(data DataObject) { func (event *MouseEvent) init(data DataObject) {
event.TimeStamp = getTimeStamp(data) event.TimeStamp = getTimeStamp(data)
event.Button = dataIntProperty(data, "button") event.Button, _ = dataIntProperty(data, "button")
event.Buttons = dataIntProperty(data, "buttons") event.Buttons, _ = dataIntProperty(data, "buttons")
event.X = dataFloatProperty(data, "x") event.X = dataFloatProperty(data, "x")
event.Y = dataFloatProperty(data, "y") event.Y = dataFloatProperty(data, "y")
event.ClientX = dataFloatProperty(data, "clientX") event.ClientX = dataFloatProperty(data, "clientX")

View File

@ -277,7 +277,7 @@ func pointerEventsHtml(view View, buffer *strings.Builder) {
func (event *PointerEvent) init(data DataObject) { func (event *PointerEvent) init(data DataObject) {
event.MouseEvent.init(data) event.MouseEvent.init(data)
event.PointerID = dataIntProperty(data, "pointerId") event.PointerID, _ = dataIntProperty(data, "pointerId")
event.Width = dataFloatProperty(data, "width") event.Width = dataFloatProperty(data, "width")
event.Height = dataFloatProperty(data, "height") event.Height = dataFloatProperty(data, "height")
event.Pressure = dataFloatProperty(data, "pressure") event.Pressure = dataFloatProperty(data, "pressure")

View File

@ -55,6 +55,7 @@ var boolProperties = []string{
Loop, Loop,
Muted, Muted,
AnimationPaused, AnimationPaused,
Multiple,
} }
var intProperties = []string{ var intProperties = []string{

View File

@ -52,13 +52,13 @@ func GetLocalIP() string {
return "localhost" return "localhost"
} }
func dataIntProperty(data DataObject, tag string) int { func dataIntProperty(data DataObject, tag string) (int, bool) {
if value, ok := data.PropertyValue(tag); ok { if value, ok := data.PropertyValue(tag); ok {
if n, err := strconv.Atoi(value); err == nil { if n, err := strconv.Atoi(value); err == nil {
return n return n, true
} }
} }
return 0 return 0, false
} }
func dataBoolProperty(data DataObject, tag string) bool { func dataBoolProperty(data DataObject, tag string) bool {

View File

@ -203,6 +203,18 @@ func ProgressBarByID(rootView View, id string) ProgressBar {
return nil return nil
} }
// ColorPickerByID return a ColorPicker with id equal to the argument of the function or
// nil if there is no such View or View is not ColorPicker
func ColorPickerByID(rootView View, id string) ColorPicker {
if view := ViewByID(rootView, id); view != nil {
if input, ok := view.(ColorPicker); ok {
return input
}
ErrorLog(`ColorPickerByID(_, "` + id + `"): The found View is not ColorPicker`)
}
return nil
}
// NumberPickerByID return a NumberPicker with id equal to the argument of the function or // NumberPickerByID return a NumberPicker with id equal to the argument of the function or
// nil if there is no such View or View is not NumberPicker // nil if there is no such View or View is not NumberPicker
func NumberPickerByID(rootView View, id string) NumberPicker { func NumberPickerByID(rootView View, id string) NumberPicker {
@ -215,6 +227,42 @@ func NumberPickerByID(rootView View, id string) NumberPicker {
return nil return nil
} }
// TimePickerByID return a TimePicker with id equal to the argument of the function or
// nil if there is no such View or View is not TimePicker
func TimePickerByID(rootView View, id string) TimePicker {
if view := ViewByID(rootView, id); view != nil {
if input, ok := view.(TimePicker); ok {
return input
}
ErrorLog(`TimePickerByID(_, "` + id + `"): The found View is not TimePicker`)
}
return nil
}
// DatePickerByID return a DatePicker with id equal to the argument of the function or
// nil if there is no such View or View is not DatePicker
func DatePickerByID(rootView View, id string) DatePicker {
if view := ViewByID(rootView, id); view != nil {
if input, ok := view.(DatePicker); ok {
return input
}
ErrorLog(`DatePickerByID(_, "` + id + `"): The found View is not DatePicker`)
}
return nil
}
// FilePickerByID return a FilePicker with id equal to the argument of the function or
// nil if there is no such View or View is not FilePicker
func FilePickerByID(rootView View, id string) FilePicker {
if view := ViewByID(rootView, id); view != nil {
if input, ok := view.(FilePicker); ok {
return input
}
ErrorLog(`FilePickerByID(_, "` + id + `"): The found View is not FilePicker`)
}
return nil
}
// CanvasViewByID return a CanvasView with id equal to the argument of the function or // CanvasViewByID return a CanvasView with id equal to the argument of the function or
// nil if there is no such View or View is not CanvasView // nil if there is no such View or View is not CanvasView
func CanvasViewByID(rootView View, id string) CanvasView { func CanvasViewByID(rootView View, id string) CanvasView {

View File

@ -24,6 +24,7 @@ var viewCreators = map[string]func(Session) View{
"ColorPicker": newColorPicker, "ColorPicker": newColorPicker,
"DatePicker": newDatePicker, "DatePicker": newDatePicker,
"TimePicker": newTimePicker, "TimePicker": newTimePicker,
"FilePicker": newFilePicker,
"EditView": newEditView, "EditView": newEditView,
"ListView": newListView, "ListView": newListView,
"CanvasView": newCanvasView, "CanvasView": newCanvasView,