feat: update memo detail page (#1682)

* feat: update memo detail page

* chore: update
This commit is contained in:
boojack 2023-05-20 08:39:39 +08:00 committed by GitHub
parent 04124a2ace
commit b40571095d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 144 additions and 186 deletions

View File

@ -1,76 +0,0 @@
package common
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"github.com/disintegration/imaging"
)
const (
ThumbnailDir = ".thumbnail_cache"
ThumbnailSize = 302 // Thumbnail size should be defined by frontend
)
func ResizeImageFile(dst, src string, mime string) error {
srcBytes, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("Failed to open %s: %s", src, err)
}
dstBytes, err := ResizeImageBlob(srcBytes, ThumbnailSize, mime)
if err != nil {
return fmt.Errorf("Failed to resise %s: %s", src, err)
}
err = os.MkdirAll(filepath.Dir(dst), os.ModePerm)
if err != nil {
return fmt.Errorf("Failed to mkdir for %s: %s", dst, err)
}
err = os.WriteFile(dst, dstBytes, 0666)
if err != nil {
return fmt.Errorf("Failed to write %s: %s", dst, err)
}
return nil
}
func ResizeImageBlob(data []byte, maxSize int, mime string) ([]byte, error) {
var err error
var oldImage image.Image
switch strings.ToLower(mime) {
case "image/jpeg":
oldImage, err = jpeg.Decode(bytes.NewReader(data))
case "image/png":
oldImage, err = png.Decode(bytes.NewReader(data))
default:
return nil, fmt.Errorf("mime %s is not support", mime)
}
if err != nil {
return nil, err
}
newImage := imaging.Resize(oldImage, maxSize, 0, imaging.NearestNeighbor)
var newBuffer bytes.Buffer
switch mime {
case "image/jpeg":
err = jpeg.Encode(&newBuffer, newImage, nil)
case "image/png":
err = png.Encode(&newBuffer, newImage)
}
if err != nil {
return nil, err
}
return newBuffer.Bytes(), nil
}

6
go.mod
View File

@ -20,12 +20,12 @@ require (
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.1.0
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a
golang.org/x/mod v0.6.0
golang.org/x/mod v0.8.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.5.0
)
require golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
require golang.org/x/image v0.7.0 // indirect
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
@ -69,7 +69,7 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.1.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect

24
go.sum
View File

@ -261,6 +261,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -281,6 +282,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
@ -298,8 +300,9 @@ golang.org/x/exp v0.0.0-20230111222715-75897c7a292a h1:/YWeLOBWYV5WAQORVPkZF3Pq9
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -321,8 +324,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -354,6 +358,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -377,6 +383,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -414,18 +422,24 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -478,6 +492,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/disintegration/imaging"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
@ -30,6 +31,9 @@ const (
// This is unrelated to maximum upload size limit, which is now set through system setting.
maxUploadBufferSizeBytes = 32 << 20
MebiByte = 1024 * 1024
// thumbnailImagePath is the directory to store image thumbnails.
thumbnailImagePath = ".thumbnail_cache"
)
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
@ -163,14 +167,6 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to copy file").SetInternal(err)
}
if filetype == "image/jpeg" || filetype == "image/png" {
thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, publicID)
err := common.ResizeImageFile(thumbnailPath, filePath, filetype)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate thumbnail").SetInternal(err)
}
}
resourceCreate = &api.ResourceCreate{
CreatorID: userID,
Filename: filename,
@ -323,14 +319,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
if resource.InternalPath != "" {
err := os.Remove(resource.InternalPath)
if err != nil {
if err := os.Remove(resource.InternalPath); err != nil {
log.Warn(fmt.Sprintf("failed to delete local file with path %s", resource.InternalPath), zap.Error(err))
}
thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, resource.PublicID)
err = os.Remove(thumbnailPath)
if err != nil {
thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, resource.PublicID)
if err := os.Remove(thumbnailPath); err != nil {
log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err))
}
}
@ -423,22 +417,6 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
blob := resource.Blob
if resource.InternalPath != "" {
resourcePath := resource.InternalPath
if c.QueryParam("thumbnail") == "1" && (resource.Type == "image/jpeg" || resource.Type == "image/png") {
thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, resource.PublicID)
if _, err := os.Stat(thumbnailPath); err == nil {
resourcePath = thumbnailPath
} else if os.IsNotExist(err) {
err := common.ResizeImageFile(thumbnailPath, resourcePath, resource.Type)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to resize resource: %s", resourcePath)).SetInternal(err)
}
resourcePath = thumbnailPath
} else {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to check resource thumbnail stat: %s", thumbnailPath)).SetInternal(err)
}
}
src, err := os.Open(resourcePath)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", resourcePath)).SetInternal(err)
@ -450,6 +428,36 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
}
}
if c.QueryParam("thumbnail") == "1" && common.HasPrefixes(resource.Type, "image/png", "image/jpeg") {
ext := filepath.Ext(filename)
thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, resource.PublicID+ext)
if _, err := os.Stat(thumbnailPath); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to check thumbnail image stat: %s", thumbnailPath)).SetInternal(err)
}
reader := bytes.NewReader(blob)
src, err := imaging.Decode(reader)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to decode thumbnail image: %s", thumbnailPath)).SetInternal(err)
}
thumbnailImage := imaging.Resize(src, 512, 0, imaging.Lanczos)
if err := imaging.Save(thumbnailImage, thumbnailPath); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to resize thumbnail image: %s", thumbnailPath)).SetInternal(err)
}
}
src, err := os.Open(thumbnailPath)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", thumbnailPath)).SetInternal(err)
}
defer src.Close()
blob, err = io.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to read the local resource: %s", thumbnailPath)).SetInternal(err)
}
}
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
resourceType := strings.ToLower(resource.Type)

