commit 4d9e0836e5d625b0402307a97e8cee8eaa8a815e Author: Adrian Zürcher Date: Fri Jan 16 07:51:28 2026 +0100 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..5802e9e --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +ENV= #empty|development +PHOTO_DIR=images +HOST=0.0.0.0 +PORT=8080 +INTERVAL_DEFAULT=120 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..219e8c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +images/ +slideshow_app \ No newline at end of file diff --git a/env/enviroment.go b/env/enviroment.go new file mode 100644 index 0000000..8d73c0f --- /dev/null +++ b/env/enviroment.go @@ -0,0 +1,34 @@ +package env + +import ( + "os" + "strings" + + "github.com/joho/godotenv" +) + +const ( + Env EnvKey = "ENV" + PhotoDir EnvKey = "PHOTO_DIR" + Host EnvKey = "HOST" + Port EnvKey = "PORT" + IntervalDefault EnvKey = "INTERVAL_DEFAULT" +) + +type EnvKey string + +func Load(path string) error { + if path == "" { + path = ".env" + } + return godotenv.Load(path) +} + +func (key EnvKey) GetValue() string { + return os.Getenv(string(key)) +} + +func (key EnvKey) GetBoolValue() bool { + value := strings.ToLower(os.Getenv(string(key))) + return value == "true" || value == "1" +} diff --git a/frame.html b/frame.html new file mode 100644 index 0000000..32f8c68 --- /dev/null +++ b/frame.html @@ -0,0 +1,211 @@ + + + + + Photo Frame + + + + + + + +
+
+ Interval: + + s +
+ +
+ + + + + Back +
+ +
+
+

Scan to Upload

