package rui import ( "embed" "io" "net/http" "os" "path/filepath" "sort" "strconv" "strings" ) const ( imageDir = "images" themeDir = "themes" viewDir = "views" rawDir = "raw" stringsDir = "strings" ) type scaledImage struct { path string scale float64 } type imagePath struct { path string fs *embed.FS } type resourceManager struct { embedFS []*embed.FS themes map[string]Theme images map[string]imagePath imageSrcSets map[string][]scaledImage path string } var resources = resourceManager{ embedFS: []*embed.FS{}, themes: map[string]Theme{}, images: map[string]imagePath{}, imageSrcSets: map[string][]scaledImage{}, } func AddEmbedResources(fs *embed.FS) { resources.embedFS = append(resources.embedFS, fs) rootDirs := embedRootDirs(fs) for _, dir := range rootDirs { switch dir { case imageDir: scanEmbedImagesDir(fs, dir, "") case themeDir: scanEmbedThemesDir(fs, dir) case stringsDir: scanEmbedStringsDir(fs, dir) case viewDir, rawDir: // do nothing default: if files, err := fs.ReadDir(dir); err == nil { for _, file := range files { if file.IsDir() { switch file.Name() { case imageDir: scanEmbedImagesDir(fs, dir+"/"+imageDir, "") case themeDir: scanEmbedThemesDir(fs, dir+"/"+themeDir) case stringsDir: scanEmbedStringsDir(fs, dir+"/"+stringsDir) case viewDir, rawDir: // do nothing } } } } } } } func embedRootDirs(fs *embed.FS) []string { result := []string{} if files, err := fs.ReadDir("."); err == nil { for _, file := range files { if file.IsDir() { result = append(result, file.Name()) } } } return result } func scanEmbedThemesDir(fs *embed.FS, dir string) { if files, err := fs.ReadDir(dir); err == nil { for _, file := range files { name := file.Name() path := dir + "/" + name if file.IsDir() { scanEmbedThemesDir(fs, path) } else if strings.ToLower(filepath.Ext(name)) == ".rui" { if data, err := fs.ReadFile(path); err == nil { registerThemeText(string(data)) } } } } } func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) { if files, err := fs.ReadDir(dir); err == nil { for _, file := range files { name := file.Name() path := dir + "/" + name if file.IsDir() { scanEmbedImagesDir(fs, path, prefix+name+"/") } else { ext := strings.ToLower(filepath.Ext(name)) switch ext { case ".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp", ".webp": registerImage(fs, path, prefix+name) } } } } } func invalidImageFileFormat(filename string) { ErrorLog(`Invalid image file name parameters: "` + filename + `". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`) } func registerImage(fs *embed.FS, path, filename string) { resources.images[filename] = imagePath{fs: fs, path: path} start := strings.LastIndex(filename, "@") if start < 0 { return } ext := strings.LastIndex(filename, ".") if start > ext || filename[ext-1] != 'x' { invalidImageFileFormat(path) return } if scale, err := strconv.ParseFloat(filename[start+1:ext-1], 32); err == nil { key := filename[:start] + filename[ext:] images, ok := resources.imageSrcSets[key] if ok { for _, image := range images { if image.scale == scale { return } } } else { images = []scaledImage{} } resources.imageSrcSets[key] = append(images, scaledImage{path: filename, scale: scale}) } else { invalidImageFileFormat(path) return } } func scanImagesDirectory(path, filePrefix string) { if files, err := os.ReadDir(path); err == nil { for _, file := range files { filename := file.Name() if filename[0] != '.' { newPath := path + `/` + filename if !file.IsDir() { registerImage(nil, newPath, filePrefix+filename) } else { scanImagesDirectory(newPath, filePrefix+filename+"/") } } } } else { ErrorLog(err.Error()) } } func scanThemesDir(path string) { if files, err := os.ReadDir(path); err == nil { for _, file := range files { filename := file.Name() if filename[0] != '.' { newPath := path + `/` + filename if file.IsDir() { scanThemesDir(newPath) } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { if data, err := os.ReadFile(newPath); err == nil { registerThemeText(string(data)) } else { ErrorLog(err.Error()) } } } } } else { ErrorLog(err.Error()) } } // SetResourcePath set path of the resource directory func SetResourcePath(path string) { resources.path = path pathLen := len(path) if pathLen > 0 && path[pathLen-1] != '/' { resources.path += "/" } scanImagesDirectory(resources.path+imageDir, "") scanThemesDir(resources.path + themeDir) scanStringsDir(resources.path + stringsDir) } func registerThemeText(text string) bool { theme, ok := CreateThemeFromText(text) if !ok { return false } name := theme.Name() if name == "" { defaultTheme.Append(theme) } else if t, ok := resources.themes[name]; ok { t.Append(theme) } else { resources.themes[name] = theme } return true } func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request) bool { serveEmbed := func(fs *embed.FS, path string) bool { if file, err := fs.Open(path); err == nil { if stat, err := file.Stat(); err == nil { http.ServeContent(w, r, filename, stat.ModTime(), file.(io.ReadSeeker)) return true } } return false } if image, ok := resources.images[filename]; ok { if image.fs != nil { if serveEmbed(image.fs, image.path) { return true } } else { if _, err := os.Stat(image.path); err == nil { http.ServeFile(w, r, image.path) return true } } } for _, fs := range resources.embedFS { if serveEmbed(fs, filename) { return true } for _, dir := range embedRootDirs(fs) { if serveEmbed(fs, dir+"/"+filename) { return true } if subDirs, err := fs.ReadDir(dir); err == nil { for _, subdir := range subDirs { if subdir.IsDir() { if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) { return true } } } } } } serve := func(path, filename string) bool { filepath := path + filename if _, err := os.Stat(filepath); err == nil { http.ServeFile(w, r, filepath) return true } filepath = path + imageDir + "/" + filename if _, err := os.Stat(filepath); err == nil { http.ServeFile(w, r, filepath) return true } return false } if resources.path != "" && serve(resources.path, filename) { return true } if exe, err := os.Executable(); err == nil { path := filepath.Dir(exe) + "/resources/" if serve(path, filename) { return true } } return false } func ReadRawResource(filename string) []byte { for _, fs := range resources.embedFS { rootDirs := embedRootDirs(fs) for _, dir := range rootDirs { switch dir { case imageDir, themeDir, viewDir: // do nothing case rawDir: if data, err := fs.ReadFile(dir + "/" + filename); err == nil { return data } default: if data, err := fs.ReadFile(dir + "/" + rawDir + "/" + filename); err == nil { return data } } } } if resources.path != "" { if data, err := os.ReadFile(resources.path + rawDir + "/" + filename); err == nil { return data } } if exe, err := os.Executable(); err == nil { if data, err := os.ReadFile(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); err == nil { return data } } ErrorLogF(`The "%s" raw file don't found`, filename) return nil } func AllRawResources() []string { result := []string{} for _, fs := range resources.embedFS { rootDirs := embedRootDirs(fs) for _, dir := range rootDirs { switch dir { case imageDir, themeDir, viewDir: // do nothing case rawDir: if files, err := fs.ReadDir(rawDir); err == nil { for _, file := range files { result = append(result, file.Name()) } } default: if files, err := fs.ReadDir(dir + "/" + rawDir); err == nil { for _, file := range files { result = append(result, file.Name()) } } } } } if resources.path != "" { if files, err := os.ReadDir(resources.path + rawDir); err == nil { for _, file := range files { result = append(result, file.Name()) } } } if exe, err := os.Executable(); err == nil { if files, err := os.ReadDir(filepath.Dir(exe) + "/resources/" + rawDir); err == nil { for _, file := range files { result = append(result, file.Name()) } } } return result } func AllImageResources() []string { result := make([]string, 0, len(resources.images)) for image := range resources.images { result = append(result, image) } sort.Strings(result) return result } func AddTheme(theme Theme) { if theme != nil { name := theme.Name() if name == "" { defaultTheme.Append(theme) } else if t, ok := resources.themes[name]; ok { t.Append(theme) } else { resources.themes[name] = theme } } }