View File

@ -4,7 +4,7 @@ import { useMemoStore } from "@/store/module";
import { getDateTimeString } from "@/helpers/datetime";
import useToggle from "@/hooks/useToggle";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import MemoResourceListView from "./MemoResourceListView";
import "@/less/memo.less";
interface Props {
@ -67,7 +67,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
</div>
</div>
<MemoContent content={memo.content} />
<MemoResources resourceList={memo.resourceList} />
<MemoResourceListView resourceList={memo.resourceList} />
</div>
);
};

View File

@ -1,4 +1,4 @@
import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete, Tooltip } from "@mui/joy";
import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete } from "@mui/joy";
import React, { useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
@ -215,31 +215,29 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => {
</div>
<List size="sm" sx={{ width: "100%" }}>
{fileList.map((file, index) => (
<Tooltip title={file.name} key={file.name} placement="top">
<ListItem className="flex justify-between">
<Typography noWrap>{file.name}</Typography>
<div className="flex gap-1">
<button
onClick={() => {
handleReorderFileList(file.name, "up");
}}
disabled={index === 0}
className="disabled:opacity-50"
>
<Icon.ArrowUp className="w-4 h-4" />
</button>
<button
onClick={() => {
handleReorderFileList(file.name, "down");
}}
disabled={index === fileList.length - 1}
className="disabled:opacity-50"
>
<Icon.ArrowDown className="w-4 h-4" />
</button>
</div>
</ListItem>
</Tooltip>
<ListItem key={file.name} className="flex justify-between">
<Typography noWrap>{file.name}</Typography>
<div className="flex gap-1">
<button
onClick={() => {
handleReorderFileList(file.name, "up");
}}
disabled={index === 0}
className="disabled:opacity-50"
>
<Icon.ArrowUp className="w-4 h-4" />
</button>
<button
onClick={() => {
handleReorderFileList(file.name, "down");
}}
disabled={index === fileList.length - 1}
className="disabled:opacity-50"
>
<Icon.ArrowDown className="w-4 h-4" />
</button>
</div>
</ListItem>
))}
</List>
</>

View File

@ -1,6 +1,6 @@
import { getTimeString } from "@/helpers/datetime";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import MemoResourceListView from "./MemoResourceListView";
import "@/less/daily-memo.less";
interface Props {
@ -18,7 +18,7 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
</div>
<div className="memo-container">
<MemoContent content={memo.content} showFull={true} />
<MemoResources resourceList={memo.resourceList} />
<MemoResourceListView resourceList={memo.resourceList} />
</div>
<div className="split-line"></div>
</div>

View File

@ -12,11 +12,11 @@ import Divider from "./kit/Divider";
import { showCommonDialog } from "./Dialog/CommonDialog";
import Icon from "./Icon";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import MemoResourceListView from "./MemoResourceListView";
import MemoRelationListView from "./MemoRelationListView";
import showShareMemo from "./ShareMemoDialog";
import showPreviewImageDialog from "./PreviewImageDialog";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import MemoRelationListView from "./MemoRelationListView";
import "@/less/memo.less";
interface Props {
@ -39,9 +39,17 @@ const Memo: React.FC<Props> = (props: Props) => {
const isVisitorMode = userStore.isVisitorMode() || readonly;
useEffect(() => {
Promise.all(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then((memoList) => {
setRelatedMemoList(uniqWith(memoList, isEqual));
});
Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then(
(results) => {
const memoList = [];
for (const result of results) {
if (result.status === "fulfilled") {
memoList.push(result.value);
}
}
setRelatedMemoList(uniqWith(memoList, isEqual));
}
);
}, [memo.relationList]);
useEffect(() => {
@ -271,13 +279,13 @@ const Memo: React.FC<Props> = (props: Props) => {
onMemoContentClick={handleMemoContentClick}
onMemoContentDoubleClick={handleMemoContentDoubleClick}
/>
<MemoResources resourceList={memo.resourceList} />
<MemoResourceListView resourceList={memo.resourceList} />
{!showRelatedMemos && <MemoRelationListView relationList={memo.relationList} />}
</div>
{showRelatedMemos && relatedMemoList.length > 0 && (
<>
<p className="font-mono text-sm mt-4 mb-1 pl-4 opacity-60 flex flex-row items-center">
<p className="text-sm mt-4 mb-1 pl-4 opacity-50 flex flex-row items-center">
<Icon.Link className="w-4 h-auto mr-1" />
<span>Related memos</span>
</p>

View File

@ -88,6 +88,10 @@ const MemoEditor = () => {
prevEditorStateRef.current = editorState;
}, [editorState.editMemoId]);
useEffect(() => {
handleEditorFocus();
}, [editorStore.state.relationList]);
const handleKeyDown = (event: React.KeyboardEvent) => {
if (!editorRef.current) {
return;

View File

@ -20,6 +20,10 @@ const MemoRelationListView = (props: Props) => {
fetchRelatedMemoList();
}, [relationList]);
const handleGotoMemoDetail = (memo: Memo) => {
window.open(`/m/${memo.id}`, "_blank");
};
return (
<>
{relatedMemoList.length > 0 && (
@ -29,6 +33,7 @@ const MemoRelationListView = (props: Props) => {
<div
key={memo.id}
className="w-auto flex flex-row justify-start items-center hover:bg-gray-100 dark:hover:bg-zinc-800 rounded text-sm p-1 text-gray-500 dark:text-gray-400 cursor-pointer"
onClick={() => handleGotoMemoDetail(memo)}
>
<div className="w-5 h-5 flex justify-center items-center shrink-0 bg-gray-100 dark:bg-zinc-800 rounded-full">
<Icon.Link className="w-3 h-auto" />

View File

@ -17,7 +17,7 @@ const getDefaultProps = (): Props => {
};
};
const MemoResources: React.FC<Props> = (props: Props) => {
const MemoResourceListView: React.FC<Props> = (props: Props) => {
const { className, resourceList } = {
...getDefaultProps(),
...props,
@ -75,4 +75,4 @@ const MemoResources: React.FC<Props> = (props: Props) => {
);
};
export default MemoResources;
export default MemoResourceListView;

View File

@ -2,6 +2,7 @@ import React from "react";
import Icon from "./Icon";
import { getResourceUrl } from "@/utils/resource";
import showPreviewImageDialog from "./PreviewImageDialog";
import SquareDiv from "./kit/SquareDiv";
import "@/less/resource-cover.less";
interface ResourceCoverProps {
@ -40,7 +41,13 @@ const ResourceCover = ({ resource }: ResourceCoverProps) => {
switch (resourceType) {
case "image/*":
return (
<img className="resource-cover h-20 w-20" src={resourceUrl + "?thumbnail=1"} onClick={() => showPreviewImageDialog(resourceUrl)} />
<SquareDiv className="h-20 w-20 flex items-center justify-center overflow-clip">
<img
className="max-w-full max-h-full object-cover shadow"
src={resourceUrl + "?thumbnail=1"}
onClick={() => showPreviewImageDialog(resourceUrl)}
/>
</SquareDiv>
);
case "video/*":
return <Icon.FileVideo2 className="resource-cover" />;

View File

@ -14,7 +14,7 @@ import useLoading from "@/hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import MemoResourceListView from "./MemoResourceListView";
import showEmbedMemoDialog from "./EmbedMemoDialog";
import "@/less/share-memo-dialog.less";
@ -177,7 +177,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
<span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{memo.createdAtStr}</span>
<div className="w-full px-6 text-base pb-4">
<MemoContent content={memo.content} showFull={true} />
<MemoResources className="!grid-cols-2" resourceList={memo.resourceList} />
<MemoResourceListView className="!grid-cols-2" resourceList={memo.resourceList} />
</div>
<div className="flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-700 py-4 px-6">
<div className="mr-2">

View File

@ -18,12 +18,12 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
const { className, datestamp, handleDateStampChange } = props;
const [currentDateStamp, setCurrentDateStamp] = useState<DateStamp>(getMonthFirstDayDateStamp(datestamp));
const [countByDate, setCountByDate] = useState(new Map());
const currentUserId = useUserStore().getCurrentUserId();
useEffect(() => {
setCurrentDateStamp(getMonthFirstDayDateStamp(datestamp));
}, [datestamp]);
const currentUserId = useUserStore().getCurrentUserId();
useEffect(() => {
getMemoStats(currentUserId).then(({ data: { data } }) => {
const m = new Map();

View File

@ -5,7 +5,7 @@ import { UNKNOWN_ID } from "@/helpers/consts";
import { useMemoStore } from "@/store/module";
import useLoading from "@/hooks/useLoading";
import MemoContent from "@/components/MemoContent";
import MemoResources from "@/components/MemoResources";
import MemoResourceListView from "@/components/MemoResourceListView";
import { getDateTimeString } from "@/helpers/datetime";
interface State {
@ -51,7 +51,7 @@ const EmbedMemo = () => {
</a>
</div>
<MemoContent className="memo-content" content={state.memo.content} onMemoContentClick={() => undefined} />
<MemoResources resourceList={state.memo.resourceList} />
<MemoResourceListView resourceList={state.memo.resourceList} />
</div>
</main>
)}

View File

@ -3,8 +3,9 @@ import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useParams } from "react-router-dom";
import { UNKNOWN_ID } from "@/helpers/consts";
import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module";
import { useGlobalStore, useMemoStore } from "@/store/module";
import useLoading from "@/hooks/useLoading";
import Icon from "@/components/Icon";
import Memo from "@/components/Memo";
interface State {
@ -17,7 +18,6 @@ const MemoDetail = () => {
const location = useLocation();
const globalStore = useGlobalStore();
const memoStore = useMemoStore();
const userStore = useUserStore();
const [state, setState] = useState<State>({
memo: {
id: UNKNOWN_ID,
@ -25,7 +25,6 @@ const MemoDetail = () => {
});
const loadingState = useLoading();
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
const user = userStore.state.user;
useEffect(() => {
const memoId = Number(params.memoId);
@ -47,38 +46,27 @@ const MemoDetail = () => {
return (
<section className="relative top-0 w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8">
<div className="sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 py-2 mt-2 bg-zinc-100 dark:bg-zinc-800">
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-6">
<div className="max-w-2xl w-full flex flex-row justify-center items-center px-4 py-2 mt-2 bg-zinc-100 dark:bg-zinc-800">
<div className="flex flex-row justify-start items-center">
<img className="h-10 w-auto rounded-lg mr-2" src={customizedProfile.logoUrl} alt="" />
<p className="text-4xl tracking-wide text-black dark:text-white">{customizedProfile.name}</p>
</div>
<div className="action-button-container">
{!loadingState.isLoading && (
<>
{user ? (
<Link
to="/"
className="block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline"
>
<span className="text-lg">🏠</span> {t("router.back-to-home")}
</Link>
) : (
<Link
to="/auth"
className="block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline"
>
<span className="text-lg">👉</span> {t("common.sign-in")}
</Link>
)}
</>
)}
</div>
</div>
{!loadingState.isLoading && (
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<Memo memo={state.memo} readonly showRelatedMemos />
</main>
<>
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<Memo memo={state.memo} readonly showRelatedMemos />
</main>
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
<Link
to="/"
className="flex flex-row justify-center items-center text-gray-600 dark:text-gray-300 text-sm px-3 hover:opacity-80 hover:underline"
>
<Icon.Home className="w-4 h-auto mr-1 -mt-0.5" /> {t("router.back-to-home")}
</Link>
</div>
</>
)}
</div>
</section>