mirror of
https://github.com/usememos/memos.git
synced 2025-03-15 10:10:11 +01:00
feat: update memo detail page (#1682)
* feat: update memo detail page * chore: update
This commit is contained in:
parent
04124a2ace
commit
b40571095d
@ -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
6
go.mod
@ -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
24
go.sum
@ -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=
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
@ -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" />;
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user