+
+ + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cf24f2b --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module slideshowApp + +go 1.25.4 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..160ba93 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/handlers/files.go b/handlers/files.go new file mode 100644 index 0000000..179786d --- /dev/null +++ b/handlers/files.go @@ -0,0 +1,96 @@ +package handlers + +import ( + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "slideshowApp/env" + "sync" + + "github.com/gorilla/websocket" +) + +var ( + // Keep track of all open slideshow connections + clients = make(map[*websocket.Conn]bool) + clientsMu sync.Mutex +) + +func UploadHandler(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(50 << 20) + files := r.MultipartForm.File["myPictures"] + uploadDir := env.PhotoDir.GetValue() + os.MkdirAll(uploadDir, os.ModePerm) + + for _, fileHeader := range files { + file, _ := fileHeader.Open() + dstPath := filepath.Join(uploadDir, fileHeader.Filename) + dst, _ := os.Create(dstPath) + io.Copy(dst, file) + file.Close() + dst.Close() + } + + // NEW: Tell all slideshows to update their lists + notifyClients() + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func ListFilesHandler(w http.ResponseWriter, r *http.Request) { + files, err := os.ReadDir(env.PhotoDir.GetValue()) + if err != nil { + http.Error(w, "Unable to read directory", http.StatusInternalServerError) + return + } + var fileNames []string + for _, file := range files { + if !file.IsDir() { + fileNames = append(fileNames, file.Name()) + } + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(fileNames) +} + +func DeleteHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // 1. Get filenames from request body + var filenames []string + err := json.NewDecoder(r.Body).Decode(&filenames) + if err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + uploadDir := env.PhotoDir.GetValue() + + // 2. Delete each file + for _, name := range filenames { + fullPath := filepath.Join(uploadDir, filepath.Base(name)) // Base() for security + os.Remove(fullPath) + } + + // 3. Notify slideshows to refresh their lists + notifyClients() + + w.WriteHeader(http.StatusOK) +} + +func notifyClients() { + clientsMu.Lock() + defer clientsMu.Unlock() + for client := range clients { + err := client.WriteMessage(websocket.TextMessage, []byte("refresh")) + if err != nil { + client.Close() + delete(clients, client) + } + } +} diff --git a/handlers/infos.go b/handlers/infos.go new file mode 100644 index 0000000..abfaf96 --- /dev/null +++ b/handlers/infos.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "encoding/json" + "net" + "net/http" + "slideshowApp/env" + "strings" +) + +// Helper to get the local network IP address +func getLocalIP() string { + if env.Host.GetValue() != "0.0.0.0" && env.Host.GetValue() != "localhost" { + return env.Host.GetValue() + } + + addrs, err := net.InterfaceAddrs() + if err != nil { + return "localhost" + } + for _, address := range addrs { + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + if env.Env.GetValue() == "development" && !strings.Contains(ipnet.IP.String(), "192.168") { + continue + } + return ipnet.IP.String() + } + } + } + return "localhost" +} + +func InfoHandler(w http.ResponseWriter, r *http.Request) { + port := env.Port.GetValue() + if port == "" { + port = "8080" + } + + speed := env.IntervalDefault.GetValue() + if speed == "" { + speed = "10" + } + + data := map[string]string{ + "ip": getLocalIP(), + "port": port, + "speed": speed, + } + json.NewEncoder(w).Encode(data) +} diff --git a/handlers/scheduler.go b/handlers/scheduler.go new file mode 100644 index 0000000..824899a --- /dev/null +++ b/handlers/scheduler.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "os" +) + +type Schedule map[string]interface{} + +func SaveSchedule(w http.ResponseWriter, r *http.Request) { + var s Schedule + json.NewDecoder(r.Body).Decode(&s) + data, _ := json.Marshal(s) + os.WriteFile("schedule.json", data, 0644) + w.WriteHeader(http.StatusOK) +} + +func GetSchedule(w http.ResponseWriter, r *http.Request) { + data, err := os.ReadFile("schedule.json") + if err != nil { + json.NewEncoder(w).Encode(Schedule{}) + return + } + w.Write(data) +} diff --git a/handlers/websocket.go b/handlers/websocket.go new file mode 100644 index 0000000..c75ee47 --- /dev/null +++ b/handlers/websocket.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "net/http" + + "github.com/gorilla/websocket" +) + +var ( + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } +) + +func Websocket(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + clientsMu.Lock() + clients[conn] = true + clientsMu.Unlock() +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..6d79a0a --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + Go File Center + + + + +
+ + + + + + +
+

Photo Upload

+

Upload multiple PNG or JPG files

+
+ +
+
+ + +
+ + + +

Click to select or drag and drop

+
+
+ +
    + + + +
    + + + +
    + + + + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..101253e --- /dev/null +++ b/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "slideshowApp/env" + "slideshowApp/handlers" + + "github.com/gorilla/mux" +) + +func main() { + + env.Load(".env") + + r := mux.NewRouter() + + uploadFolder := env.PhotoDir.GetValue() + + if _, err := os.Stat(uploadFolder); err != nil { + fmt.Println("upload folder for images not found: ", uploadFolder) + fmt.Println("use fallback") + uploadFolder = "./images" + + } + + fmt.Println("upload folder for images: ", uploadFolder) + r.PathPrefix("/uploads/").Handler(http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadFolder)))) + r.HandleFunc("/api/images", handlers.ListFilesHandler).Methods("GET") + r.HandleFunc("/ws", handlers.Websocket) + r.HandleFunc("/upload", handlers.UploadHandler).Methods("POST") + r.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "settings.html") }) + r.HandleFunc("/api/save-schedule", handlers.SaveSchedule).Methods("POST") + r.HandleFunc("/api/get-schedule", handlers.GetSchedule).Methods("GET") + + r.HandleFunc("/api/delete", handlers.DeleteHandler).Methods("POST") + r.HandleFunc("/manage", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "manage.html") + }) + + r.HandleFunc("/api/info", handlers.InfoHandler).Methods("GET") + + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "index.html") + }) + // We add a route for the slideshow page specifically + r.HandleFunc("/slideshow", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "frame.html") + }) + + host := env.Host.GetValue() + port := env.Port.GetValue() + url := fmt.Sprintf("%s:%s", host, port) + fmt.Println("Server running at", url) + log.Fatal(http.ListenAndServe(url, r)) +} diff --git a/manage.html b/manage.html new file mode 100644 index 0000000..6f63611 --- /dev/null +++ b/manage.html @@ -0,0 +1,87 @@ + + + + + Manage Files + + + +
    + +
    +
    +

    Gallery Manager

    +

    Select files to remove from the slideshow

    +
    + ← Back to Upload +
    + +
    + + +
    + +
    +
    Loading files...
    +
    +
    + + + + \ No newline at end of file diff --git a/schedule.json b/schedule.json new file mode 100644 index 0000000..c1cfd86 --- /dev/null +++ b/schedule.json @@ -0,0 +1 @@ +{"Friday_active":false,"Friday_end":"07:38","Friday_start":"07:00","Monday_active":false,"Monday_end":"22:00","Monday_start":"08:00","Saturday_active":false,"Saturday_end":"22:00","Saturday_start":"08:00","Sunday_active":false,"Sunday_end":"22:00","Sunday_start":"08:00","Thursday_active":false,"Thursday_end":"22:00","Thursday_start":"08:00","Tuesday_active":false,"Tuesday_end":"22:00","Tuesday_start":"08:00","Wednesday_active":false,"Wednesday_end":"22:00","Wednesday_start":"08:00"} \ No newline at end of file diff --git a/settings.html b/settings.html new file mode 100644 index 0000000..b5280d6 --- /dev/null +++ b/settings.html @@ -0,0 +1,81 @@ + + + + + Sheduler Settings + + + +
    +
    +

    Weekly Scheduler

    + Back +
    + +
    + + + + + + + + + + + +
    DayEnabledStart TimeEnd Time
    + +
    +
    + + + + \ No newline at end of file