package main import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "strings" "sync" "time" ) 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: "pixel-8", Name: "Google Pixel 8", SW: 411, SH: 914, SCR: 22, Notch: "punch", PS: 14, SAT: 28, SAB: 24}, {ID: "iphone-14", Name: "iPhone 14", SW: 390, SH: 844, SCR: 38, Notch: "notch", NW: 130, NH: 34, SAT: 47, 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-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-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-15", Name: "iPhone 15", SW: 390, SH: 844, SCR: 38, 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-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-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-16", Name: "iPhone 16", SW: 390, SH: 844, SCR: 40, Notch: "island", NW: 120, NH: 36, NR: 18, SAT: 59, 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-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-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-17", Name: "iPhone 17", SW: 402, SH: 874, SCR: 42, 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-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-air", Name: "iPhone Air", SW: 420, SH: 912, SCR: 42, Notch: "island", NW: 126, NH: 37, NR: 20, SAT: 62, SAB: 34}, {ID: "iphone-se3", Name: "iPhone SE (3rd gen)", SW: 375, SH: 667, SCR: 4, Notch: "none", SAT: 20, SAB: 0}, {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) } var reGDriveFileIDSim = regexp.MustCompile(`/file/d/([a-zA-Z0-9_-]+)`) var reGDriveIDParamSim = regexp.MustCompile(`[?&]id=([a-zA-Z0-9_-]+)`) func resolveSimGDriveURL(rawURL string) string { if m := reGDriveFileIDSim.FindStringSubmatch(rawURL); m != nil { return "https://drive.google.com/uc?export=download&id=" + m[1] } if m := reGDriveIDParamSim.FindStringSubmatch(rawURL); m != nil { return "https://drive.google.com/uc?export=download&id=" + m[1] } return rawURL } type simRemoteCache struct { mu sync.Mutex devices string languages string devURL string langURL string } var simCache simRemoteCache func fetchRemoteJSON(rawURL string) (string, error) { url := resolveSimGDriveURL(rawURL) client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("User-Agent", "Mozilla/5.0") resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently { loc := resp.Header.Get("Location") if loc != "" { return fetchRemoteJSON(loc) } } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("HTTP %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", err } var v any if err := json.Unmarshal(body, &v); err != nil { return "", fmt.Errorf("invalid JSON: %w", err) } return string(body), nil } func DeviceSimulatorRemoteDevicesEndpoint(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() url := strings.TrimSpace(cfg.DeviceSimulator.DevicesURL) if url == "" { writeJSON(w, map[string]any{"error": "devicesUrl not configured"}) return } simCache.mu.Lock() if simCache.devURL == url && simCache.devices != "" { cached := simCache.devices simCache.mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(cached)) return } simCache.mu.Unlock() data, err := fetchRemoteJSON(url) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } simCache.mu.Lock() simCache.devURL = url simCache.devices = data simCache.mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(data)) } func DeviceSimulatorRemoteLanguagesEndpoint(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() url := strings.TrimSpace(cfg.DeviceSimulator.LanguagesURL) if url == "" { writeJSON(w, map[string]any{"error": "languagesUrl not configured"}) return } simCache.mu.Lock() if simCache.langURL == url && simCache.languages != "" { cached := simCache.languages simCache.mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(cached)) return } simCache.mu.Unlock() data, err := fetchRemoteJSON(url) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } simCache.mu.Lock() simCache.langURL = url simCache.languages = data simCache.mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(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 }