A simple way to edit images #7
|
|
@ -36,10 +36,15 @@ const processors = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class UploadEditController extends Controller {
|
export default class UploadEditController extends Controller {
|
||||||
static targets = ['processList'];
|
static targets = ['processList', 'preview'];
|
||||||
|
static values = {
|
||||||
|
uploadId: Number,
|
||||||
|
siteId: Number,
|
||||||
|
};
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this._rebuildProcessList();
|
this._rebuildProcessList();
|
||||||
|
this._createSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
_rebuildProcessList() {
|
_rebuildProcessList() {
|
||||||
|
|
@ -56,4 +61,22 @@ export default class UploadEditController extends Controller {
|
||||||
el.innerHTML = cardOuter;
|
el.innerHTML = cardOuter;
|
||||||
// END TEMP
|
// END TEMP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _createSession() {
|
||||||
|
try {
|
||||||
|
let resp = await fetch(`/sites/${this.siteIdValue}/imageedit/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
"base_upload": this.uploadIdValue,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
this._state = await resp.json();
|
||||||
|
this.previewTarget.src = this._state.preview_url;
|
||||||
|
|
||||||
|
console.log("Session created");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +111,7 @@ Starting weiro without any arguments will start the server.
|
||||||
lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth}
|
lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth}
|
||||||
ph := handlers.PostsHandler{PostService: svcs.Posts, CategoryService: svcs.Categories}
|
ph := handlers.PostsHandler{PostService: svcs.Posts, CategoryService: svcs.Categories}
|
||||||
uh := handlers.UploadsHandler{UploadsService: svcs.Uploads}
|
uh := handlers.UploadsHandler{UploadsService: svcs.Uploads}
|
||||||
|
ieh := handlers.ImageEditHandlers{ImageEditService: svcs.ImageEdit}
|
||||||
ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites}
|
ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites}
|
||||||
ch := handlers.CategoriesHandler{CategoryService: svcs.Categories}
|
ch := handlers.CategoriesHandler{CategoryService: svcs.Categories}
|
||||||
pgh := handlers.PagesHandler{PageService: svcs.Pages}
|
pgh := handlers.PagesHandler{PageService: svcs.Pages}
|
||||||
|
|
@ -151,6 +152,9 @@ Starting weiro without any arguments will start the server.
|
||||||
siteGroup.Delete("/uploads/:uploadID", uh.Delete)
|
siteGroup.Delete("/uploads/:uploadID", uh.Delete)
|
||||||
siteGroup.Get("/uploads/:uploadID/edit", uh.Edit)
|
siteGroup.Get("/uploads/:uploadID/edit", uh.Edit)
|
||||||
|
|
||||||
|
siteGroup.Post("/imageedit", ieh.Create)
|
||||||
|
siteGroup.Get("/imageedit/:sessionID/preview/:versionID", ieh.Preview)
|
||||||
|
|
||||||
siteGroup.Get("/settings", ssh.General)
|
siteGroup.Get("/settings", ssh.General)
|
||||||
siteGroup.Post("/settings", ssh.UpdateGeneral)
|
siteGroup.Post("/settings", ssh.UpdateGeneral)
|
||||||
|
|
||||||
|
|
|
||||||
67
handlers/imageedit.go
Normal file
67
handlers/imageedit.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/services/imgedit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageEditHandlers struct {
|
||||||
|
ImageEditService *imgedit.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ieh ImageEditHandlers) Create(c fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
BaseUploadID int64 `json:"base_upload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Bind().JSON(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ieh.ImageEditService.NewSession(c.Context(), req.BaseUploadID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp = struct {
|
||||||
|
Session models.ImageEditSession `json:"session"`
|
||||||
|
PreviewURL string `json:"preview_url"`
|
||||||
|
}{
|
||||||
|
Session: res,
|
||||||
|
PreviewURL: res.PreviewURL(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(http.StatusCreated).JSON(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ieh ImageEditHandlers) Preview(c fiber.Ctx) error {
|
||||||
|
log.Printf("Previewing image edit session %v/%v", c.Params("sessionID"), c.Params("versionID"))
|
||||||
|
sessionID := c.Params("sessionID")
|
||||||
|
versionID := c.Params("versionID")
|
||||||
|
|
||||||
|
mimeTime, rw, err := ieh.ImageEditService.LoadImageVersion(c.Context(), sessionID, versionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Type", mimeTime)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
return c.SendStreamWriter(func(w *bufio.Writer) {
|
||||||
|
rw, err := rw()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rw.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(w, rw)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -8,3 +8,4 @@ var NotFoundError = errors.New("not found")
|
||||||
var SiteRequiredError = errors.New("site required")
|
var SiteRequiredError = errors.New("site required")
|
||||||
var DeleteDebounceError = errors.New("permanent delete too soon, try again in a few seconds")
|
var DeleteDebounceError = errors.New("permanent delete too soon, try again in a few seconds")
|
||||||
var SlugConflictError = errors.New("a record with this slug already exists")
|
var SlugConflictError = errors.New("a record with this slug already exists")
|
||||||
|
var UnsupportedImageFormat = errors.New("unsupported image format")
|
||||||
|
|
|
||||||
59
models/imgedit.go
Normal file
59
models/imgedit.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageEditSession struct {
|
||||||
|
GUID string `json:"guid"`
|
||||||
|
SiteID int64 `json:"siteId"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
BaseUploadID int64 `json:"baseUploadId"`
|
||||||
|
ImageExt string `json:"imageExt"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
Processors []ImageEditProcessor `json:"processors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ieh ImageEditSession) PreviewURL() string {
|
||||||
|
return fmt.Sprintf("/sites/%v/imageedit/%v/preview/%v", ieh.SiteID, ieh.GUID, ieh.Processors[len(ieh.Processors)-1].VersionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ieh *ImageEditSession) RecalcVersionIDs() {
|
||||||
|
for i, p := range ieh.Processors {
|
||||||
|
if i == 0 {
|
||||||
|
p.SetVersionID("")
|
||||||
|
} else {
|
||||||
|
p.SetVersionID(ieh.Processors[i-1].VersionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ieh.Processors[i] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageEditProcessor struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Props json.RawMessage `json:"props"`
|
||||||
|
|
||||||
|
// VersionID is a unique hash of the particular processor. This includes the version ID of the previous processor,
|
||||||
|
// thereby causing a change of one processor to affect the version IDs of processors down the line.
|
||||||
|
VersionID string `json:"versionId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ieh *ImageEditProcessor) SetVersionID(previousVersionID string) {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(previousVersionID)
|
||||||
|
sb.WriteString("-")
|
||||||
|
sb.WriteString(ieh.Type)
|
||||||
|
sb.WriteString("-")
|
||||||
|
sb.WriteString(string(ieh.Props))
|
||||||
|
ieh.VersionID = fmt.Sprintf("%x", md5.Sum([]byte(sb.String())))
|
||||||
|
}
|
||||||
|
|
||||||
|
type CopyUploadProps struct {
|
||||||
|
UploadID int64 `json:"uploadId"`
|
||||||
|
}
|
||||||
91
services/imgedit/processing.go
Normal file
91
services/imgedit/processing.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package imgedit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) reprocess(ctx context.Context, session models.ImageEditSession) (imageSource, error) {
|
||||||
|
var img imageSource
|
||||||
|
|
||||||
|
for _, p := range session.Processors {
|
||||||
|
// Check if there's currently a cached image of this processor
|
||||||
|
cachedImageFile := filepath.Join(s.scratchDir, session.GUID, fmt.Sprintf("%v.%v", p.VersionID, session.ImageExt))
|
||||||
|
if s, err := os.Stat(cachedImageFile); err == nil && !s.IsDir() {
|
||||||
|
img = fileImageSource(cachedImageFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to process the image
|
||||||
|
var srcImg image.Image
|
||||||
|
if img != nil {
|
||||||
|
var err error
|
||||||
|
srcImg, err = img.image()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resImg, err := s.processImage(ctx, srcImg, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the processed image
|
||||||
|
if err := imaging.Save(resImg, cachedImageFile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
img = imageImageSource{resImg}
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processImage(ctx context.Context, srcImg image.Image, processor models.ImageEditProcessor) (image.Image, error) {
|
||||||
|
switch processor.Type {
|
||||||
|
case "copy-upload":
|
||||||
|
var p models.CopyUploadProps
|
||||||
|
if err := json.Unmarshal(processor.Props, &p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, rc, err := s.uploadService.OpenUpload(ctx, p.UploadID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := rc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return imaging.Decode(f)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown processor type: %v", processor.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageSource interface {
|
||||||
|
image() (image.Image, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileImageSource string
|
||||||
|
|
||||||
|
func (f fileImageSource) image() (image.Image, error) {
|
||||||
|
return imaging.Open(string(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageImageSource struct {
|
||||||
|
img image.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i imageImageSource) image() (image.Image, error) {
|
||||||
|
return i.img, nil
|
||||||
|
}
|
||||||
116
services/imgedit/service.go
Normal file
116
services/imgedit/service.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package imgedit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/services/uploads"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
scratchDir string
|
||||||
|
uploadService *uploads.Service
|
||||||
|
sessionStore *sessionStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
uploadService *uploads.Service,
|
||||||
|
scratchDir string,
|
||||||
|
) *Service {
|
||||||
|
return &Service{
|
||||||
|
scratchDir: scratchDir,
|
||||||
|
uploadService: uploadService,
|
||||||
|
sessionStore: &sessionStore{baseDir: scratchDir},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) LoadImageVersion(ctx context.Context, sessionID string, versionID string) (mimeType string, rw func() (io.ReadCloser, error), err error) {
|
||||||
|
site, user, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := s.sessionStore.get(sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
} else if session.SiteID != site.ID || session.UserID != user.ID {
|
||||||
|
return "", nil, models.PermissionError
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.sessionStore.getImage(session, versionID+"."+session.ImageExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (models.ImageEditSession, error) {
|
||||||
|
site, user, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return models.ImageEditSession{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, _, err := s.uploadService.OpenUpload(ctx, baseUploadID)
|
||||||
|
if err != nil {
|
||||||
|
return models.ImageEditSession{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ext string
|
||||||
|
switch upload.MIMEType {
|
||||||
|
case "image/jpeg":
|
||||||
|
ext = "jpg"
|
||||||
|
case "image/png":
|
||||||
|
ext = "png"
|
||||||
|
default:
|
||||||
|
return models.ImageEditSession{}, models.UnsupportedImageFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
newSession := models.ImageEditSession{
|
||||||
|
GUID: models.NewNanoID(),
|
||||||
|
SiteID: site.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
BaseUploadID: baseUploadID,
|
||||||
|
ImageExt: ext,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
Processors: []models.ImageEditProcessor{
|
||||||
|
{
|
||||||
|
Type: "copy-upload",
|
||||||
|
Props: mustToJSON(models.CopyUploadProps{UploadID: baseUploadID}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
newSession.RecalcVersionIDs()
|
||||||
|
if err := s.sessionStore.create(newSession); err != nil {
|
||||||
|
return models.ImageEditSession{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.reprocess(ctx, newSession); err != nil {
|
||||||
|
return models.ImageEditSession{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSession, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) fetchSiteAndUser(ctx context.Context) (models.Site, models.User, error) {
|
||||||
|
user, ok := models.GetUser(ctx)
|
||||||
|
if !ok {
|
||||||
|
return models.Site{}, models.User{}, models.UserRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return models.Site{}, models.User{}, models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
if site.OwnerID != user.ID {
|
||||||
|
return models.Site{}, models.User{}, models.PermissionError
|
||||||
|
}
|
||||||
|
|
||||||
|
return site, user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustToJSON(a any) json.RawMessage {
|
||||||
|
b, _ := json.Marshal(a)
|
||||||
|
return b
|
||||||
|
}
|
||||||
66
services/imgedit/store.go
Normal file
66
services/imgedit/store.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package imgedit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sessionStore struct {
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *sessionStore) create(newSession models.ImageEditSession) error {
|
||||||
|
sessionMeta, err := json.Marshal(newSession)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(ss.baseDir, newSession.GUID), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(ss.baseDir, newSession.GUID, "session.json"), sessionMeta, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *sessionStore) get(guid string) (models.ImageEditSession, error) {
|
||||||
|
sessionDataBts, err := os.ReadFile(filepath.Join(ss.baseDir, guid, "session.json"))
|
||||||
|
if err != nil {
|
||||||
|
return models.ImageEditSession{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData := models.ImageEditSession{}
|
||||||
|
if err := json.Unmarshal(sessionDataBts, &sessionData); err != nil {
|
||||||
|
return models.ImageEditSession{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *sessionStore) getImage(session models.ImageEditSession, imageFilename string) (string, func() (io.ReadCloser, error), error) {
|
||||||
|
fullPath := filepath.Join(ss.baseDir, session.GUID, imageFilename)
|
||||||
|
if s, err := os.Stat(fullPath); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
} else if s.IsDir() {
|
||||||
|
return "", nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimeType string
|
||||||
|
switch filepath.Ext(imageFilename) {
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
case ".png":
|
||||||
|
mimeType = "image/png"
|
||||||
|
default:
|
||||||
|
return "", nil, models.UnsupportedImageFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return mimeType, func() (io.ReadCloser, error) {
|
||||||
|
return os.Open(fullPath)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
||||||
"lmika.dev/lmika/weiro/services/auth"
|
"lmika.dev/lmika/weiro/services/auth"
|
||||||
"lmika.dev/lmika/weiro/services/categories"
|
"lmika.dev/lmika/weiro/services/categories"
|
||||||
|
"lmika.dev/lmika/weiro/services/imgedit"
|
||||||
"lmika.dev/lmika/weiro/services/pages"
|
"lmika.dev/lmika/weiro/services/pages"
|
||||||
"lmika.dev/lmika/weiro/services/posts"
|
"lmika.dev/lmika/weiro/services/posts"
|
||||||
"lmika.dev/lmika/weiro/services/publisher"
|
"lmika.dev/lmika/weiro/services/publisher"
|
||||||
|
|
@ -23,6 +24,7 @@ type Services struct {
|
||||||
Posts *posts.Service
|
Posts *posts.Service
|
||||||
Sites *sites.Service
|
Sites *sites.Service
|
||||||
Uploads *uploads.Service
|
Uploads *uploads.Service
|
||||||
|
ImageEdit *imgedit.Service
|
||||||
Categories *categories.Service
|
Categories *categories.Service
|
||||||
Pages *pages.Service
|
Pages *pages.Service
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +43,7 @@ func New(cfg config.Config) (*Services, error) {
|
||||||
postService := posts.New(dbp, publisherQueue)
|
postService := posts.New(dbp, publisherQueue)
|
||||||
siteService := sites.New(dbp)
|
siteService := sites.New(dbp)
|
||||||
uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
|
uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
|
||||||
|
imageEditService := imgedit.New(uploadService, filepath.Join(cfg.ScratchDir, "imageedit"))
|
||||||
categoriesService := categories.New(dbp, publisherQueue)
|
categoriesService := categories.New(dbp, publisherQueue)
|
||||||
pagesService := pages.New(dbp, publisherQueue)
|
pagesService := pages.New(dbp, publisherQueue)
|
||||||
|
|
||||||
|
|
@ -52,6 +55,7 @@ func New(cfg config.Config) (*Services, error) {
|
||||||
Posts: postService,
|
Posts: postService,
|
||||||
Sites: siteService,
|
Sites: siteService,
|
||||||
Uploads: uploadService,
|
Uploads: uploadService,
|
||||||
|
ImageEdit: imageEditService,
|
||||||
Categories: categoriesService,
|
Categories: categoriesService,
|
||||||
Pages: pagesService,
|
Pages: pagesService,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
<main class="flex-grow-1 flex-shrink-1">
|
<main class="flex-grow-1 flex-shrink-1">
|
||||||
<div class="flex-grow-1 flex-shrink-1 d-flex flex-column">
|
<div class="flex-grow-1 flex-shrink-1 d-flex flex-column">
|
||||||
<div class="row flex-grow-1 flex-shrink-1 m-0" data-controller="edit-upload">
|
<div class="row flex-grow-1 flex-shrink-1 m-0"
|
||||||
|
data-controller="edit-upload"
|
||||||
|
data-edit-upload-site-id-value="{{ .site.ID }}"
|
||||||
|
data-edit-upload-upload-id-value="{{ .upload.Upload.ID }}"
|
||||||
|
>
|
||||||
<figure class="col-md-9 p-3">
|
<figure class="col-md-9 p-3">
|
||||||
<img src="{{ .upload.URL }}" alt="{{ .upload.Upload.Alt }}" class="img-fluid">
|
<img data-edit-upload-target="preview" src="{{ .upload.URL }}" alt="{{ .upload.Upload.Alt }}" class="img-fluid">
|
||||||
</figure>
|
</figure>
|
||||||
<div class="col-md-3 p-3">
|
<div class="col-md-3 p-3">
|
||||||
<div data-edit-upload-target="processList"></div>
|
<div data-edit-upload-target="processList"></div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue