A simple way to edit images #7
|
|
@ -1,3 +1,4 @@
|
|||
import feather from "feather-icons/dist/feather.js";
|
||||
import Handlebars from "handlebars";
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
|
|
@ -7,12 +8,12 @@ Handlebars.registerHelper("submit_on", function (id, event) {
|
|||
|
||||
const processorFrame = Handlebars.compile(`
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>{{name}}</span>
|
||||
<a href="#" class="btn btn-sm btn-secondary float-end"
|
||||
<a href="#" class="float-end"
|
||||
data-action="edit-upload#removeProcessor"
|
||||
data-edit-upload-id-param="{{id}}"
|
||||
>X</a>
|
||||
><i data-feather="x" width="18" height="18"></i></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form data-role="processor-params" data-params-id="{{id}}">{{{props}}}</form>
|
||||
|
|
@ -78,6 +79,16 @@ export default class UploadEditController extends Controller {
|
|||
await this._removeProcessor(id);
|
||||
}
|
||||
|
||||
async saveUpload(ev) {
|
||||
ev.preventDefault();
|
||||
await this._save("replace");
|
||||
}
|
||||
|
||||
async saveNewUpload(ev) {
|
||||
ev.preventDefault();
|
||||
await this._save("copy");
|
||||
}
|
||||
|
||||
async updateProcessor(ev) {
|
||||
ev.preventDefault();
|
||||
let id = ev.params.id;
|
||||
|
|
@ -108,6 +119,8 @@ export default class UploadEditController extends Controller {
|
|||
});
|
||||
el.innerHTML += cardOuter;
|
||||
}
|
||||
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
async _createSession() {
|
||||
|
|
@ -179,6 +192,33 @@ export default class UploadEditController extends Controller {
|
|||
})
|
||||
}
|
||||
|
||||
async _save(mode) {
|
||||
if (!this._state || !this._state.session) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let resp = await fetch(`/sites/${this.siteIdValue}/imageedit/${this._state.session.guid}/save`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ mode })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
console.error("Save failed:", resp.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = await resp.json();
|
||||
window.location.href = `/sites/${this.siteIdValue}/uploads/${result.upload_id}`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async _doReturningState(fn) {
|
||||
try {
|
||||
this._state = await fn();
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ Starting weiro without any arguments will start the server.
|
|||
siteGroup.Patch("/imageedit/:sessionID", ieh.PatchSession)
|
||||
siteGroup.Post("/imageedit/:sessionID/processors", ieh.AddProcessor)
|
||||
siteGroup.Delete("/imageedit/:sessionID/processors/:processorID", ieh.DeleteProcessor)
|
||||
siteGroup.Post("/imageedit/:sessionID/save", ieh.Save)
|
||||
siteGroup.Get("/imageedit/:sessionID/preview/:versionID", ieh.Preview)
|
||||
|
||||
siteGroup.Get("/settings", ssh.General)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,27 @@ func (ieh ImageEditHandlers) DeleteProcessor(c fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (ieh ImageEditHandlers) Save(c fiber.Ctx) error {
|
||||
sessionID := c.Params("sessionID")
|
||||
if sessionID == "" {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
result, err := ieh.ImageEditService.Save(c.Context(), sessionID, req.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(http.StatusOK).JSON(result)
|
||||
}
|
||||
|
||||
func (ieh ImageEditHandlers) PatchSession(c fiber.Ctx) error {
|
||||
var req struct {
|
||||
UpdateProc *imgedit.UpdateProcessorReq `json:"processor"`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: categories.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
|
||||
package sqlgen
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
|
||||
package sqlgen
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: pages.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: pending_uploads.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: posts.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: pubtargets.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: sites.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: uploads.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
@ -18,7 +18,7 @@ func (q *Queries) DeleteUpload(ctx context.Context, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
const insertUpload = `-- name: InsertUpload :exec
|
||||
const insertUpload = `-- name: InsertUpload :one
|
||||
INSERT INTO uploads (
|
||||
site_id,
|
||||
guid,
|
||||
|
|
@ -43,8 +43,8 @@ type InsertUploadParams struct {
|
|||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) error {
|
||||
_, err := q.db.ExecContext(ctx, insertUpload,
|
||||
func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertUpload,
|
||||
arg.SiteID,
|
||||
arg.Guid,
|
||||
arg.MimeType,
|
||||
|
|
@ -54,7 +54,9 @@ func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) erro
|
|||
arg.Alt,
|
||||
arg.CreatedAt,
|
||||
)
|
||||
return err
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const selectUploadByID = `-- name: SelectUploadByID :one
|
||||
|
|
@ -154,3 +156,17 @@ func (q *Queries) UpdateUpload(ctx context.Context, arg UpdateUploadParams) erro
|
|||
_, err := q.db.ExecContext(ctx, updateUpload, arg.Alt, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUploadFileSize = `-- name: UpdateUploadFileSize :exec
|
||||
UPDATE uploads SET file_size = ? WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateUploadFileSizeParams struct {
|
||||
FileSize int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUploadFileSize(ctx context.Context, arg UpdateUploadFileSizeParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateUploadFileSize, arg.FileSize, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// sqlc v1.30.0
|
||||
// source: users.sql
|
||||
|
||||
package sqlgen
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func (db *Provider) SelectUploadBySiteIDAndSlug(ctx context.Context, siteID int6
|
|||
|
||||
func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error {
|
||||
if upload.ID == 0 {
|
||||
if err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{
|
||||
newID, err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{
|
||||
SiteID: upload.SiteID,
|
||||
Guid: upload.GUID,
|
||||
MimeType: upload.MIMEType,
|
||||
|
|
@ -53,9 +53,11 @@ func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error
|
|||
Slug: upload.Slug,
|
||||
Alt: upload.Alt,
|
||||
CreatedAt: upload.CreatedAt.Unix(),
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
upload.ID = newID
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +67,13 @@ func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error
|
|||
})
|
||||
}
|
||||
|
||||
func (db *Provider) UpdateUploadFileSize(ctx context.Context, id int64, fileSize int64) error {
|
||||
return db.queries.UpdateUploadFileSize(ctx, sqlgen.UpdateUploadFileSizeParams{
|
||||
FileSize: fileSize,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Provider) DeleteUpload(ctx context.Context, id int64) error {
|
||||
return db.queries.DeleteUpload(ctx, id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ func copyFile(src, dst string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (p *Provider) ReplaceFile(site models.Site, up models.Upload, srcPath string) error {
|
||||
fullPath := p.uploadFileName(site, up)
|
||||
return copyFile(srcPath, fullPath)
|
||||
}
|
||||
|
||||
func (p *Provider) OpenUpload(site models.Site, up models.Upload) (io.ReadCloser, error) {
|
||||
fullPath := p.uploadFileName(site, up)
|
||||
return os.Open(fullPath)
|
||||
|
|
|
|||
|
|
@ -175,6 +175,58 @@ func (s *Service) UpdateProcessor(ctx context.Context, sessionID string, req Upd
|
|||
return session, nil
|
||||
}
|
||||
|
||||
type SaveResult struct {
|
||||
UploadID int64 `json:"upload_id"`
|
||||
}
|
||||
|
||||
func (s *Service) Save(ctx context.Context, sessionID string, mode string) (*SaveResult, error) {
|
||||
session, err := s.loadAndVerifySession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(session.Processors) == 0 {
|
||||
return nil, fmt.Errorf("no processors in session")
|
||||
}
|
||||
|
||||
lastProc := session.Processors[len(session.Processors)-1]
|
||||
finalImagePath := fmt.Sprintf("%v/%v/%v.%v", s.scratchDir, session.GUID, lastProc.VersionID, session.ImageExt)
|
||||
|
||||
var mimeType string
|
||||
switch session.ImageExt {
|
||||
case "jpg", "jpeg":
|
||||
mimeType = "image/jpeg"
|
||||
case "png":
|
||||
mimeType = "image/png"
|
||||
}
|
||||
|
||||
var uploadID int64
|
||||
switch mode {
|
||||
case "replace":
|
||||
upload, err := s.uploadService.ReplaceUploadFile(ctx, session.BaseUploadID, finalImagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadID = upload.ID
|
||||
case "copy":
|
||||
baseUpload, _, err := s.uploadService.OpenUpload(ctx, session.BaseUploadID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upload, err := s.uploadService.CreateUploadFromFile(ctx, finalImagePath, baseUpload.Filename, mimeType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadID = upload.ID
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown save mode: %v", mode)
|
||||
}
|
||||
|
||||
s.sessionStore.delete(session.GUID)
|
||||
|
||||
return &SaveResult{UploadID: uploadID}, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadAndVerifySession(ctx context.Context, sessionID string) (*models.ImageEditSession, error) {
|
||||
site, user, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ func (ss *sessionStore) get(guid string) (*models.ImageEditSession, error) {
|
|||
return &sessionData, nil
|
||||
}
|
||||
|
||||
func (ss *sessionStore) delete(guid string) {
|
||||
os.RemoveAll(filepath.Join(ss.baseDir, guid))
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import (
|
|||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
)
|
||||
|
|
@ -67,6 +70,75 @@ func (s *Service) renderCopyTemplate(upload models.Upload) string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
func (s *Service) ReplaceUploadFile(ctx context.Context, uploadID int64, srcPath string) (models.Upload, error) {
|
||||
site, _, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
upload, err := s.db.SelectUploadByID(ctx, uploadID)
|
||||
if err != nil {
|
||||
return models.Upload{}, err
|
||||
} else if upload.SiteID != site.ID {
|
||||
return models.Upload{}, models.NotFoundError
|
||||
}
|
||||
|
||||
if err := s.up.ReplaceFile(site, upload, srcPath); err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
upload.FileSize = stat.Size()
|
||||
|
||||
if err := s.db.UpdateUploadFileSize(ctx, upload.ID, upload.FileSize); err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateUploadFromFile(ctx context.Context, srcPath string, filename string, mimeType string) (models.Upload, error) {
|
||||
site, _, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
newUploadGUID := models.NewNanoID()
|
||||
newTime := time.Now().UTC()
|
||||
newSlug := filepath.Join(
|
||||
fmt.Sprintf("%04d", newTime.Year()),
|
||||
fmt.Sprintf("%02d", newTime.Month()),
|
||||
newUploadGUID+filepath.Ext(filename),
|
||||
)
|
||||
|
||||
newUpload := models.Upload{
|
||||
SiteID: site.ID,
|
||||
GUID: models.NewNanoID(),
|
||||
FileSize: stat.Size(),
|
||||
MIMEType: mimeType,
|
||||
Filename: filename,
|
||||
CreatedAt: newTime,
|
||||
Slug: newSlug,
|
||||
}
|
||||
if err := s.db.SaveUpload(ctx, &newUpload); err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
if err := s.up.AdoptFile(site, newUpload, srcPath); err != nil {
|
||||
return models.Upload{}, err
|
||||
}
|
||||
|
||||
return newUpload, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListUploads(ctx context.Context) (res []UploadWithURL, _ error) {
|
||||
site, _, err := s.fetchSiteAndUser(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SELECT * FROM uploads WHERE id = ? LIMIT 1;
|
|||
-- name: SelectUploadBySiteIDAndSlug :one
|
||||
SELECT * FROM uploads WHERE site_id = ? AND slug = ? LIMIT 1;
|
||||
|
||||
-- name: InsertUpload :exec
|
||||
-- name: InsertUpload :one
|
||||
INSERT INTO uploads (
|
||||
site_id,
|
||||
guid,
|
||||
|
|
@ -23,5 +23,8 @@ RETURNING id;
|
|||
-- name: UpdateUpload :exec
|
||||
UPDATE uploads SET alt = ? WHERE id = ?;
|
||||
|
||||
-- name: UpdateUploadFileSize :exec
|
||||
UPDATE uploads SET file_size = ? WHERE id = ?;
|
||||
|
||||
-- name: DeleteUpload :exec
|
||||
DELETE FROM uploads WHERE id = ?;
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="col-md-9 m-3">
|
||||
<button class="btn btn-primary" data-action="edit-upload#saveUpload">Save</button>
|
||||
<button class="btn btn-secondary" data-action="edit-upload#saveNewUpload">Save as Copy</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
data-show-upload-site-id-value="{{ .upload.Upload.SiteID }}"
|
||||
data-show-upload-upload-id-value="{{ .upload.Upload.ID }}">
|
||||
<button class="btn btn-outline-dark" data-action="show-upload#copy">Copy HTML</button>
|
||||
<span>
|
||||
<a href="/sites/{{ .site.ID }}/uploads/{{ .upload.Upload.ID }}/edit" class="btn btn-secondary">Edit</a>
|
||||
<button class="btn btn-danger" data-action="show-upload#delete">Delete</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<figure>
|
||||
|
|
|
|||
Loading…
Reference in a new issue