You've already forked collage-maker
							
							Compare commits
	
		
			12 Commits
		
	
	
		
			uploadexp
			...
			12b4386f14
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 12b4386f14 | |||
| c009c01a3f | |||
| efda3b72b8 | |||
| 04a5dd62b6 | |||
| 1db7b2b4ad | |||
| 1bf6bf9033 | |||
| e21cc83a5f | |||
| 298fed011d | |||
| 12befa0ac3 | |||
| 5fee186dcb | |||
| bf6bdf9b72 | |||
| fa1c8e6963 | 
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
|  |  | ||||||
|  | 	"github.com/disintegration/imaging" | ||||||
| 	"go.oneofone.dev/resize" | 	"go.oneofone.dev/resize" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -102,6 +103,6 @@ func GetImage(source fs.FS, imageName string) (image.Image, error) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	img, _, err := image.Decode(imgF) | 	img, err := imaging.Decode(imgF, imaging.AutoOrientation(true)) | ||||||
| 	return img, err | 	return img, err | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,8 +1,9 @@ | |||||||
| module go.balki.me/collage-maker | module go.balki.me/collage-maker | ||||||
|  |  | ||||||
| go 1.21 | go 1.22 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
|  | 	github.com/disintegration/imaging v1.6.2 | ||||||
| 	go.balki.me/anyhttp v0.3.0 | 	go.balki.me/anyhttp v0.3.0 | ||||||
| 	go.oneofone.dev/resize v1.0.1 | 	go.oneofone.dev/resize v1.0.1 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,3 +1,5 @@ | |||||||
|  | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= | ||||||
|  | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= | ||||||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
| go.balki.me/anyhttp v0.3.0 h1:WtBQ0rnkg567sX/O4ij/+qBbdCIUt5VURSe718sITBY= | go.balki.me/anyhttp v0.3.0 h1:WtBQ0rnkg567sX/O4ij/+qBbdCIUt5VURSe718sITBY= | ||||||
| go.balki.me/anyhttp v0.3.0/go.mod h1:JhfekOIjgVODoVqUCficjpIgmB3wwlB7jhN0eN2EZ/s= | go.balki.me/anyhttp v0.3.0/go.mod h1:JhfekOIjgVODoVqUCficjpIgmB3wwlB7jhN0eN2EZ/s= | ||||||
| @@ -5,6 +7,7 @@ go.oneofone.dev/resize v1.0.1 h1:HjpVar/4pxMGrjO44ThaMX1Q5UOBw0KxzbxxRDZPQuA= | |||||||
| go.oneofone.dev/resize v1.0.1/go.mod h1:zGFmn7q4EUZVlnDmxqf+b0mWpxsTt0MH2yx6ng8tpq0= | go.oneofone.dev/resize v1.0.1/go.mod h1:zGFmn7q4EUZVlnDmxqf+b0mWpxsTt0MH2yx6ng8tpq0= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||||
|  | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||||
| golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk= | golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk= | ||||||
| golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= | golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= | ||||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||||
|   | |||||||
							
								
								
									
										231
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										231
									
								
								main.go
									
									
									
									
									
								
							| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| @@ -23,42 +24,50 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	imagesDir      string |  | ||||||
