package main import ( "encoding/json" "net/http" "os" "path/filepath" "strings" ) type deviceSimulatorDevice struct { ID string `json:"id"` Name string `json:"name"` SW int `json:"sw"` SH int `json:"sh"` SCR int `json:"scr"` Notch string `json:"notch"` NW int `json:"nw,omitempty"` NH int `json:"nh,omitempty"` NR int `json:"nr,omitempty"` PS int `json:"ps,omitempty"` SAT int `json:"sat"` SAB int `json:"sab"` } var deviceSimulatorDevices = []deviceSimulatorDevice{ {ID: "iphone-17-pro-max", Name: "iPhone 17 Pro Max", SW: 440, SH: 956, SCR: 44, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-17-pro", Name: "iPhone 17 Pro", SW: 402, SH: 874, SCR: 42, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-air", Name: "iPhone Air", SW: 420, SH: 912, SCR: 42, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-17", Name: "iPhone 17", SW: 402, SH: 874, SCR: 42, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-16-pro-max", Name: "iPhone 16 Pro Max", SW: 440, SH: 956, SCR: 44, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-16-pro", Name: "iPhone 16 Pro", SW: 402, SH: 874, SCR: 42, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-16-plus", Name: "iPhone 16 Plus", SW: 430, SH: 932, SCR: 42, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-16", Name: "iPhone 16", SW: 390, SH: 844, SCR: 40, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-15-pro-max", Name: "iPhone 15 Pro Max", SW: 430, SH: 932, SCR: 42, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-15-pro", Name: "iPhone 15 Pro", SW: 393, SH: 852, SCR: 40, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-15-plus", Name: "iPhone 15 Plus", SW: 430, SH: 932, SCR: 42, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-15", Name: "iPhone 15", SW: 390, SH: 844, SCR: 38, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-14-pro-max", Name: "iPhone 14 Pro Max", SW: 430, SH: 932, SCR: 42, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-14-pro", Name: "iPhone 14 Pro", SW: 393, SH: 852, SCR: 40, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, SAB: 34}, {ID: "iphone-14-plus", Name: "iPhone 14 Plus", SW: 428, SH: 926, SCR: 40, Notch: "notch", NW: 130, NH: 34, SAT: 47, SAB: 34}, {ID: "iphone-14", Name: "iPhone 14", SW: 390, SH: 844, SCR: 38, Notch: "notch", NW: 130, NH: 34, SAT: 47, SAB: 34}, {ID: "iphone-se3", Name: "iPhone SE (3rd gen)", SW: 375, SH: 667, SCR: 4, Notch: "none", SAT: 20, SAB: 0}, {ID: "pixel-8", Name: "Google Pixel 8", SW: 411, SH: 914, SCR: 22, Notch: "punch", PS: 14, SAT: 28, SAB: 24}, {ID: "galaxy-s24", Name: "Samsung Galaxy S24", SW: 360, SH: 780, SCR: 20, Notch: "punch", PS: 12, SAT: 28, SAB: 24}, } func DeviceSimulatorPage(w http.ResponseWriter, r *http.Request) { devicesJSON, _ := json.Marshal(deviceSimulatorDevices) body := `

Device Simulator

Preview HTML playables inside mobile device frames with notch and Dynamic Island overlays.

Input

Playable HTML
or drop an HTML file here
(no file selected)

Playable

` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/device-simulator", "Device Simulator", body))) } func DeviceSimulatorPick(w http.ResponseWriter, r *http.Request) { paths := PickFiles("Select HTML file", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, false, "") path := "" if len(paths) > 0 { path = paths[0] } writeJSON(w, map[string]any{"path": path}) } func DeviceSimulatorLoad(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } if req.Path == "" || !isHTMLPath(req.Path) { writeJSON(w, map[string]any{"error": "Pick an HTML file."}) return } info, err := os.Stat(req.Path) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } if info.IsDir() { writeJSON(w, map[string]any{"error": "Expected an HTML file, got a folder."}) return } data, err := os.ReadFile(req.Path) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } writeJSON(w, map[string]any{ "name": filepath.Base(req.Path), "path": req.Path, "content": string(data), }) } func DeviceSimulatorPreview(w http.ResponseWriter, r *http.Request) { path := r.URL.Query().Get("path") if path == "" || !isHTMLPath(path) { http.Error(w, "Pick an HTML file.", http.StatusBadRequest) return } info, err := os.Stat(path) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if info.IsDir() { http.Error(w, "Expected an HTML file, got a folder.", http.StatusBadRequest) return } baseDir := filepath.Dir(path) assetName := filepath.ToSlash(r.URL.Path[len("/api/device-simulator/preview/"):]) targetPath := path if assetName != "" && assetName != "index.html" { cleanName := filepath.Clean(filepath.FromSlash(assetName)) targetPath = filepath.Join(baseDir, cleanName) resolvedBase, err := filepath.Abs(baseDir) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } resolvedTarget, err := filepath.Abs(targetPath) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if resolvedTarget != resolvedBase && len(resolvedTarget) > len(resolvedBase) && resolvedTarget[:len(resolvedBase)+1] != resolvedBase+string(os.PathSeparator) { http.Error(w, "Forbidden", http.StatusForbidden) return } } data, err := os.ReadFile(targetPath) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if targetPath == path { w.Header().Set("Content-Type", "text/html; charset=utf-8") data = []byte(injectDeviceSimulatorMuteBridge(string(data))) } w.Header().Set("Cache-Control", "no-store") _, _ = w.Write(data) } func injectDeviceSimulatorMuteBridge(html string) string { bridge := `` lower := strings.ToLower(html) headIndex := strings.Index(lower, "= 0 { closeIndex := strings.Index(lower[headIndex:], ">") if closeIndex >= 0 { insertAt := headIndex + closeIndex + 1 return html[:insertAt] + bridge + html[insertAt:] } } return bridge + html }