| 	collageDir      string | 	collageDir      string | ||||||
|  | 	photosDir       string | ||||||
| 	devMode         bool | 	devMode         bool | ||||||
| 	collageNameGen  *nameGen | 	collageNameGen  *nameGen | ||||||
| 	imagesDirFs     fs.FS | 	imagesDirFs     fs.FS | ||||||
| 	listenAddr      string | 	listenAddr      string | ||||||
|  | 	photoPrismURL   *url.URL | ||||||
|  | 	photoPrismToken string | ||||||
|  |  | ||||||
| 	//go:embed web | 	//go:embed web | ||||||
| 	webFS embed.FS | 	webFS embed.FS | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	kb = 1024 |  | ||||||
| 	mb = 1024 * kb |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	flag.StringVar(&imagesDir, "images-dir", "images", "Sets the images dir") | 	var ppURL string | ||||||
|  |  | ||||||
| 	flag.StringVar(&collageDir, "collages-dir", "collages", "Sets the collages dir") | 	flag.StringVar(&collageDir, "collages-dir", "collages", "Sets the collages dir") | ||||||
|  | 	flag.StringVar(&photosDir, "photos-dir", "photos", "Cache directory for downloaded photos") | ||||||
| 	flag.BoolVar(&devMode, "dev", false, "Serve local assets during development") | 	flag.BoolVar(&devMode, "dev", false, "Serve local assets during development") | ||||||
| 	flag.StringVar(&listenAddr, "addr", "127.0.0.1:8767", "Web listen address, see https://pkg.go.dev/go.balki.me/anyhttp#readme-address-syntax") | 	flag.StringVar(&listenAddr, "addr", "127.0.0.1:8767", "Web listen address, see https://pkg.go.dev/go.balki.me/anyhttp#readme-address-syntax") | ||||||
|  | 	flag.StringVar(&ppURL, "pp-url", "", "Base url for photoprism") | ||||||
|  | 	flag.StringVar(&photoPrismToken, "pp-token", "", "API token for photoprism") | ||||||
|  |  | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
|  |  | ||||||
|  | 	photoPrismURL = func() *url.URL { | ||||||
|  | 		photoPrismURL, err := url.Parse(ppURL) | ||||||
|  | 		if err != nil { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 		return photoPrismURL | ||||||
|  | 	}() | ||||||
| 	collageNameGen = NewNameGen() | 	collageNameGen = NewNameGen() | ||||||
| 	imagesDirFs = os.DirFS(imagesDir) | 	imagesDirFs = os.DirFS(photosDir) | ||||||
| 	imagesURLPath := "images" |  | ||||||
| 	collagesPath := "collages" | 	collagesPath := "collages" | ||||||
|  | 	photosPath := "photos" | ||||||
|  |  | ||||||
| 	addFileServer := func(path, dir string) { | 	addFileServer := func(path, dir string) { | ||||||
| 		httpFileServer := http.FileServer(http.Dir(dir)) | 		httpFileServer := http.FileServer(http.Dir(dir)) | ||||||
| 		http.Handle("/"+path+"/", http.StripPrefix("/"+path, httpFileServer)) | 		http.Handle("/"+path+"/", http.StripPrefix("/"+path, httpFileServer)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	addFileServer(imagesURLPath, imagesDir) |  | ||||||
| 	addFileServer(collagesPath, collageDir) | 	addFileServer(collagesPath, collageDir) | ||||||
|  | 	addFileServer(photosPath, photosDir) | ||||||
|  |  | ||||||
| 	if devMode { | 	if devMode { | ||||||
| 		httpFileServer := http.FileServer(http.Dir("web")) | 		httpFileServer := http.FileServer(http.Dir("web")) | ||||||
| @@ -105,56 +114,65 @@ func main() { | |||||||
| 			w.WriteHeader(http.StatusInternalServerError) | 			w.WriteHeader(http.StatusInternalServerError) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		collageFile, err := MakeCollage(&collageReq) | 		collageFilePath, err := MakeCollage(&collageReq) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			slog.Error("failed to make collage", "error", err) | 			slog.Error("failed to make collage", "error", err) | ||||||
| 			w.WriteHeader(http.StatusInternalServerError) | 			w.WriteHeader(http.StatusInternalServerError) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		if _, err := w.Write([]byte(collageFile)); err != nil { | 		if _, err := w.Write([]byte(collageFilePath)); err != nil { | ||||||
| 			slog.Error("Failed to write collageFile", "error", err) | 			slog.Error("Failed to write collageFile", "error", err) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| 	http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		err := func() error { | 	http.HandleFunc("/get-albums", func(w http.ResponseWriter, r *http.Request) { | ||||||
| 			err := r.ParseMultipartForm(500 * mb) | 		albums, err := GetAlbums() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				return err | 			slog.Error("failed to get albums", "error", err) | ||||||
| 			} | 			w.WriteHeader(http.StatusInternalServerError) | ||||||
| 			files, ok := r.MultipartForm.File["photos"] |  | ||||||
| 			if !ok { |  | ||||||
| 				return fmt.Errorf("photos not found in request") |  | ||||||
| 			} |  | ||||||
| 			for i, f := range files { |  | ||||||
| 				imgFile, err := f.Open() |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 				defer imgFile.Close() |  | ||||||
| 				imagePath := path.Join(imagesDir, fmt.Sprintf("img%d.jpg", i+1)) |  | ||||||
| 				os.Remove(imagePath) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 				op, err := os.Create(imagePath) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 				defer op.Close() |  | ||||||
| 				_, err = io.Copy(op, imgFile) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			return nil |  | ||||||
| 		}() |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Error("Image upload failed", "error", err) |  | ||||||
| 			w.WriteHeader(http.StatusBadRequest) |  | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		http.Redirect(w, r, "/", http.StatusSeeOther) | 		jData, err := json.Marshal(albums) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Error("failed to marshal albums", "error", err) | ||||||
|  | 			w.WriteHeader(http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		w.Header().Set("Content-Type", "application/json") | ||||||
|  | 		w.Write(jData) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	http.HandleFunc("/load-photos/{albumID}", func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		albumID := r.PathValue("albumID") | ||||||
|  | 		photos, err := LoadPhotos(albumID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Error("failed to load photos", "error", err, "album", albumID) | ||||||
|  | 			w.WriteHeader(http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		photoURLs := []string{} | ||||||
|  | 		for _, photo := range photos { | ||||||
|  | 			photoURLs = append(photoURLs, fmt.Sprintf("/%s/%s.jpg", photosPath, photo.FileUID)) | ||||||
|  | 		} | ||||||
|  | 		go func() { | ||||||
|  | 			for _, photo := range photos { | ||||||
|  | 				_, err := DownloadPhoto(&photo) | ||||||
|  | 				if err != nil { | ||||||
|  | 					slog.Error("failed to download photo", "error", err, "album", albumID, "photoID", photo.FileUID) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  |  | ||||||
|  | 		jData, err := json.Marshal(photoURLs) | ||||||
|  | 		if err != nil { | ||||||
|  | 			slog.Error("failed to marshal photoURLs", "error", err) | ||||||
|  | 			w.WriteHeader(http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		w.Header().Set("Content-Type", "application/json") | ||||||
|  | 		w.Write(jData) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	addrType, server, done, err := anyhttp.Serve(listenAddr, idle.WrapHandler(nil)) | 	addrType, server, done, err := anyhttp.Serve(listenAddr, idle.WrapHandler(nil)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		slog.Error("anyhttp Serve failed", "error", err) | 		slog.Error("anyhttp Serve failed", "error", err) | ||||||
| @@ -186,3 +204,120 @@ func MakeCollage(req *collage.Request) (string, error) { | |||||||
| 	} | 	} | ||||||
| 	return collageFile, nil | 	return collageFile, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Album struct { | ||||||
|  | 	Title string `json:"Title"` | ||||||
|  | 	UID   string `json:"UID"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetAlbums() ([]Album, error) { | ||||||
|  | 	albumURL := func() string { | ||||||
|  | 		u := *photoPrismURL | ||||||
|  | 		u.Path = "/api/v1/albums" | ||||||
|  | 		v := url.Values{} | ||||||
|  | 		v.Add("count", "20") | ||||||
|  | 		v.Add("type", "album") | ||||||
|  | 		u.RawQuery = v.Encode() | ||||||
|  | 		return u.String() | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	req, err := http.NewRequest("GET", albumURL, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", photoPrismToken)) | ||||||
|  | 	resp, err := http.DefaultClient.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	respBytes, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	albums := []Album{} | ||||||
|  |  | ||||||
|  | 	err = json.Unmarshal(respBytes, &albums) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return albums, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Photo struct { | ||||||
|  | 	Hash    string `json:"Hash"` | ||||||
|  | 	FileUID string `json:"FileUID"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LoadPhotos(albumID string) ([]Photo, error) { | ||||||
|  | 	loadPhotosURL := func() string { | ||||||
|  | 		u := *photoPrismURL | ||||||
|  | 		u.Path = "/api/v1/photos" | ||||||
|  | 		v := url.Values{} | ||||||
|  | 		v.Add("count", "50") | ||||||
|  | 		v.Add("s", albumID) | ||||||
|  | 		v.Add("merged", "true") | ||||||
|  | 		v.Add("video", "false") | ||||||
|  | 		u.RawQuery = v.Encode() | ||||||
|  | 		return u.String() | ||||||
|  |  | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	req, err := http.NewRequest("GET", loadPhotosURL, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", photoPrismToken)) | ||||||
|  | 	resp, err := http.DefaultClient.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	respBytes, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	photos := []Photo{} | ||||||
|  | 	err = json.Unmarshal(respBytes, &photos) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return photos, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DownloadPhoto(photo *Photo) (string, error) { | ||||||
|  | 	photoPath := path.Join(photosDir, fmt.Sprintf("%s.jpg", photo.FileUID)) | ||||||
|  | 	_, err := os.Stat(photoPath) | ||||||
|  | 	if err == nil { | ||||||
|  | 		return photoPath, nil | ||||||
|  | 	} else if !os.IsNotExist(err) { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	out, err := os.Create(photoPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("failed to create collage output file, err: %w", err) | ||||||
|  | 	} | ||||||
|  | 	defer out.Close() | ||||||
|  | 	downloadPhotoURL := func() string { | ||||||
|  | 		u := *photoPrismURL | ||||||
|  | 		u.Path = fmt.Sprintf("/api/v1/dl/%s", photo.Hash) | ||||||
|  | 		return u.String() | ||||||
|  | 	}() | ||||||
|  | 	req, err := http.NewRequest("GET", downloadPhotoURL, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", photoPrismToken)) | ||||||
|  | 	resp, err := http.DefaultClient.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = io.Copy(out, resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return photoPath, nil | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								web/choose.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								web/choose.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | #album_photos { | ||||||
|  |     display: flex; | ||||||
|  |     gap: 1rem; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     background-color: lightblue; | ||||||
|  |     padding: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #album_photos img { | ||||||
|  |     width: 200px; | ||||||
|  |     height: 200px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #selected_photos { | ||||||
|  |     display: flex; | ||||||
|  |     gap: 1rem; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     background-color: lightyellow; | ||||||
|  |     padding: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #selected_photos img { | ||||||
|  |     width: 100px; | ||||||
|  |     height: 100px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .container { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     gap: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .controls { | ||||||
|  |     background-color: lightgrey; | ||||||
|  |     display: flex; | ||||||
|  |     gap: 1rem; | ||||||
|  |     padding: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #album_selector { | ||||||
|  |     width: 400px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .current { | ||||||
|  |     border: 5px solid; | ||||||
|  |     border-color: red; | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								web/choose.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								web/choose.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |   <head> | ||||||
|  |     <link rel="icon" href="data:;base64,iVBORw0KGgo=" /> | ||||||
|  |     <!-- DEVONLY --> <script src="http://localhost:35729/livereload.js"></script> | ||||||
|  |     <script src="choose.js" defer></script> | ||||||
|  |     <link rel="stylesheet" href="choose.css" /> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <dialog id="notice_dialog"> | ||||||
|  |       <p id="notice_p"></p> | ||||||
|  |       <form><button type="submit" formmethod="dialog">X</button></form> | ||||||
|  |     </dialog> | ||||||
|  |     <div class="container"> | ||||||
|  |       <div class="controls"> | ||||||
|  |         <button id="load_albums_button">Load Albums</button> | ||||||
|  |         <select id="album_selector" size="8"> </select> | ||||||
|  |         <button id="load_photos_button">Load Photos</button> | ||||||
|  |         <button id="make_collage_button">Make Collage</button> | ||||||
|  |       </div> | ||||||
|  |       <div id="selected_photos"> | ||||||
|  |         <div>1<img src="stock.svg" /> </div> | ||||||
|  |         <div>2<img src="stock.svg" /> </div> | ||||||
|  |         <div>3<img src="stock.svg" /> </div> | ||||||
|  |         <div>4<img src="stock.svg" /> </div> | ||||||
|  |         <div>5<img src="stock.svg" /> </div> | ||||||
|  |         <div>6<img src="stock.svg" /> </div> | ||||||
|  |         <div>7<img src="stock.svg" /> </div> | ||||||
|  |         <div>8<img src="stock.svg" /> </div> | ||||||
|  |         <div>9<img src="stock.svg" /> </div> | ||||||
|  |         <div>10<img src="stock.svg" /> </div> | ||||||
|  |         <div>11<img src="stock.svg" /> </div> | ||||||
|  |         <div>12<img src="stock.svg" /> </div> | ||||||
|  |       </div> | ||||||
|  |       <div id="album_photos"></div> | ||||||
|  |     </div> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										142
									
								
								web/choose.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								web/choose.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | // elements | ||||||
|  |  | ||||||
|  | /** @type {HTMLButtonElement} */ | ||||||
|  | let loadAlbumsBtn | ||||||
|  |  | ||||||
|  | /** @type {HTMLButtonElement} */ | ||||||
|  | let loadPhotosBtn | ||||||
|  |  | ||||||
|  | /** @type {HTMLButtonElement} */ | ||||||
|  | let makeCollageBtn | ||||||
|  |  | ||||||
|  | /** @type {HTMLSelectElement} */ | ||||||
|  | let albumSelect | ||||||
|  |  | ||||||
|  | /** @type {HTMLParagraphElement} */ | ||||||
|  | let noticeP | ||||||
|  |  | ||||||
|  | /** @type {HTMLDialogElement} */ | ||||||
|  | let noticeDialog | ||||||
|  |  | ||||||
|  | /** @type {HTMLDivElement} */ | ||||||
|  | let albumPhotosDiv | ||||||
|  |  | ||||||
|  | /** @type {HTMLDivElement} */ | ||||||
|  | let selectedPhotosDiv | ||||||
|  |  | ||||||
|  | /** @type {HTMLImageElement} */ | ||||||
|  | let selectedPhotoImg | ||||||
|  |  | ||||||
|  | function main() { | ||||||
|  |     loadAlbumsBtn = document.getElementById("load_albums_button") | ||||||
|  |     loadPhotosBtn = document.getElementById("load_photos_button") | ||||||
|  |     makeCollageBtn = document.getElementById("make_collage_button") | ||||||
|  |     albumSelect = document.getElementById("album_selector") | ||||||
|  |     noticeP = document.getElementById("notice_p") | ||||||
|  |     noticeDialog = document.getElementById("notice_dialog") | ||||||
|  |     albumPhotosDiv = document.getElementById("album_photos") | ||||||
|  |     selectedPhotosDiv = document.getElementById("selected_photos") | ||||||
|  |  | ||||||
|  |     loadAlbumsBtn.onclick = () => loadAlbums() | ||||||
|  |     loadPhotosBtn.onclick = () => loadPhotos() | ||||||
|  |     makeCollageBtn.onclick = () => gotoCollage() | ||||||
|  |  | ||||||
|  |     /** @type HTMLImageElement[] */ | ||||||
|  |     const selectedPhotos = selectedPhotosDiv.getElementsByTagName("img") | ||||||
|  |     for (const img of selectedPhotos) { | ||||||
|  |         img.onclick = () => { | ||||||
|  |             selectedPhotoImg?.classList.remove("current") | ||||||
|  |             selectedPhotoImg = img | ||||||
|  |             selectedPhotoImg.classList.add("current") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     selectedPhotos[0].click() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function loadAlbums() { | ||||||
|  |     (async () => { | ||||||
|  |         try { | ||||||
|  |             closeNotice() | ||||||
|  |             const resp = await fetch("get-albums") | ||||||
|  |             const albums = await resp.json() | ||||||
|  |             if(albums.length == 0) { | ||||||
|  |                 showNotice("No Albums found") | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             albumSelect.replaceChildren() // This empties existing options | ||||||
|  |             for(const album of albums) { | ||||||
|  |                 albumSelect.add(new Option(album.Title, album.UID)) | ||||||
|  |             } | ||||||
|  |         } catch(e) { | ||||||
|  |             console.log(e) | ||||||
|  |         } | ||||||
|  |     })(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function closeNotice() { | ||||||
|  |     noticeDialog.close() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** @param {string} notice */ | ||||||
|  | function showNotice(notice) { | ||||||
|  |     noticeP.textContent = notice | ||||||
|  |     noticeDialog.show() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function loadPhotos() { | ||||||
|  |     closeNotice() | ||||||
|  |     const selected = albumSelect.selectedOptions | ||||||
|  |     if(selected.length != 1) { | ||||||
|  |         showNotice("Select an album to load photos") | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  |     const [elem] = selected; | ||||||
|  |  | ||||||
|  |     (async () => { | ||||||
|  |         try { | ||||||
|  |             const resp = await fetch(`load-photos/${elem.value}`) | ||||||
|  |             /** @type String[] */ | ||||||
|  |             const photos = await resp.json() | ||||||
|  |             if(photos.length == 0) { | ||||||
|  |                 showNotice("No Photos found") | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             albumPhotosDiv.replaceChildren() | ||||||
|  |             for(const url of photos) { | ||||||
|  |                 const img = new Image() | ||||||
|  |                 img.src =  url | ||||||
|  |                 albumPhotosDiv.appendChild(img) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             /** @type HTMLImageElement[] */ | ||||||
|  |             const photoImgs = albumPhotosDiv.children | ||||||
|  |             for(const photo of photoImgs) { | ||||||
|  |                 photo.onclick = () => { | ||||||
|  |                     selectedPhotoImg.src = photo.src | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch(e) { | ||||||
|  |             console.log(e) | ||||||
|  |         } | ||||||
|  |     })(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function gotoCollage() { | ||||||
|  |     /** @type HTMLImageElement[] */ | ||||||
|  |     const selectedPhotos = selectedPhotosDiv.getElementsByTagName("img") | ||||||
|  |     /** @type String[] */ | ||||||
|  |     const photoUrls = []; | ||||||
|  |  | ||||||
|  |     for (const img of selectedPhotos) { | ||||||
|  |         if (!img.src.endsWith("stock.svg")) { | ||||||
|  |             photoUrls.push((new URL(img.src)).pathname) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     const encodedURLS = encodeURIComponent(JSON.stringify(photoUrls)) | ||||||
|  |     window.location.href = `index.html?urls=${encodedURLS}` | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | main() | ||||||
| @@ -10,36 +10,7 @@ | |||||||
|     justify-content: space-around; |     justify-content: space-around; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     flex: 25%; |     flex: 25%; | ||||||
|     overflow: scroll; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .img-container { |  | ||||||
|     /** margin: 30px; **/ |  | ||||||
|     display: flex; |  | ||||||
|     gap: 1rem; |  | ||||||
|     flex-wrap: wrap; |  | ||||||
|     justify-content: center; |  | ||||||
| } |  | ||||||
| .img-container img { |  | ||||||
|     width: 100px; |  | ||||||
|     height: 100px; |  | ||||||
|     border: 5px solid #999; |  | ||||||
| } |  | ||||||
| .img-container p { |  | ||||||
|     text-align: center; |  | ||||||
|     font-size: 2rem; |  | ||||||
| } |  | ||||||
| .toolbar { |  | ||||||
|     height: 30px; |  | ||||||
|     display: flex; |  | ||||||
|     gap: 1rem; |  | ||||||
|     justify-content: center; |  | ||||||
|     align-items: flex-start; |  | ||||||
| } |  | ||||||
| .selected-img { |  | ||||||
|     border: 5px solid lightgreen !important; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .imagebox { | .imagebox { | ||||||
|     padding: 2rem; |     padding: 2rem; | ||||||
|     flex: 75%; |     flex: 75%; | ||||||
| @@ -85,53 +56,61 @@ | |||||||
|     border: 1px solid; |     border: 1px solid; | ||||||
| } | } | ||||||
|  |  | ||||||
| .single div { | .single { | ||||||
|     width: 100%; |     display: grid; | ||||||
|     height: 100%; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .one-two { | .one-two { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-areas:  |     grid-template-areas:  | ||||||
|     "one two" |     "A B" | ||||||
|     "one three"; |     "A C"; | ||||||
| } | } | ||||||
| .one-two .img1{ | .one-two .img1{ | ||||||
|     grid-area: one; |     grid-area: A; | ||||||
| } | } | ||||||
|  |  | ||||||
| .half-leftright { | .half-topbottom { | ||||||
|     display:flex; |     display: grid; | ||||||
| } |  | ||||||
|  |  | ||||||
| .half-leftright .img { |  | ||||||
|     flex: 50%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .half-topbottom .img { |  | ||||||
|     width: 100%; |  | ||||||
|     height: 50%; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .two-one-two-leftright { | .two-one-two-leftright { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-areas: |     grid-template-areas: | ||||||
|     "one two two three" |     "A  B  B  C" | ||||||
|     "four two two five"; |     "D  B  B  E"; | ||||||
| } | } | ||||||
| .two-one-two-leftright .img2 { | .two-one-two-leftright .img1 { | ||||||
|     grid-area: two; |     grid-area: B; | ||||||
| } | } | ||||||
|  |  | ||||||
| .two-one-two-topbottom { | .two-one-two-topbottom { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-areas:  |     grid-template-areas:  | ||||||
|     "one two" |     "A  B" | ||||||
|     "three three" |     "C  C" | ||||||
|     "three three" |     "C  C" | ||||||
|     "four five" |     "D  E" | ||||||
| } | } | ||||||
|  |  | ||||||
| .two-one-two-topbottom .img3 { | .two-one-two-topbottom .img1 { | ||||||
|     grid-area: three; |     grid-area: C; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .two-row { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-areas:  | ||||||
|  |     "A B" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .three-row { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-areas: | ||||||
|  |     "A B C" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .four-row { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-areas: | ||||||
|  |     "A B C D" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,10 +12,11 @@ | |||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <div class="controls"> |             <div class="controls"> | ||||||
|  |  | ||||||
|  |                 <p><a href="choose.html">Select Images</a></p> | ||||||
|  |  | ||||||
|                 <label> |                 <label> | ||||||
|                     <span>Paper size</span> |                     <span>Paper size</span> | ||||||
|                     <select id="page_size_selector" size=8> |                     <select id="page_size_selector" size=6> | ||||||
|                           <option          value="letter-portrait"     > Letter (Portrait)  </option> |                           <option          value="letter-portrait"     > Letter (Portrait)  </option> | ||||||
|                           <option selected value="letter-landscape"    > Letter (Landscape) </option> |                           <option selected value="letter-landscape"    > Letter (Landscape) </option> | ||||||
|                           <option          value="fiveseven-portrait"  > 5 × 7 (Portrait)   </option> |                           <option          value="fiveseven-portrait"  > 5 × 7 (Portrait)   </option> | ||||||
| @@ -24,49 +25,35 @@ | |||||||
|                           <option          value="foursix-landscape"   > 4 × 6 (Landscape)  </option> |                           <option          value="foursix-landscape"   > 4 × 6 (Landscape)  </option> | ||||||
|                     </select> |                     </select> | ||||||
|                 </label> |                 </label> | ||||||
|                 <div> |  | ||||||
|                     <div class="toolbar"> |  | ||||||
|                         <button>Left</button> |  | ||||||
|                         <button>Right</button> |  | ||||||
|                     </div> |  | ||||||
|                     <div class="img-container"> |  | ||||||
|                         <div> <img src="images/img1.jpg"/>                        </div> |  | ||||||
|                         <div> <img class="selected-img" src="images/img2.jpg"/>   </div> |  | ||||||
|                         <div> <img src="images/img3.jpg"/>                        </div> |  | ||||||
|                         <div> <img src="images/img4.jpg"/>                        </div> |  | ||||||
|                         <div> <img src="images/img5.jpg"/>                        </div> |  | ||||||
|                         <div> <img src="images/img6.jpg"/>                        </div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|                 <ul class="templates"> |                 <ul class="templates"> | ||||||
|                     <li> |                     <li> | ||||||
|                         <div id="default_template" class="template single" data-collage-template="single"> |                         <div id="default_template" class="template single"> | ||||||
|                             <div class="img img1"></div> |                             <div class="img img1"></div> | ||||||
|                         </div> |                         </div> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li> |                     <li> | ||||||
|                         <div class="template one-two" data-collage-template="one-two"> |                         <div class="template one-two"> | ||||||
|                             <div class="img img1"></div> |                             <div class="img img1"></div> | ||||||
|                             <div class="img img2"></div> |                             <div class="img img2"></div> | ||||||
|                             <div class="img img3"></div> |                             <div class="img img3"></div> | ||||||
|                         </div> |                         </div> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li> |                     <li> | ||||||
|                         <div class="template half-leftright" data-collage-template="half-leftright"> |                         <div class="template two-row"> | ||||||
|                             <div class="img img1"></div> |                             <div class="img img1"></div> | ||||||
|                             <div class="img img2"></div> |                             <div class="img img2"></div> | ||||||
|                         </div> |                         </div> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li> |                     <li> | ||||||
|                         <div class="template half-topbottom" data-collage-template="half-topbottom"> |                         <div class="template half-topbottom"> | ||||||
|                             <div class="img img1"></div> |                             <div class="img img1"></div> | ||||||
|                             <div class="img img2"></div> |                             <div class="img img2"></div> | ||||||
|                         </div> |                         </div> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li> |                     <li> | ||||||
|                         <div class="template two-one-two-leftright" data-collage-template="two-one-two-leftright"> |                         <div class="template two-one-two-leftright"> | ||||||
|                             <div class="img img1"></div> |                             <div class="img img1"></div> | ||||||
|                             <div class="img img2"></div> |                             <div class="img img2"></div> | ||||||
|                             <div class="img img3"></div> |                             <div class="img img3"></div> | ||||||
| @@ -75,13 +62,56 @@ | |||||||
|                         </div> |                         </div> | ||||||
|                     </li> |                     </li> | ||||||
|                     <li> |                     <li> | ||||||
|                         <div class="template two-one-two-topbottom" data-collage-template="two-one-two-topbottom"> |                         <div class="template two-one-two-topbottom"> | ||||||
|                             <div class="img img1"></div> |                             <div class="img img1"></div> | ||||||
|                             <div class="img img2"></div> |                             <div class="img img2"></div> | ||||||
|                             <div class="img img3"></div> |                             <div class="img img3"></div> | ||||||
|                             <div class="img img4"></div> |                             <div class="img img4"></div> | ||||||
|                             <div class="img img5"></div> |                             <div class="img img5"></div> | ||||||
|                         </div> |                         </div> | ||||||
|  |                     <li> | ||||||
|  |                         <div class="template four-row"> | ||||||
|  |                             <div class="img img1"></div> | ||||||
|  |                             <div class="img img2"></div> | ||||||
|  |                             <div class="img img3"></div> | ||||||
|  |                             <div class="img img4"></div> | ||||||
|  |                             <div class="img img5"></div> | ||||||
|  |                             <div class="img img6"></div> | ||||||
|  |                             <div class="img img7"></div> | ||||||
|  |                             <div class="img img8"></div> | ||||||
|  |                         </div> | ||||||
|  |                     </li> | ||||||
|  |                     <li> | ||||||
|  |                         <div class="template four-row"> | ||||||
|  |                             <div class="img img1"></div> | ||||||
|  |                             <div class="img img2"></div> | ||||||
|  |                             <div class="img img3"></div> | ||||||
|  |                             <div class="img img4"></div> | ||||||
|  |                             <div class="img img5"></div> | ||||||
|  |                             <div class="img img6"></div> | ||||||
|  |                             <div class="img img7"></div> | ||||||
|  |                             <div class="img img8"></div> | ||||||
|  |                             <div class="img img9"></div> | ||||||
|  |                             <div class="img img10"></div> | ||||||
|  |                             <div class="img img11"></div> | ||||||
|  |                             <div class="img img12"></div> | ||||||
|  |                         </div> | ||||||
|  |                     </li> | ||||||
|  |                     <li> | ||||||
|  |                         <div class="template three-row"> | ||||||
|  |                             <div class="img img1"></div> | ||||||
|  |                             <div class="img img2"></div> | ||||||
|  |                             <div class="img img3"></div> | ||||||
|  |                             <div class="img img4"></div> | ||||||
|  |                             <div class="img img5"></div> | ||||||
|  |                             <div class="img img6"></div> | ||||||
|  |                             <div class="img img7"></div> | ||||||
|  |                             <div class="img img8"></div> | ||||||
|  |                             <div class="img img9"></div> | ||||||
|  |                             <div class="img img10"></div> | ||||||
|  |                             <div class="img img11"></div> | ||||||
|  |                             <div class="img img12"></div> | ||||||
|  |                         </div> | ||||||
|                     </li> |                     </li> | ||||||
|                 </ul> |                 </ul> | ||||||
|                 <button id="snapper" class="showbuton">Snap Collage</button> |                 <button id="snapper" class="showbuton">Snap Collage</button> | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								web/index.js
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								web/index.js
									
									
									
									
									
								
							| @@ -51,16 +51,8 @@ let collageUrlA | |||||||
|  |  | ||||||
| let crops = []; | let crops = []; | ||||||
| let pageSize = "letter-landscape"; | let pageSize = "letter-landscape"; | ||||||
| const imageUrls = [ | /** @type {String[]} */ | ||||||
|     , // images start with index 1 | let imageUrls = [] | ||||||
|     "images/img1.jpg", |  | ||||||
|     "images/img2.jpg", |  | ||||||
|     "images/img3.jpg", |  | ||||||
|     "images/img4.jpg", |  | ||||||
|     "images/img5.jpg", |  | ||||||
|     "images/img6.jpg", |  | ||||||
|     "images/img7.jpg", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| function main() { | function main() { | ||||||
| @@ -73,8 +65,14 @@ function main() { | |||||||
|     pageSizeSelect.onchange  = () => pageSizeChange() |     pageSizeSelect.onchange  = () => pageSizeChange() | ||||||
|  |  | ||||||
|     for(const tmpl of document.getElementsByClassName("template")) { |     for(const tmpl of document.getElementsByClassName("template")) { | ||||||
|  |         // Assumes second class in the template is the collage's template | ||||||
|  |         const [_, collageTemplate] = tmpl.classList | ||||||
|  |         tmpl.dataset.collageTemplate = collageTemplate | ||||||
|         tmpl.onclick = () => applyTemplate(tmpl) |         tmpl.onclick = () => applyTemplate(tmpl) | ||||||
|     } |     } | ||||||
|  |     const queryUrls = loadImageUrlsFromQuery() | ||||||
|  |     // Skipping first entry in array to make the images start with index 1 | ||||||
|  |     imageUrls = [,].concat(queryUrls) | ||||||
|  |  | ||||||
|     applyTemplate(document.getElementById("default_template")) |     applyTemplate(document.getElementById("default_template")) | ||||||
| } | } | ||||||
| @@ -147,7 +145,7 @@ function snap() { | |||||||
|         const fsy = elem.offsetTop - cot |         const fsy = elem.offsetTop - cot | ||||||
|         const [sx, sy, ex, ey] = cpie.get().points; |         const [sx, sy, ex, ey] = cpie.get().points; | ||||||
|         const photo = { |         const photo = { | ||||||
|             image: elem.dataset.collageImageUrl.slice("images/".length), |             image: elem.dataset.collageImageUrl.slice("/photos/".length), | ||||||
|             crop: { |             crop: { | ||||||
|                 start: { |                 start: { | ||||||
|                     x: parseInt(sx), |                     x: parseInt(sx), | ||||||
| @@ -173,6 +171,7 @@ function snap() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     (async () => { |     (async () => { | ||||||
|  |         collageUrlA.text = "Collage is being generated..."; | ||||||
|         const collagFile = await makeCollage(req) |         const collagFile = await makeCollage(req) | ||||||
|         collageUrlA.href = `collages/${collagFile}`; |         collageUrlA.href = `collages/${collagFile}`; | ||||||
|         collageUrlA.text = `${collagFile} generated`; |         collageUrlA.text = `${collagFile} generated`; | ||||||
| @@ -185,9 +184,7 @@ function pageSizeChange() { | |||||||
|     initCollage() |     initCollage() | ||||||
| } | } | ||||||
|  |  | ||||||
| /**  | /** @param {HTMLDivElement} templateDiv */ | ||||||
|  * @param {HTMLDivElement} templateDiv  |  | ||||||
|  */ |  | ||||||
| function applyTemplate(templateDiv) { | function applyTemplate(templateDiv) { | ||||||
|     document.getElementsByClassName("template-selected").item(0)?.classList.remove("template-selected") |     document.getElementsByClassName("template-selected").item(0)?.classList.remove("template-selected") | ||||||
|     /** @type {HTMLDivElement} */ |     /** @type {HTMLDivElement} */ | ||||||
| @@ -210,4 +207,10 @@ function applyTemplate(templateDiv) { | |||||||
|     initCollage() |     initCollage() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function loadImageUrlsFromQuery() { | ||||||
|  |     const params = new URLSearchParams(window.location.search) | ||||||
|  |     const urlsstr = params.get('urls') | ||||||
|  |     return JSON.parse(urlsstr) | ||||||
|  | } | ||||||
|  |  | ||||||
| main() | main() | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								web/stock.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								web/stock.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |     <circle cx="50" cy="25" r="20" stroke="green" stroke-width="4" fill="lightgrey" /> | ||||||
|  |     <polygon points="50,50 80,95 20,95" stroke="green" stroke-width="4" fill="lightgrey" /> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 252 B | 
| @@ -1,12 +0,0 @@ | |||||||
| <!DOCTYPE html> |  | ||||||
| <html> |  | ||||||
|     <head> |  | ||||||
|         <!-- DEVONLY --> <script src="http://localhost:35729/livereload.js"></script> |  | ||||||
|     </head> |  | ||||||
|     <body> |  | ||||||
|         <form action="/upload" method="POST" enctype="multipart/form-data"> |  | ||||||
|             <input name="photos" type=file multiple accept="image/*" /> |  | ||||||
|             <input type=submit> |  | ||||||
|         </form> |  | ||||||
|     </body> |  | ||||||
| </html> |  | ||||||
		Reference in New Issue
	
	Block a user