Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
4b4e7194
Unverified
Commit
4b4e7194
authored
Apr 06, 2026
by
boojack
Committed by
GitHub
Apr 06, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(attachments): add Live Photo and Motion Photo support (#5810)
parent
894b3eb0
Changes
42
Show whitespace changes
Inline
Side-by-side
Showing
42 changed files
with
2499 additions
and
333 deletions
+2499
-333
motionphoto.go
internal/motionphoto/motionphoto.go
+110
-0
motionphoto_test.go
internal/motionphoto/motionphoto_test.go
+25
-0
motionphoto.go
internal/testutil/motionphoto.go
+18
-0
attachment_service.proto
proto/api/v1/attachment_service.proto
+35
-0
attachment_service.connect.go
proto/gen/api/v1/apiv1connect/attachment_service.connect.go
+36
-5
attachment_service.pb.go
proto/gen/api/v1/attachment_service.pb.go
+320
-60
attachment_service.pb.gw.go
proto/gen/api/v1/attachment_service.pb.gw.go
+76
-10
attachment_service_grpc.pb.go
proto/gen/api/v1/attachment_service_grpc.pb.go
+45
-5
openapi.yaml
proto/gen/openapi.yaml
+59
-0
attachment.pb.go
proto/gen/store/attachment.pb.go
+234
-26
attachment.proto
proto/store/attachment.proto
+23
-0
attachment_motion.go
server/router/api/v1/attachment_motion.go
+69
-0
attachment_service.go
server/router/api/v1/attachment_service.go
+125
-14
connect_services.go
server/router/api/v1/connect_services.go
+8
-0
memo_attachment_service.go
server/router/api/v1/memo_attachment_service.go
+110
-28
attachment_service_test.go
server/router/api/v1/test/attachment_service_test.go
+116
-0
memo_attachment_service_test.go
server/router/api/v1/test/memo_attachment_service_test.go
+60
-0
fileserver.go
server/router/fileserver/fileserver.go
+72
-2
fileserver_test.go
server/router/fileserver/fileserver_test.go
+46
-0
attachment.go
store/attachment.go
+70
-3
attachment.go
store/db/mysql/attachment.go
+33
-0
attachment.go
store/db/postgres/attachment.go
+33
-0
attachment.go
store/db/sqlite/attachment.go
+33
-0
driver.go
store/driver.go
+1
-0
EditorMetadata.tsx
web/src/components/MemoEditor/components/EditorMetadata.tsx
+1
-0
useFileUpload.ts
web/src/components/MemoEditor/hooks/useFileUpload.ts
+58
-4
uploadService.ts
web/src/components/MemoEditor/services/uploadService.ts
+4
-2
actions.ts
web/src/components/MemoEditor/state/actions.ts
+5
-0
reducer.ts
web/src/components/MemoEditor/state/reducer.ts
+6
-0
types.ts
web/src/components/MemoEditor/state/types.ts
+1
-0
attachment.ts
web/src/components/MemoEditor/types/attachment.ts
+95
-16
AttachmentListEditor.tsx
...mponents/MemoMetadata/Attachment/AttachmentListEditor.tsx
+63
-28
AttachmentListView.tsx
...components/MemoMetadata/Attachment/AttachmentListView.tsx
+67
-79
MemoPreview.tsx
web/src/components/MemoPreview/MemoPreview.tsx
+27
-19
MemoView.tsx
web/src/components/MemoView/MemoView.tsx
+1
-1
MemoViewContext.tsx
web/src/components/MemoView/MemoViewContext.tsx
+2
-1
useImagePreview.ts
web/src/components/MemoView/hooks/useImagePreview.ts
+35
-5
useMemoHandlers.ts
web/src/components/MemoView/hooks/useMemoHandlers.ts
+2
-1
PreviewImageDialog.tsx
web/src/components/PreviewImageDialog.tsx
+35
-13
attachment_service_pb.ts
web/src/types/proto/api/v1/attachment_service_pb.ts
+138
-10
attachment.ts
web/src/utils/attachment.ts
+23
-1
media-item.ts
web/src/utils/media-item.ts
+179
-0
No files found.
internal/motionphoto/motionphoto.go
0 → 100644
View file @
4b4e7194
package
motionphoto
import
(
"bytes"
"encoding/binary"
"regexp"
"strconv"
)
type
Detection
struct
{
VideoStart
int
PresentationTimestampUs
int64
}
var
(
motionPhotoMarkerRegex
=
regexp
.
MustCompile
(
`(?i)(?:Camera:MotionPhoto|GCamera:MotionPhoto|MicroVideo)["'=:\s>]+1`
)
presentationRegex
=
regexp
.
MustCompile
(
`(?i)(?:Camera:MotionPhotoPresentationTimestampUs|GCamera:MotionPhotoPresentationTimestampUs)["'=:\s>]+(-?\d+)`
)
microVideoOffsetRegex
=
regexp
.
MustCompile
(
`(?i)(?:Camera:MicroVideoOffset|GCamera:MicroVideoOffset)["'=:\s>]+(\d+)`
)
)
const
maxMetadataScanBytes
=
256
*
1024
func
DetectJPEG
(
blob
[]
byte
)
*
Detection
{
if
len
(
blob
)
<
16
||
!
bytes
.
HasPrefix
(
blob
,
[]
byte
{
0xFF
,
0xD8
})
{
return
nil
}
text
:=
string
(
blob
[
:
min
(
len
(
blob
),
maxMetadataScanBytes
)])
if
!
motionPhotoMarkerRegex
.
MatchString
(
text
)
{
return
nil
}
videoStart
:=
detectVideoStart
(
blob
,
text
)
if
videoStart
<
0
||
videoStart
>=
len
(
blob
)
{
return
nil
}
return
&
Detection
{
VideoStart
:
videoStart
,
PresentationTimestampUs
:
parsePresentationTimestampUs
(
text
),
}
}
func
ExtractVideo
(
blob
[]
byte
)
([]
byte
,
*
Detection
)
{
detection
:=
DetectJPEG
(
blob
)
if
detection
==
nil
{
return
nil
,
nil
}
videoBlob
:=
blob
[
detection
.
VideoStart
:
]
if
!
looksLikeMP4
(
videoBlob
)
{
return
nil
,
nil
}
return
videoBlob
,
detection
}
func
detectVideoStart
(
blob
[]
byte
,
text
string
)
int
{
if
matches
:=
microVideoOffsetRegex
.
FindStringSubmatch
(
text
);
len
(
matches
)
==
2
{
if
offset
,
err
:=
strconv
.
Atoi
(
matches
[
1
]);
err
==
nil
&&
offset
>
0
&&
offset
<
len
(
blob
)
{
start
:=
len
(
blob
)
-
offset
if
looksLikeMP4
(
blob
[
start
:
])
{
return
start
}
}
}
return
findEmbeddedMP4Start
(
blob
)
}
func
parsePresentationTimestampUs
(
text
string
)
int64
{
matches
:=
presentationRegex
.
FindStringSubmatch
(
text
)
if
len
(
matches
)
!=
2
{
return
0
}
value
,
err
:=
strconv
.
ParseInt
(
matches
[
1
],
10
,
64
)
if
err
!=
nil
{
return
0
}
return
value
}
func
findEmbeddedMP4Start
(
blob
[]
byte
)
int
{
searchFrom
:=
len
(
blob
)
for
searchFrom
>
8
{
index
:=
bytes
.
LastIndex
(
blob
[
:
searchFrom
],
[]
byte
(
"ftyp"
))
if
index
<
4
{
return
-
1
}
start
:=
index
-
4
if
looksLikeMP4
(
blob
[
start
:
])
{
return
start
}
searchFrom
=
index
-
1
}
return
-
1
}
func
looksLikeMP4
(
blob
[]
byte
)
bool
{
if
len
(
blob
)
<
12
||
!
bytes
.
Equal
(
blob
[
4
:
8
],
[]
byte
(
"ftyp"
))
{
return
false
}
size
:=
binary
.
BigEndian
.
Uint32
(
blob
[
:
4
])
return
size
==
1
||
size
>=
8
}
internal/motionphoto/motionphoto_test.go
0 → 100644
View file @
4b4e7194
package
motionphoto
import
(
"bytes"
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/internal/testutil"
)
func
TestDetectJPEG
(
t
*
testing
.
T
)
{
t
.
Parallel
()
blob
:=
testutil
.
BuildMotionPhotoJPEG
()
detection
:=
DetectJPEG
(
blob
)
require
.
NotNil
(
t
,
detection
)
require
.
Positive
(
t
,
detection
.
VideoStart
)
require
.
EqualValues
(
t
,
123456
,
detection
.
PresentationTimestampUs
)
videoBlob
,
extracted
:=
ExtractVideo
(
blob
)
require
.
NotNil
(
t
,
extracted
)
require
.
True
(
t
,
bytes
.
Equal
(
videoBlob
[
:
4
],
[]
byte
{
0x00
,
0x00
,
0x00
,
0x10
}))
require
.
Equal
(
t
,
[]
byte
(
"ftyp"
),
videoBlob
[
4
:
8
])
}
internal/testutil/motionphoto.go
0 → 100644
View file @
4b4e7194
package
testutil
// BuildMotionPhotoJPEG returns a minimal JPEG blob with Motion Photo metadata
// and an embedded MP4 header for tests.
func
BuildMotionPhotoJPEG
()
[]
byte
{
return
append
(
[]
byte
{
0xFF
,
0xD8
,
0xFF
,
0xE1
,
},
append
(
[]
byte
(
`<?xpacket begin=""?><rdf:Description GCamera:MotionPhoto="1" GCamera:MotionPhotoPresentationTimestampUs="123456"></rdf:Description>`
),
[]
byte
{
0xFF
,
0xD9
,
0x00
,
0x00
,
0x00
,
0x10
,
'f'
,
't'
,
'y'
,
'p'
,
'i'
,
's'
,
'o'
,
'm'
,
0x00
,
0x00
,
0x00
,
0x00
,
}
...
,
)
...
,
)
}
proto/api/v1/attachment_service.proto
View file @
4b4e7194
...
@@ -43,6 +43,34 @@ service AttachmentService {
...
@@ -43,6 +43,34 @@ service AttachmentService {
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{name=attachments/*}"
};
option
(
google.api.http
)
=
{
delete
:
"/api/v1/{name=attachments/*}"
};
option
(
google.api.method_signature
)
=
"name"
;
option
(
google.api.method_signature
)
=
"name"
;
}
}
// BatchDeleteAttachments deletes multiple attachments in one request.
rpc
BatchDeleteAttachments
(
BatchDeleteAttachmentsRequest
)
returns
(
google.protobuf.Empty
)
{
option
(
google.api.http
)
=
{
post
:
"/api/v1/attachments:batchDelete"
body
:
"*"
};
}
}
enum
MotionMediaFamily
{
MOTION_MEDIA_FAMILY_UNSPECIFIED
=
0
;
APPLE_LIVE_PHOTO
=
1
;
ANDROID_MOTION_PHOTO
=
2
;
}
enum
MotionMediaRole
{
MOTION_MEDIA_ROLE_UNSPECIFIED
=
0
;
STILL
=
1
;
VIDEO
=
2
;
CONTAINER
=
3
;
}
message
MotionMedia
{
MotionMediaFamily
family
=
1
;
MotionMediaRole
role
=
2
;
string
group_id
=
3
;
int64
presentation_timestamp_us
=
4
;
bool
has_embedded_video
=
5
;
}
}
message
Attachment
{
message
Attachment
{
...
@@ -78,6 +106,9 @@ message Attachment {
...
@@ -78,6 +106,9 @@ message Attachment {
// Optional. The related memo. Refer to `Memo.name`.
// Optional. The related memo. Refer to `Memo.name`.
// Format: memos/{memo}
// Format: memos/{memo}
optional
string
memo
=
8
[(
google.api.field_behavior
)
=
OPTIONAL
];
optional
string
memo
=
8
[(
google.api.field_behavior
)
=
OPTIONAL
];
// Optional. Motion media metadata.
MotionMedia
motion_media
=
9
[(
google.api.field_behavior
)
=
OPTIONAL
];
}
}
message
CreateAttachmentRequest
{
message
CreateAttachmentRequest
{
...
@@ -148,3 +179,7 @@ message DeleteAttachmentRequest {
...
@@ -148,3 +179,7 @@ message DeleteAttachmentRequest {
(
google.api.resource_reference
)
=
{
type
:
"memos.api.v1/Attachment"
}
(
google.api.resource_reference
)
=
{
type
:
"memos.api.v1/Attachment"
}
];
];
}
}
message
BatchDeleteAttachmentsRequest
{
repeated
string
names
=
1
[(
google.api.field_behavior
)
=
REQUIRED
];
}
proto/gen/api/v1/apiv1connect/attachment_service.connect.go
View file @
4b4e7194
...
@@ -49,6 +49,9 @@ const (
...
@@ -49,6 +49,9 @@ const (
// AttachmentServiceDeleteAttachmentProcedure is the fully-qualified name of the AttachmentService's
// AttachmentServiceDeleteAttachmentProcedure is the fully-qualified name of the AttachmentService's
// DeleteAttachment RPC.
// DeleteAttachment RPC.
AttachmentServiceDeleteAttachmentProcedure
=
"/memos.api.v1.AttachmentService/DeleteAttachment"
AttachmentServiceDeleteAttachmentProcedure
=
"/memos.api.v1.AttachmentService/DeleteAttachment"
// AttachmentServiceBatchDeleteAttachmentsProcedure is the fully-qualified name of the
// AttachmentService's BatchDeleteAttachments RPC.
AttachmentServiceBatchDeleteAttachmentsProcedure
=
"/memos.api.v1.AttachmentService/BatchDeleteAttachments"
)
)
// AttachmentServiceClient is a client for the memos.api.v1.AttachmentService service.
// AttachmentServiceClient is a client for the memos.api.v1.AttachmentService service.
...
@@ -63,6 +66,8 @@ type AttachmentServiceClient interface {
...
@@ -63,6 +66,8 @@ type AttachmentServiceClient interface {
UpdateAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
UpdateAttachmentRequest
])
(
*
connect
.
Response
[
v1
.
Attachment
],
error
)
UpdateAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
UpdateAttachmentRequest
])
(
*
connect
.
Response
[
v1
.
Attachment
],
error
)
// DeleteAttachment deletes an attachment by name.
// DeleteAttachment deletes an attachment by name.
DeleteAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
DeleteAttachmentRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
DeleteAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
DeleteAttachmentRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
// BatchDeleteAttachments deletes multiple attachments in one request.
BatchDeleteAttachments
(
context
.
Context
,
*
connect
.
Request
[
v1
.
BatchDeleteAttachmentsRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
}
}
// NewAttachmentServiceClient constructs a client for the memos.api.v1.AttachmentService service. By
// NewAttachmentServiceClient constructs a client for the memos.api.v1.AttachmentService service. By
...
@@ -106,6 +111,12 @@ func NewAttachmentServiceClient(httpClient connect.HTTPClient, baseURL string, o
...
@@ -106,6 +111,12 @@ func NewAttachmentServiceClient(httpClient connect.HTTPClient, baseURL string, o
connect
.
WithSchema
(
attachmentServiceMethods
.
ByName
(
"DeleteAttachment"
)),
connect
.
WithSchema
(
attachmentServiceMethods
.
ByName
(
"DeleteAttachment"
)),
connect
.
WithClientOptions
(
opts
...
),
connect
.
WithClientOptions
(
opts
...
),
),
),
batchDeleteAttachments
:
connect
.
NewClient
[
v1
.
BatchDeleteAttachmentsRequest
,
emptypb
.
Empty
](
httpClient
,
baseURL
+
AttachmentServiceBatchDeleteAttachmentsProcedure
,
connect
.
WithSchema
(
attachmentServiceMethods
.
ByName
(
"BatchDeleteAttachments"
)),
connect
.
WithClientOptions
(
opts
...
),
),
}
}
}
}
...
@@ -116,6 +127,7 @@ type attachmentServiceClient struct {
...
@@ -116,6 +127,7 @@ type attachmentServiceClient struct {
getAttachment
*
connect
.
Client
[
v1
.
GetAttachmentRequest
,
v1
.
Attachment
]
getAttachment
*
connect
.
Client
[
v1
.
GetAttachmentRequest
,
v1
.
Attachment
]
updateAttachment
*
connect
.
Client
[
v1
.
UpdateAttachmentRequest
,
v1
.
Attachment
]
updateAttachment
*
connect
.
Client
[
v1
.
UpdateAttachmentRequest
,
v1
.
Attachment
]
deleteAttachment
*
connect
.
Client
[
v1
.
DeleteAttachmentRequest
,
emptypb
.
Empty
]
deleteAttachment
*
connect
.
Client
[
v1
.
DeleteAttachmentRequest
,
emptypb
.
Empty
]
batchDeleteAttachments
*
connect
.
Client
[
v1
.
BatchDeleteAttachmentsRequest
,
emptypb
.
Empty
]
}
}
// CreateAttachment calls memos.api.v1.AttachmentService.CreateAttachment.
// CreateAttachment calls memos.api.v1.AttachmentService.CreateAttachment.
...
@@ -143,6 +155,11 @@ func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, req *con
...
@@ -143,6 +155,11 @@ func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, req *con
return
c
.
deleteAttachment
.
CallUnary
(
ctx
,
req
)
return
c
.
deleteAttachment
.
CallUnary
(
ctx
,
req
)
}
}
// BatchDeleteAttachments calls memos.api.v1.AttachmentService.BatchDeleteAttachments.
func
(
c
*
attachmentServiceClient
)
BatchDeleteAttachments
(
ctx
context
.
Context
,
req
*
connect
.
Request
[
v1
.
BatchDeleteAttachmentsRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
{
return
c
.
batchDeleteAttachments
.
CallUnary
(
ctx
,
req
)
}
// AttachmentServiceHandler is an implementation of the memos.api.v1.AttachmentService service.
// AttachmentServiceHandler is an implementation of the memos.api.v1.AttachmentService service.
type
AttachmentServiceHandler
interface
{
type
AttachmentServiceHandler
interface
{
// CreateAttachment creates a new attachment.
// CreateAttachment creates a new attachment.
...
@@ -155,6 +172,8 @@ type AttachmentServiceHandler interface {
...
@@ -155,6 +172,8 @@ type AttachmentServiceHandler interface {
UpdateAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
UpdateAttachmentRequest
])
(
*
connect
.
Response
[
v1
.
Attachment
],
error
)
UpdateAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
UpdateAttachmentRequest
])
(
*
connect
.
Response
[
v1
.
Attachment
],
error
)
// DeleteAttachment deletes an attachment by name.
// DeleteAttachment deletes an attachment by name.
DeleteAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
DeleteAttachmentRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
DeleteAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
DeleteAttachmentRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
// BatchDeleteAttachments deletes multiple attachments in one request.
BatchDeleteAttachments
(
context
.
Context
,
*
connect
.
Request
[
v1
.
BatchDeleteAttachmentsRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
}
}
// NewAttachmentServiceHandler builds an HTTP handler from the service implementation. It returns
// NewAttachmentServiceHandler builds an HTTP handler from the service implementation. It returns
...
@@ -194,6 +213,12 @@ func NewAttachmentServiceHandler(svc AttachmentServiceHandler, opts ...connect.H
...
@@ -194,6 +213,12 @@ func NewAttachmentServiceHandler(svc AttachmentServiceHandler, opts ...connect.H
connect
.
WithSchema
(
attachmentServiceMethods
.
ByName
(
"DeleteAttachment"
)),
connect
.
WithSchema
(
attachmentServiceMethods
.
ByName
(
"DeleteAttachment"
)),
connect
.
WithHandlerOptions
(
opts
...
),
connect
.
WithHandlerOptions
(
opts
...
),
)
)
attachmentServiceBatchDeleteAttachmentsHandler
:=
connect
.
NewUnaryHandler
(
AttachmentServiceBatchDeleteAttachmentsProcedure
,
svc
.
BatchDeleteAttachments
,
connect
.
WithSchema
(
attachmentServiceMethods
.
ByName
(
"BatchDeleteAttachments"
)),
connect
.
WithHandlerOptions
(
opts
...
),
)
return
"/memos.api.v1.AttachmentService/"
,
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
return
"/memos.api.v1.AttachmentService/"
,
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
switch
r
.
URL
.
Path
{
switch
r
.
URL
.
Path
{
case
AttachmentServiceCreateAttachmentProcedure
:
case
AttachmentServiceCreateAttachmentProcedure
:
...
@@ -206,6 +231,8 @@ func NewAttachmentServiceHandler(svc AttachmentServiceHandler, opts ...connect.H
...
@@ -206,6 +231,8 @@ func NewAttachmentServiceHandler(svc AttachmentServiceHandler, opts ...connect.H
attachmentServiceUpdateAttachmentHandler
.
ServeHTTP
(
w
,
r
)
attachmentServiceUpdateAttachmentHandler
.
ServeHTTP
(
w
,
r
)
case
AttachmentServiceDeleteAttachmentProcedure
:
case
AttachmentServiceDeleteAttachmentProcedure
:
attachmentServiceDeleteAttachmentHandler
.
ServeHTTP
(
w
,
r
)
attachmentServiceDeleteAttachmentHandler
.
ServeHTTP
(
w
,
r
)
case
AttachmentServiceBatchDeleteAttachmentsProcedure
:
attachmentServiceBatchDeleteAttachmentsHandler
.
ServeHTTP
(
w
,
r
)
default
:
default
:
http
.
NotFound
(
w
,
r
)
http
.
NotFound
(
w
,
r
)
}
}
...
@@ -234,3 +261,7 @@ func (UnimplementedAttachmentServiceHandler) UpdateAttachment(context.Context, *
...
@@ -234,3 +261,7 @@ func (UnimplementedAttachmentServiceHandler) UpdateAttachment(context.Context, *
func
(
UnimplementedAttachmentServiceHandler
)
DeleteAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
DeleteAttachmentRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
{
func
(
UnimplementedAttachmentServiceHandler
)
DeleteAttachment
(
context
.
Context
,
*
connect
.
Request
[
v1
.
DeleteAttachmentRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
{
return
nil
,
connect
.
NewError
(
connect
.
CodeUnimplemented
,
errors
.
New
(
"memos.api.v1.AttachmentService.DeleteAttachment is not implemented"
))
return
nil
,
connect
.
NewError
(
connect
.
CodeUnimplemented
,
errors
.
New
(
"memos.api.v1.AttachmentService.DeleteAttachment is not implemented"
))
}
}
func
(
UnimplementedAttachmentServiceHandler
)
BatchDeleteAttachments
(
context
.
Context
,
*
connect
.
Request
[
v1
.
BatchDeleteAttachmentsRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
{
return
nil
,
connect
.
NewError
(
connect
.
CodeUnimplemented
,
errors
.
New
(
"memos.api.v1.AttachmentService.BatchDeleteAttachments is not implemented"
))
}
proto/gen/api/v1/attachment_service.pb.go
View file @
4b4e7194
...
@@ -25,6 +25,183 @@ const (
...
@@ -25,6 +25,183 @@ const (
_
=
protoimpl
.
EnforceVersion
(
protoimpl
.
MaxVersion
-
20
)
_
=
protoimpl
.
EnforceVersion
(
protoimpl
.
MaxVersion
-
20
)
)
)
type
MotionMediaFamily
int32
const
(
MotionMediaFamily_MOTION_MEDIA_FAMILY_UNSPECIFIED
MotionMediaFamily
=
0
MotionMediaFamily_APPLE_LIVE_PHOTO
MotionMediaFamily
=
1
MotionMediaFamily_ANDROID_MOTION_PHOTO
MotionMediaFamily
=
2
)
// Enum value maps for MotionMediaFamily.
var
(
MotionMediaFamily_name
=
map
[
int32
]
string
{
0
:
"MOTION_MEDIA_FAMILY_UNSPECIFIED"
,
1
:
"APPLE_LIVE_PHOTO"
,
2
:
"ANDROID_MOTION_PHOTO"
,
}
MotionMediaFamily_value
=
map
[
string
]
int32
{
"MOTION_MEDIA_FAMILY_UNSPECIFIED"
:
0
,
"APPLE_LIVE_PHOTO"
:
1
,
"ANDROID_MOTION_PHOTO"
:
2
,
}
)
func
(
x
MotionMediaFamily
)
Enum
()
*
MotionMediaFamily
{
p
:=
new
(
MotionMediaFamily
)
*
p
=
x
return
p
}
func
(
x
MotionMediaFamily
)
String
()
string
{
return
protoimpl
.
X
.
EnumStringOf
(
x
.
Descriptor
(),
protoreflect
.
EnumNumber
(
x
))
}
func
(
MotionMediaFamily
)
Descriptor
()
protoreflect
.
EnumDescriptor
{
return
file_api_v1_attachment_service_proto_enumTypes
[
0
]
.
Descriptor
()
}
func
(
MotionMediaFamily
)
Type
()
protoreflect
.
EnumType
{
return
&
file_api_v1_attachment_service_proto_enumTypes
[
0
]
}
func
(
x
MotionMediaFamily
)
Number
()
protoreflect
.
EnumNumber
{
return
protoreflect
.
EnumNumber
(
x
)
}
// Deprecated: Use MotionMediaFamily.Descriptor instead.
func
(
MotionMediaFamily
)
EnumDescriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
0
}
}
type
MotionMediaRole
int32
const
(
MotionMediaRole_MOTION_MEDIA_ROLE_UNSPECIFIED
MotionMediaRole
=
0
MotionMediaRole_STILL
MotionMediaRole
=
1
MotionMediaRole_VIDEO
MotionMediaRole
=
2
MotionMediaRole_CONTAINER
MotionMediaRole
=
3
)
// Enum value maps for MotionMediaRole.
var
(
MotionMediaRole_name
=
map
[
int32
]
string
{
0
:
"MOTION_MEDIA_ROLE_UNSPECIFIED"
,
1
:
"STILL"
,
2
:
"VIDEO"
,
3
:
"CONTAINER"
,
}
MotionMediaRole_value
=
map
[
string
]
int32
{
"MOTION_MEDIA_ROLE_UNSPECIFIED"
:
0
,
"STILL"
:
1
,
"VIDEO"
:
2
,
"CONTAINER"
:
3
,
}
)
func
(
x
MotionMediaRole
)
Enum
()
*
MotionMediaRole
{
p
:=
new
(
MotionMediaRole
)
*
p
=
x
return
p
}
func
(
x
MotionMediaRole
)
String
()
string
{
return
protoimpl
.
X
.
EnumStringOf
(
x
.
Descriptor
(),
protoreflect
.
EnumNumber
(
x
))
}
func
(
MotionMediaRole
)
Descriptor
()
protoreflect
.
EnumDescriptor
{
return
file_api_v1_attachment_service_proto_enumTypes
[
1
]
.
Descriptor
()
}
func
(
MotionMediaRole
)
Type
()
protoreflect
.
EnumType
{
return
&
file_api_v1_attachment_service_proto_enumTypes
[
1
]
}
func
(
x
MotionMediaRole
)
Number
()
protoreflect
.
EnumNumber
{
return
protoreflect
.
EnumNumber
(
x
)
}
// Deprecated: Use MotionMediaRole.Descriptor instead.
func
(
MotionMediaRole
)
EnumDescriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
1
}
}
type
MotionMedia
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
Family
MotionMediaFamily
`protobuf:"varint,1,opt,name=family,proto3,enum=memos.api.v1.MotionMediaFamily" json:"family,omitempty"`
Role
MotionMediaRole
`protobuf:"varint,2,opt,name=role,proto3,enum=memos.api.v1.MotionMediaRole" json:"role,omitempty"`
GroupId
string
`protobuf:"bytes,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
PresentationTimestampUs
int64
`protobuf:"varint,4,opt,name=presentation_timestamp_us,json=presentationTimestampUs,proto3" json:"presentation_timestamp_us,omitempty"`
HasEmbeddedVideo
bool
`protobuf:"varint,5,opt,name=has_embedded_video,json=hasEmbeddedVideo,proto3" json:"has_embedded_video,omitempty"`
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
}
func
(
x
*
MotionMedia
)
Reset
()
{
*
x
=
MotionMedia
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
0
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
}
func
(
x
*
MotionMedia
)
String
()
string
{
return
protoimpl
.
X
.
MessageStringOf
(
x
)
}
func
(
*
MotionMedia
)
ProtoMessage
()
{}
func
(
x
*
MotionMedia
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
0
]
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
ms
.
StoreMessageInfo
(
mi
)
}
return
ms
}
return
mi
.
MessageOf
(
x
)
}
// Deprecated: Use MotionMedia.ProtoReflect.Descriptor instead.
func
(
*
MotionMedia
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
0
}
}
func
(
x
*
MotionMedia
)
GetFamily
()
MotionMediaFamily
{
if
x
!=
nil
{
return
x
.
Family
}
return
MotionMediaFamily_MOTION_MEDIA_FAMILY_UNSPECIFIED
}
func
(
x
*
MotionMedia
)
GetRole
()
MotionMediaRole
{
if
x
!=
nil
{
return
x
.
Role
}
return
MotionMediaRole_MOTION_MEDIA_ROLE_UNSPECIFIED
}
func
(
x
*
MotionMedia
)
GetGroupId
()
string
{
if
x
!=
nil
{
return
x
.
GroupId
}
return
""
}
func
(
x
*
MotionMedia
)
GetPresentationTimestampUs
()
int64
{
if
x
!=
nil
{
return
x
.
PresentationTimestampUs
}
return
0
}
func
(
x
*
MotionMedia
)
GetHasEmbeddedVideo
()
bool
{
if
x
!=
nil
{
return
x
.
HasEmbeddedVideo
}
return
false
}
type
Attachment
struct
{
type
Attachment
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
state
protoimpl
.
MessageState
`protogen:"open.v1"`
// The name of the attachment.
// The name of the attachment.
...
@@ -45,13 +222,15 @@ type Attachment struct {
...
@@ -45,13 +222,15 @@ type Attachment struct {
// Optional. The related memo. Refer to `Memo.name`.
// Optional. The related memo. Refer to `Memo.name`.
// Format: memos/{memo}
// Format: memos/{memo}
Memo
*
string
`protobuf:"bytes,8,opt,name=memo,proto3,oneof" json:"memo,omitempty"`
Memo
*
string
`protobuf:"bytes,8,opt,name=memo,proto3,oneof" json:"memo,omitempty"`
// Optional. Motion media metadata.
MotionMedia
*
MotionMedia
`protobuf:"bytes,9,opt,name=motion_media,json=motionMedia,proto3" json:"motion_media,omitempty"`
unknownFields
protoimpl
.
UnknownFields
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
sizeCache
protoimpl
.
SizeCache
}
}
func
(
x
*
Attachment
)
Reset
()
{
func
(
x
*
Attachment
)
Reset
()
{
*
x
=
Attachment
{}
*
x
=
Attachment
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
0
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
1
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -63,7 +242,7 @@ func (x *Attachment) String() string {
...
@@ -63,7 +242,7 @@ func (x *Attachment) String() string {
func
(
*
Attachment
)
ProtoMessage
()
{}
func
(
*
Attachment
)
ProtoMessage
()
{}
func
(
x
*
Attachment
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
Attachment
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
0
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
1
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -76,7 +255,7 @@ func (x *Attachment) ProtoReflect() protoreflect.Message {
...
@@ -76,7 +255,7 @@ func (x *Attachment) ProtoReflect() protoreflect.Message {
// Deprecated: Use Attachment.ProtoReflect.Descriptor instead.
// Deprecated: Use Attachment.ProtoReflect.Descriptor instead.
func
(
*
Attachment
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
Attachment
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
0
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
1
}
}
}
func
(
x
*
Attachment
)
GetName
()
string
{
func
(
x
*
Attachment
)
GetName
()
string
{
...
@@ -135,6 +314,13 @@ func (x *Attachment) GetMemo() string {
...
@@ -135,6 +314,13 @@ func (x *Attachment) GetMemo() string {
return
""
return
""
}
}
func
(
x
*
Attachment
)
GetMotionMedia
()
*
MotionMedia
{
if
x
!=
nil
{
return
x
.
MotionMedia
}
return
nil
}
type
CreateAttachmentRequest
struct
{
type
CreateAttachmentRequest
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
state
protoimpl
.
MessageState
`protogen:"open.v1"`
// Required. The attachment to create.
// Required. The attachment to create.
...
@@ -148,7 +334,7 @@ type CreateAttachmentRequest struct {
...
@@ -148,7 +334,7 @@ type CreateAttachmentRequest struct {
func
(
x
*
CreateAttachmentRequest
)
Reset
()
{
func
(
x
*
CreateAttachmentRequest
)
Reset
()
{
*
x
=
CreateAttachmentRequest
{}
*
x
=
CreateAttachmentRequest
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
1
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
2
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -160,7 +346,7 @@ func (x *CreateAttachmentRequest) String() string {
...
@@ -160,7 +346,7 @@ func (x *CreateAttachmentRequest) String() string {
func
(
*
CreateAttachmentRequest
)
ProtoMessage
()
{}
func
(
*
CreateAttachmentRequest
)
ProtoMessage
()
{}
func
(
x
*
CreateAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
CreateAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
1
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
2
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -173,7 +359,7 @@ func (x *CreateAttachmentRequest) ProtoReflect() protoreflect.Message {
...
@@ -173,7 +359,7 @@ func (x *CreateAttachmentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateAttachmentRequest.ProtoReflect.Descriptor instead.
// Deprecated: Use CreateAttachmentRequest.ProtoReflect.Descriptor instead.
func
(
*
CreateAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
CreateAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
1
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
2
}
}
}
func
(
x
*
CreateAttachmentRequest
)
GetAttachment
()
*
Attachment
{
func
(
x
*
CreateAttachmentRequest
)
GetAttachment
()
*
Attachment
{
...
@@ -214,7 +400,7 @@ type ListAttachmentsRequest struct {
...
@@ -214,7 +400,7 @@ type ListAttachmentsRequest struct {
func
(
x
*
ListAttachmentsRequest
)
Reset
()
{
func
(
x
*
ListAttachmentsRequest
)
Reset
()
{
*
x
=
ListAttachmentsRequest
{}
*
x
=
ListAttachmentsRequest
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
2
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
3
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -226,7 +412,7 @@ func (x *ListAttachmentsRequest) String() string {
...
@@ -226,7 +412,7 @@ func (x *ListAttachmentsRequest) String() string {
func
(
*
ListAttachmentsRequest
)
ProtoMessage
()
{}
func
(
*
ListAttachmentsRequest
)
ProtoMessage
()
{}
func
(
x
*
ListAttachmentsRequest
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
ListAttachmentsRequest
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
2
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
3
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -239,7 +425,7 @@ func (x *ListAttachmentsRequest) ProtoReflect() protoreflect.Message {
...
@@ -239,7 +425,7 @@ func (x *ListAttachmentsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAttachmentsRequest.ProtoReflect.Descriptor instead.
// Deprecated: Use ListAttachmentsRequest.ProtoReflect.Descriptor instead.
func
(
*
ListAttachmentsRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
ListAttachmentsRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
2
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
3
}
}
}
func
(
x
*
ListAttachmentsRequest
)
GetPageSize
()
int32
{
func
(
x
*
ListAttachmentsRequest
)
GetPageSize
()
int32
{
...
@@ -285,7 +471,7 @@ type ListAttachmentsResponse struct {
...
@@ -285,7 +471,7 @@ type ListAttachmentsResponse struct {
func
(
x
*
ListAttachmentsResponse
)
Reset
()
{
func
(
x
*
ListAttachmentsResponse
)
Reset
()
{
*
x
=
ListAttachmentsResponse
{}
*
x
=
ListAttachmentsResponse
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
3
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
4
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -297,7 +483,7 @@ func (x *ListAttachmentsResponse) String() string {
...
@@ -297,7 +483,7 @@ func (x *ListAttachmentsResponse) String() string {
func
(
*
ListAttachmentsResponse
)
ProtoMessage
()
{}
func
(
*
ListAttachmentsResponse
)
ProtoMessage
()
{}
func
(
x
*
ListAttachmentsResponse
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
ListAttachmentsResponse
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
3
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
4
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -310,7 +496,7 @@ func (x *ListAttachmentsResponse) ProtoReflect() protoreflect.Message {
...
@@ -310,7 +496,7 @@ func (x *ListAttachmentsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAttachmentsResponse.ProtoReflect.Descriptor instead.
// Deprecated: Use ListAttachmentsResponse.ProtoReflect.Descriptor instead.
func
(
*
ListAttachmentsResponse
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
ListAttachmentsResponse
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
3
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
4
}
}
}
func
(
x
*
ListAttachmentsResponse
)
GetAttachments
()
[]
*
Attachment
{
func
(
x
*
ListAttachmentsResponse
)
GetAttachments
()
[]
*
Attachment
{
...
@@ -345,7 +531,7 @@ type GetAttachmentRequest struct {
...
@@ -345,7 +531,7 @@ type GetAttachmentRequest struct {
func
(
x
*
GetAttachmentRequest
)
Reset
()
{
func
(
x
*
GetAttachmentRequest
)
Reset
()
{
*
x
=
GetAttachmentRequest
{}
*
x
=
GetAttachmentRequest
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
4
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
5
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -357,7 +543,7 @@ func (x *GetAttachmentRequest) String() string {
...
@@ -357,7 +543,7 @@ func (x *GetAttachmentRequest) String() string {
func
(
*
GetAttachmentRequest
)
ProtoMessage
()
{}
func
(
*
GetAttachmentRequest
)
ProtoMessage
()
{}
func
(
x
*
GetAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
GetAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
4
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
5
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -370,7 +556,7 @@ func (x *GetAttachmentRequest) ProtoReflect() protoreflect.Message {
...
@@ -370,7 +556,7 @@ func (x *GetAttachmentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAttachmentRequest.ProtoReflect.Descriptor instead.
// Deprecated: Use GetAttachmentRequest.ProtoReflect.Descriptor instead.
func
(
*
GetAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
GetAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
4
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
5
}
}
}
func
(
x
*
GetAttachmentRequest
)
GetName
()
string
{
func
(
x
*
GetAttachmentRequest
)
GetName
()
string
{
...
@@ -392,7 +578,7 @@ type UpdateAttachmentRequest struct {
...
@@ -392,7 +578,7 @@ type UpdateAttachmentRequest struct {
func
(
x
*
UpdateAttachmentRequest
)
Reset
()
{
func
(
x
*
UpdateAttachmentRequest
)
Reset
()
{
*
x
=
UpdateAttachmentRequest
{}
*
x
=
UpdateAttachmentRequest
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
5
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
6
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -404,7 +590,7 @@ func (x *UpdateAttachmentRequest) String() string {
...
@@ -404,7 +590,7 @@ func (x *UpdateAttachmentRequest) String() string {
func
(
*
UpdateAttachmentRequest
)
ProtoMessage
()
{}
func
(
*
UpdateAttachmentRequest
)
ProtoMessage
()
{}
func
(
x
*
UpdateAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
UpdateAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
5
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
6
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -417,7 +603,7 @@ func (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message {
...
@@ -417,7 +603,7 @@ func (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateAttachmentRequest.ProtoReflect.Descriptor instead.
// Deprecated: Use UpdateAttachmentRequest.ProtoReflect.Descriptor instead.
func
(
*
UpdateAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
UpdateAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
5
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
6
}
}
}
func
(
x
*
UpdateAttachmentRequest
)
GetAttachment
()
*
Attachment
{
func
(
x
*
UpdateAttachmentRequest
)
GetAttachment
()
*
Attachment
{
...
@@ -445,7 +631,7 @@ type DeleteAttachmentRequest struct {
...
@@ -445,7 +631,7 @@ type DeleteAttachmentRequest struct {
func
(
x
*
DeleteAttachmentRequest
)
Reset
()
{
func
(
x
*
DeleteAttachmentRequest
)
Reset
()
{
*
x
=
DeleteAttachmentRequest
{}
*
x
=
DeleteAttachmentRequest
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
6
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
7
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -457,7 +643,7 @@ func (x *DeleteAttachmentRequest) String() string {
...
@@ -457,7 +643,7 @@ func (x *DeleteAttachmentRequest) String() string {
func
(
*
DeleteAttachmentRequest
)
ProtoMessage
()
{}
func
(
*
DeleteAttachmentRequest
)
ProtoMessage
()
{}
func
(
x
*
DeleteAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
DeleteAttachmentRequest
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
6
]
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
7
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -470,7 +656,7 @@ func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {
...
@@ -470,7 +656,7 @@ func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead.
// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead.
func
(
*
DeleteAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
DeleteAttachmentRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
6
}
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
7
}
}
}
func
(
x
*
DeleteAttachmentRequest
)
GetName
()
string
{
func
(
x
*
DeleteAttachmentRequest
)
GetName
()
string
{
...
@@ -480,11 +666,61 @@ func (x *DeleteAttachmentRequest) GetName() string {
...
@@ -480,11 +666,61 @@ func (x *DeleteAttachmentRequest) GetName() string {
return
""
return
""
}
}
type
BatchDeleteAttachmentsRequest
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
Names
[]
string
`protobuf:"bytes,1,rep,name=names,proto3" json:"names,omitempty"`
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
}
func
(
x
*
BatchDeleteAttachmentsRequest
)
Reset
()
{
*
x
=
BatchDeleteAttachmentsRequest
{}
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
8
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
}
func
(
x
*
BatchDeleteAttachmentsRequest
)
String
()
string
{
return
protoimpl
.
X
.
MessageStringOf
(
x
)
}
func
(
*
BatchDeleteAttachmentsRequest
)
ProtoMessage
()
{}
func
(
x
*
BatchDeleteAttachmentsRequest
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_api_v1_attachment_service_proto_msgTypes
[
8
]
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
ms
.
StoreMessageInfo
(
mi
)
}
return
ms
}
return
mi
.
MessageOf
(
x
)
}
// Deprecated: Use BatchDeleteAttachmentsRequest.ProtoReflect.Descriptor instead.
func
(
*
BatchDeleteAttachmentsRequest
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_api_v1_attachment_service_proto_rawDescGZIP
(),
[]
int
{
8
}
}
func
(
x
*
BatchDeleteAttachmentsRequest
)
GetNames
()
[]
string
{
if
x
!=
nil
{
return
x
.
Names
}
return
nil
}
var
File_api_v1_attachment_service_proto
protoreflect
.
FileDescriptor
var
File_api_v1_attachment_service_proto
protoreflect
.
FileDescriptor
const
file_api_v1_attachment_service_proto_rawDesc
=
""
+
const
file_api_v1_attachment_service_proto_rawDesc
=
""
+
"
\n
"
+
"
\n
"
+
"
\x1f
api/v1/attachment_service.proto
\x12\f
memos.api.v1
\x1a\x1c
google/api/annotations.proto
\x1a\x17
google/api/client.proto
\x1a\x1f
google/api/field_behavior.proto
\x1a\x19
google/api/resource.proto
\x1a\x1b
google/protobuf/empty.proto
\x1a
google/protobuf/field_mask.proto
\x1a\x1f
google/protobuf/timestamp.proto
\"\xfb\x02\n
"
+
"
\x1f
api/v1/attachment_service.proto
\x12\f
memos.api.v1
\x1a\x1c
google/api/annotations.proto
\x1a\x17
google/api/client.proto
\x1a\x1f
google/api/field_behavior.proto
\x1a\x19
google/api/resource.proto
\x1a\x1b
google/protobuf/empty.proto
\x1a
google/protobuf/field_mask.proto
\x1a\x1f
google/protobuf/timestamp.proto
\"\xfe\x01\n
"
+
"
\v
MotionMedia
\x12
7
\n
"
+
"
\x06
family
\x18\x01
\x01
(
\x0e
2
\x1f
.memos.api.v1.MotionMediaFamilyR
\x06
family
\x12
1
\n
"
+
"
\x04
role
\x18\x02
\x01
(
\x0e
2
\x1d
.memos.api.v1.MotionMediaRoleR
\x04
role
\x12\x19\n
"
+
"
\b
group_id
\x18\x03
\x01
(
\t
R
\a
groupId
\x12
:
\n
"
+
"
\x19
presentation_timestamp_us
\x18\x04
\x01
(
\x03
R
\x17
presentationTimestampUs
\x12
,
\n
"
+
"
\x12
has_embedded_video
\x18\x05
\x01
(
\b
R
\x10
hasEmbeddedVideo
\"\xbe\x03\n
"
+
"
\n
"
+
"
\n
"
+
"Attachment
\x12\x17\n
"
+
"Attachment
\x12\x17\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x03\xe0
A
\b
R
\x04
name
\x12
@
\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x03\xe0
A
\b
R
\x04
name
\x12
@
\n
"
+
...
@@ -495,7 +731,8 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
...
@@ -495,7 +731,8 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
"
\r
external_link
\x18\x05
\x01
(
\t
B
\x03\xe0
A
\x01
R
\f
externalLink
\x12\x17\n
"
+
"
\r
external_link
\x18\x05
\x01
(
\t
B
\x03\xe0
A
\x01
R
\f
externalLink
\x12\x17\n
"
+
"
\x04
type
\x18\x06
\x01
(
\t
B
\x03\xe0
A
\x02
R
\x04
type
\x12\x17\n
"
+
"
\x04
type
\x18\x06
\x01
(
\t
B
\x03\xe0
A
\x02
R
\x04
type
\x12\x17\n
"
+
"
\x04
size
\x18\a
\x01
(
\x03
B
\x03\xe0
A
\x03
R
\x04
size
\x12\x1c\n
"
+
"
\x04
size
\x18\a
\x01
(
\x03
B
\x03\xe0
A
\x03
R
\x04
size
\x12\x1c\n
"
+
"
\x04
memo
\x18\b
\x01
(
\t
B
\x03\xe0
A
\x01
H
\x00
R
\x04
memo
\x88\x01\x01
:O
\xea
AL
\n
"
+
"
\x04
memo
\x18\b
\x01
(
\t
B
\x03\xe0
A
\x01
H
\x00
R
\x04
memo
\x88\x01\x01\x12
A
\n
"
+
"
\f
motion_media
\x18\t
\x01
(
\v
2
\x19
.memos.api.v1.MotionMediaB
\x03\xe0
A
\x01
R
\v
motionMedia:O
\xea
AL
\n
"
+
"
\x17
memos.api.v1/Attachment
\x12\x18
attachments/{attachment}*
\v
attachments2
\n
"
+
"
\x17
memos.api.v1/Attachment
\x12\x18
attachments/{attachment}*
\v
attachments2
\n
"
+
"attachmentB
\a\n
"
+
"attachmentB
\a\n
"
+
"
\x05
_memo
\"\x82\x01\n
"
+
"
\x05
_memo
\"\x82\x01\n
"
+
...
@@ -526,7 +763,18 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
...
@@ -526,7 +763,18 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
"updateMask
\"
N
\n
"
+
"updateMask
\"
N
\n
"
+
"
\x17
DeleteAttachmentRequest
\x12
3
\n
"
+
"
\x17
DeleteAttachmentRequest
\x12
3
\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x1f\xe0
A
\x02\xfa
A
\x19\n
"
+
"
\x04
name
\x18\x01
\x01
(
\t
B
\x1f\xe0
A
\x02\xfa
A
\x19\n
"
+
"
\x17
memos.api.v1/AttachmentR
\x04
name2
\xc4\x05\n
"
+
"
\x17
memos.api.v1/AttachmentR
\x04
name
\"
:
\n
"
+
"
\x1d
BatchDeleteAttachmentsRequest
\x12\x19\n
"
+
"
\x05
names
\x18\x01
\x03
(
\t
B
\x03\xe0
A
\x02
R
\x05
names*h
\n
"
+
"
\x11
MotionMediaFamily
\x12
#
\n
"
+
"
\x1f
MOTION_MEDIA_FAMILY_UNSPECIFIED
\x10\x00\x12\x14\n
"
+
"
\x10
APPLE_LIVE_PHOTO
\x10\x01\x12\x18\n
"
+
"
\x14
ANDROID_MOTION_PHOTO
\x10\x02
*Y
\n
"
+
"
\x0f
MotionMediaRole
\x12
!
\n
"
+
"
\x1d
MOTION_MEDIA_ROLE_UNSPECIFIED
\x10\x00\x12\t\n
"
+
"
\x05
STILL
\x10\x01\x12\t\n
"
+
"
\x05
VIDEO
\x10\x02\x12\r\n
"
+
"
\t
CONTAINER
\x10\x03
2
\xd0\x06\n
"
+
"
\x11
AttachmentService
\x12\x89\x01\n
"
+
"
\x11
AttachmentService
\x12\x89\x01\n
"
+
"
\x10
CreateAttachment
\x12
%.memos.api.v1.CreateAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
4
\xda
A
\n
"
+
"
\x10
CreateAttachment
\x12
%.memos.api.v1.CreateAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
4
\xda
A
\n
"
+
"attachment
\x82\xd3\xe4\x93\x02
!:
\n
"
+
"attachment
\x82\xd3\xe4\x93\x02
!:
\n
"
+
...
@@ -535,7 +783,8 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
...
@@ -535,7 +783,8 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
"
\r
GetAttachment
\x12\"
.memos.api.v1.GetAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e\x12\x1c
/api/v1/{name=attachments/*}
\x12\xa9\x01\n
"
+
"
\r
GetAttachment
\x12\"
.memos.api.v1.GetAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e\x12\x1c
/api/v1/{name=attachments/*}
\x12\xa9\x01\n
"
+
"
\x10
UpdateAttachment
\x12
%.memos.api.v1.UpdateAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
T
\xda
A
\x16
attachment,update_mask
\x82\xd3\xe4\x93\x02
5:
\n
"
+
"
\x10
UpdateAttachment
\x12
%.memos.api.v1.UpdateAttachmentRequest
\x1a\x18
.memos.api.v1.Attachment
\"
T
\xda
A
\x16
attachment,update_mask
\x82\xd3\xe4\x93\x02
5:
\n
"
+
"attachment2'/api/v1/{attachment.name=attachments/*}
\x12
~
\n
"
+
"attachment2'/api/v1/{attachment.name=attachments/*}
\x12
~
\n
"
+
"
\x10
DeleteAttachment
\x12
%.memos.api.v1.DeleteAttachmentRequest
\x1a\x16
.google.protobuf.Empty
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e
*
\x1c
/api/v1/{name=attachments/*}B
\xae\x01\n
"
+
"
\x10
DeleteAttachment
\x12
%.memos.api.v1.DeleteAttachmentRequest
\x1a\x16
.google.protobuf.Empty
\"
+
\xda
A
\x04
name
\x82\xd3\xe4\x93\x02\x1e
*
\x1c
/api/v1/{name=attachments/*}
\x12\x89\x01\n
"
+
"
\x16
BatchDeleteAttachments
\x12
+.memos.api.v1.BatchDeleteAttachmentsRequest
\x1a\x16
.google.protobuf.Empty
\"
*
\x82\xd3\xe4\x93\x02
$:
\x01
*
\"\x1f
/api/v1/attachments:batchDeleteB
\xae\x01\n
"
+
"
\x10
com.memos.api.v1B
\x16
AttachmentServiceProtoP
\x01
Z0github.com/usememos/memos/proto/gen/api/v1;apiv1
\xa2\x02\x03
MAX
\xaa\x02\f
Memos.Api.V1
\xca\x02\f
Memos
\\
Api
\\
V1
\xe2\x02\x18
Memos
\\
Api
\\
V1
\\
GPBMetadata
\xea\x02\x0e
Memos::Api::V1b
\x06
proto3"
"
\x10
com.memos.api.v1B
\x16
AttachmentServiceProtoP
\x01
Z0github.com/usememos/memos/proto/gen/api/v1;apiv1
\xa2\x02\x03
MAX
\xaa\x02\f
Memos.Api.V1
\xca\x02\f
Memos
\\
Api
\\
V1
\xe2\x02\x18
Memos
\\
Api
\\
V1
\\
GPBMetadata
\xea\x02\x0e
Memos::Api::V1b
\x06
proto3"
var
(
var
(
...
@@ -550,40 +799,50 @@ func file_api_v1_attachment_service_proto_rawDescGZIP() []byte {
...
@@ -550,40 +799,50 @@ func file_api_v1_attachment_service_proto_rawDescGZIP() []byte {
return
file_api_v1_attachment_service_proto_rawDescData
return
file_api_v1_attachment_service_proto_rawDescData
}
}
var
file_api_v1_attachment_service_proto_msgTypes
=
make
([]
protoimpl
.
MessageInfo
,
7
)
var
file_api_v1_attachment_service_proto_enumTypes
=
make
([]
protoimpl
.
EnumInfo
,
2
)
var
file_api_v1_attachment_service_proto_msgTypes
=
make
([]
protoimpl
.
MessageInfo
,
9
)
var
file_api_v1_attachment_service_proto_goTypes
=
[]
any
{
var
file_api_v1_attachment_service_proto_goTypes
=
[]
any
{
(
*
Attachment
)(
nil
),
// 0: memos.api.v1.Attachment
(
MotionMediaFamily
)(
0
),
// 0: memos.api.v1.MotionMediaFamily
(
*
CreateAttachmentRequest
)(
nil
),
// 1: memos.api.v1.CreateAttachmentRequest
(
MotionMediaRole
)(
0
),
// 1: memos.api.v1.MotionMediaRole
(
*
ListAttachmentsRequest
)(
nil
),
// 2: memos.api.v1.ListAttachmentsRequest
(
*
MotionMedia
)(
nil
),
// 2: memos.api.v1.MotionMedia
(
*
ListAttachmentsResponse
)(
nil
),
// 3: memos.api.v1.ListAttachmentsResponse
(
*
Attachment
)(
nil
),
// 3: memos.api.v1.Attachment
(
*
GetAttachmentRequest
)(
nil
),
// 4: memos.api.v1.GetAttachmentRequest
(
*
CreateAttachmentRequest
)(
nil
),
// 4: memos.api.v1.CreateAttachmentRequest
(
*
UpdateAttachmentRequest
)(
nil
),
// 5: memos.api.v1.UpdateAttachmentRequest
(
*
ListAttachmentsRequest
)(
nil
),
// 5: memos.api.v1.ListAttachmentsRequest
(
*
DeleteAttachmentRequest
)(
nil
),
// 6: memos.api.v1.DeleteAttachmentRequest
(
*
ListAttachmentsResponse
)(
nil
),
// 6: memos.api.v1.ListAttachmentsResponse
(
*
timestamppb
.
Timestamp
)(
nil
),
// 7: google.protobuf.Timestamp
(
*
GetAttachmentRequest
)(
nil
),
// 7: memos.api.v1.GetAttachmentRequest
(
*
fieldmaskpb
.
FieldMask
)(
nil
),
// 8: google.protobuf.FieldMask
(
*
UpdateAttachmentRequest
)(
nil
),
// 8: memos.api.v1.UpdateAttachmentRequest
(
*
emptypb
.
Empty
)(
nil
),
// 9: google.protobuf.Empty
(
*
DeleteAttachmentRequest
)(
nil
),
// 9: memos.api.v1.DeleteAttachmentRequest
(
*
BatchDeleteAttachmentsRequest
)(
nil
),
// 10: memos.api.v1.BatchDeleteAttachmentsRequest
(
*
timestamppb
.
Timestamp
)(
nil
),
// 11: google.protobuf.Timestamp
(
*
fieldmaskpb
.
FieldMask
)(
nil
),
// 12: google.protobuf.FieldMask
(
*
emptypb
.
Empty
)(
nil
),
// 13: google.protobuf.Empty
}
}
var
file_api_v1_attachment_service_proto_depIdxs
=
[]
int32
{
var
file_api_v1_attachment_service_proto_depIdxs
=
[]
int32
{
7
,
// 0: memos.api.v1.Attachment.create_time:type_name -> google.protobuf.Timestamp
0
,
// 0: memos.api.v1.MotionMedia.family:type_name -> memos.api.v1.MotionMediaFamily
0
,
// 1: memos.api.v1.CreateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
1
,
// 1: memos.api.v1.MotionMedia.role:type_name -> memos.api.v1.MotionMediaRole
0
,
// 2: memos.api.v1.ListAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment
11
,
// 2: memos.api.v1.Attachment.create_time:type_name -> google.protobuf.Timestamp
0
,
// 3: memos.api.v1.UpdateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
2
,
// 3: memos.api.v1.Attachment.motion_media:type_name -> memos.api.v1.MotionMedia
8
,
// 4: memos.api.v1.UpdateAttachmentRequest.update_mask:type_name -> google.protobuf.FieldMask
3
,
// 4: memos.api.v1.CreateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
1
,
// 5: memos.api.v1.AttachmentService.CreateAttachment:input_type -> memos.api.v1.CreateAttachmentRequest
3
,
// 5: memos.api.v1.ListAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment
2
,
// 6: memos.api.v1.AttachmentService.ListAttachments:input_type -> memos.api.v1.ListAttachmentsRequest
3
,
// 6: memos.api.v1.UpdateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
4
,
// 7: memos.api.v1.AttachmentService.GetAttachment:input_type -> memos.api.v1.GetAttachmentRequest
12
,
// 7: memos.api.v1.UpdateAttachmentRequest.update_mask:type_name -> google.protobuf.FieldMask
5
,
// 8: memos.api.v1.AttachmentService.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest
4
,
// 8: memos.api.v1.AttachmentService.CreateAttachment:input_type -> memos.api.v1.CreateAttachmentRequest
6
,
// 9: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest
5
,
// 9: memos.api.v1.AttachmentService.ListAttachments:input_type -> memos.api.v1.ListAttachmentsRequest
0
,
// 10: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment
7
,
// 10: memos.api.v1.AttachmentService.GetAttachment:input_type -> memos.api.v1.GetAttachmentRequest
3
,
// 11: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse
8
,
// 11: memos.api.v1.AttachmentService.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest
0
,
// 12: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment
9
,
// 12: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest
0
,
// 13: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment
10
,
// 13: memos.api.v1.AttachmentService.BatchDeleteAttachments:input_type -> memos.api.v1.BatchDeleteAttachmentsRequest
9
,
// 14: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty
3
,
// 14: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment
10
,
// [10:15] is the sub-list for method output_type
6
,
// 15: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse
5
,
// [5:10] is the sub-list for method input_type
3
,
// 16: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment
5
,
// [5:5] is the sub-list for extension type_name
3
,
// 17: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment
5
,
// [5:5] is the sub-list for extension extendee
13
,
// 18: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty
0
,
// [0:5] is the sub-list for field type_name
13
,
// 19: memos.api.v1.AttachmentService.BatchDeleteAttachments:output_type -> google.protobuf.Empty
14
,
// [14:20] is the sub-list for method output_type
8
,
// [8:14] is the sub-list for method input_type
8
,
// [8:8] is the sub-list for extension type_name
8
,
// [8:8] is the sub-list for extension extendee
0
,
// [0:8] is the sub-list for field type_name
}
}
func
init
()
{
file_api_v1_attachment_service_proto_init
()
}
func
init
()
{
file_api_v1_attachment_service_proto_init
()
}
...
@@ -591,19 +850,20 @@ func file_api_v1_attachment_service_proto_init() {
...
@@ -591,19 +850,20 @@ func file_api_v1_attachment_service_proto_init() {
if
File_api_v1_attachment_service_proto
!=
nil
{
if
File_api_v1_attachment_service_proto
!=
nil
{
return
return
}
}
file_api_v1_attachment_service_proto_msgTypes
[
0
]
.
OneofWrappers
=
[]
any
{}
file_api_v1_attachment_service_proto_msgTypes
[
1
]
.
OneofWrappers
=
[]
any
{}
type
x
struct
{}
type
x
struct
{}
out
:=
protoimpl
.
TypeBuilder
{
out
:=
protoimpl
.
TypeBuilder
{
File
:
protoimpl
.
DescBuilder
{
File
:
protoimpl
.
DescBuilder
{
GoPackagePath
:
reflect
.
TypeOf
(
x
{})
.
PkgPath
(),
GoPackagePath
:
reflect
.
TypeOf
(
x
{})
.
PkgPath
(),
RawDescriptor
:
unsafe
.
Slice
(
unsafe
.
StringData
(
file_api_v1_attachment_service_proto_rawDesc
),
len
(
file_api_v1_attachment_service_proto_rawDesc
)),
RawDescriptor
:
unsafe
.
Slice
(
unsafe
.
StringData
(
file_api_v1_attachment_service_proto_rawDesc
),
len
(
file_api_v1_attachment_service_proto_rawDesc
)),
NumEnums
:
0
,
NumEnums
:
2
,
NumMessages
:
7
,
NumMessages
:
9
,
NumExtensions
:
0
,
NumExtensions
:
0
,
NumServices
:
1
,
NumServices
:
1
,
},
},
GoTypes
:
file_api_v1_attachment_service_proto_goTypes
,
GoTypes
:
file_api_v1_attachment_service_proto_goTypes
,
DependencyIndexes
:
file_api_v1_attachment_service_proto_depIdxs
,
DependencyIndexes
:
file_api_v1_attachment_service_proto_depIdxs
,
EnumInfos
:
file_api_v1_attachment_service_proto_enumTypes
,
MessageInfos
:
file_api_v1_attachment_service_proto_msgTypes
,
MessageInfos
:
file_api_v1_attachment_service_proto_msgTypes
,
}
.
Build
()
}
.
Build
()
File_api_v1_attachment_service_proto
=
out
.
File
File_api_v1_attachment_service_proto
=
out
.
File
...
...
proto/gen/api/v1/attachment_service.pb.gw.go
View file @
4b4e7194
...
@@ -270,6 +270,33 @@ func local_request_AttachmentService_DeleteAttachment_0(ctx context.Context, mar
...
@@ -270,6 +270,33 @@ func local_request_AttachmentService_DeleteAttachment_0(ctx context.Context, mar
return
msg
,
metadata
,
err
return
msg
,
metadata
,
err
}
}
func
request_AttachmentService_BatchDeleteAttachments_0
(
ctx
context
.
Context
,
marshaler
runtime
.
Marshaler
,
client
AttachmentServiceClient
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
(
proto
.
Message
,
runtime
.
ServerMetadata
,
error
)
{
var
(
protoReq
BatchDeleteAttachmentsRequest
metadata
runtime
.
ServerMetadata
)
if
err
:=
marshaler
.
NewDecoder
(
req
.
Body
)
.
Decode
(
&
protoReq
);
err
!=
nil
&&
!
errors
.
Is
(
err
,
io
.
EOF
)
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"%v"
,
err
)
}
if
req
.
Body
!=
nil
{
_
,
_
=
io
.
Copy
(
io
.
Discard
,
req
.
Body
)
}
msg
,
err
:=
client
.
BatchDeleteAttachments
(
ctx
,
&
protoReq
,
grpc
.
Header
(
&
metadata
.
HeaderMD
),
grpc
.
Trailer
(
&
metadata
.
TrailerMD
))
return
msg
,
metadata
,
err
}
func
local_request_AttachmentService_BatchDeleteAttachments_0
(
ctx
context
.
Context
,
marshaler
runtime
.
Marshaler
,
server
AttachmentServiceServer
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
(
proto
.
Message
,
runtime
.
ServerMetadata
,
error
)
{
var
(
protoReq
BatchDeleteAttachmentsRequest
metadata
runtime
.
ServerMetadata
)
if
err
:=
marshaler
.
NewDecoder
(
req
.
Body
)
.
Decode
(
&
protoReq
);
err
!=
nil
&&
!
errors
.
Is
(
err
,
io
.
EOF
)
{
return
nil
,
metadata
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"%v"
,
err
)
}
msg
,
err
:=
server
.
BatchDeleteAttachments
(
ctx
,
&
protoReq
)
return
msg
,
metadata
,
err
}
// RegisterAttachmentServiceHandlerServer registers the http handlers for service AttachmentService to "mux".
// RegisterAttachmentServiceHandlerServer registers the http handlers for service AttachmentService to "mux".
// UnaryRPC :call AttachmentServiceServer directly.
// UnaryRPC :call AttachmentServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
...
@@ -376,6 +403,26 @@ func RegisterAttachmentServiceHandlerServer(ctx context.Context, mux *runtime.Se
...
@@ -376,6 +403,26 @@ func RegisterAttachmentServiceHandlerServer(ctx context.Context, mux *runtime.Se
}
}
forward_AttachmentService_DeleteAttachment_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
forward_AttachmentService_DeleteAttachment_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
})
mux
.
Handle
(
http
.
MethodPost
,
pattern_AttachmentService_BatchDeleteAttachments_0
,
func
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
req
.
Context
())
defer
cancel
()
var
stream
runtime
.
ServerTransportStream
ctx
=
grpc
.
NewContextWithServerTransportStream
(
ctx
,
&
stream
)
inboundMarshaler
,
outboundMarshaler
:=
runtime
.
MarshalerForRequest
(
mux
,
req
)
annotatedContext
,
err
:=
runtime
.
AnnotateIncomingContext
(
ctx
,
mux
,
req
,
"/memos.api.v1.AttachmentService/BatchDeleteAttachments"
,
runtime
.
WithHTTPPathPattern
(
"/api/v1/attachments:batchDelete"
))
if
err
!=
nil
{
runtime
.
HTTPError
(
ctx
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
resp
,
md
,
err
:=
local_request_AttachmentService_BatchDeleteAttachments_0
(
annotatedContext
,
inboundMarshaler
,
server
,
req
,
pathParams
)
md
.
HeaderMD
,
md
.
TrailerMD
=
metadata
.
Join
(
md
.
HeaderMD
,
stream
.
Header
()),
metadata
.
Join
(
md
.
TrailerMD
,
stream
.
Trailer
())
annotatedContext
=
runtime
.
NewServerMetadataContext
(
annotatedContext
,
md
)
if
err
!=
nil
{
runtime
.
HTTPError
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
forward_AttachmentService_BatchDeleteAttachments_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
return
nil
return
nil
}
}
...
@@ -501,6 +548,23 @@ func RegisterAttachmentServiceHandlerClient(ctx context.Context, mux *runtime.Se
...
@@ -501,6 +548,23 @@ func RegisterAttachmentServiceHandlerClient(ctx context.Context, mux *runtime.Se
}
}
forward_AttachmentService_DeleteAttachment_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
forward_AttachmentService_DeleteAttachment_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
})
mux
.
Handle
(
http
.
MethodPost
,
pattern_AttachmentService_BatchDeleteAttachments_0
,
func
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
,
pathParams
map
[
string
]
string
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
req
.
Context
())
defer
cancel
()
inboundMarshaler
,
outboundMarshaler
:=
runtime
.
MarshalerForRequest
(
mux
,
req
)
annotatedContext
,
err
:=
runtime
.
AnnotateContext
(
ctx
,
mux
,
req
,
"/memos.api.v1.AttachmentService/BatchDeleteAttachments"
,
runtime
.
WithHTTPPathPattern
(
"/api/v1/attachments:batchDelete"
))
if
err
!=
nil
{
runtime
.
HTTPError
(
ctx
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
resp
,
md
,
err
:=
request_AttachmentService_BatchDeleteAttachments_0
(
annotatedContext
,
inboundMarshaler
,
client
,
req
,
pathParams
)
annotatedContext
=
runtime
.
NewServerMetadataContext
(
annotatedContext
,
md
)
if
err
!=
nil
{
runtime
.
HTTPError
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
err
)
return
}
forward_AttachmentService_BatchDeleteAttachments_0
(
annotatedContext
,
mux
,
outboundMarshaler
,
w
,
req
,
resp
,
mux
.
GetForwardResponseOptions
()
...
)
})
return
nil
return
nil
}
}
...
@@ -510,6 +574,7 @@ var (
...
@@ -510,6 +574,7 @@ var (
pattern_AttachmentService_GetAttachment_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
},
[]
string
{
"api"
,
"v1"
,
"attachments"
,
"name"
},
""
))
pattern_AttachmentService_GetAttachment_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
},
[]
string
{
"api"
,
"v1"
,
"attachments"
,
"name"
},
""
))
pattern_AttachmentService_UpdateAttachment_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
},
[]
string
{
"api"
,
"v1"
,
"attachments"
,
"attachment.name"
},
""
))
pattern_AttachmentService_UpdateAttachment_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
},
[]
string
{
"api"
,
"v1"
,
"attachments"
,
"attachment.name"
},
""
))
pattern_AttachmentService_DeleteAttachment_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
},
[]
string
{
"api"
,
"v1"
,
"attachments"
,
"name"
},
""
))
pattern_AttachmentService_DeleteAttachment_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
,
1
,
0
,
4
,
2
,
5
,
3
},
[]
string
{
"api"
,
"v1"
,
"attachments"
,
"name"
},
""
))
pattern_AttachmentService_BatchDeleteAttachments_0
=
runtime
.
MustPattern
(
runtime
.
NewPattern
(
1
,
[]
int
{
2
,
0
,
2
,
1
,
2
,
2
},
[]
string
{
"api"
,
"v1"
,
"attachments"
},
"batchDelete"
))
)
)
var
(
var
(
...
@@ -518,4 +583,5 @@ var (
...
@@ -518,4 +583,5 @@ var (
forward_AttachmentService_GetAttachment_0
=
runtime
.
ForwardResponseMessage
forward_AttachmentService_GetAttachment_0
=
runtime
.
ForwardResponseMessage
forward_AttachmentService_UpdateAttachment_0
=
runtime
.
ForwardResponseMessage
forward_AttachmentService_UpdateAttachment_0
=
runtime
.
ForwardResponseMessage
forward_AttachmentService_DeleteAttachment_0
=
runtime
.
ForwardResponseMessage
forward_AttachmentService_DeleteAttachment_0
=
runtime
.
ForwardResponseMessage
forward_AttachmentService_BatchDeleteAttachments_0
=
runtime
.
ForwardResponseMessage
)
)
proto/gen/api/v1/attachment_service_grpc.pb.go
View file @
4b4e7194
...
@@ -25,6 +25,7 @@ const (
...
@@ -25,6 +25,7 @@ const (
AttachmentService_GetAttachment_FullMethodName
=
"/memos.api.v1.AttachmentService/GetAttachment"
AttachmentService_GetAttachment_FullMethodName
=
"/memos.api.v1.AttachmentService/GetAttachment"
AttachmentService_UpdateAttachment_FullMethodName
=
"/memos.api.v1.AttachmentService/UpdateAttachment"
AttachmentService_UpdateAttachment_FullMethodName
=
"/memos.api.v1.AttachmentService/UpdateAttachment"
AttachmentService_DeleteAttachment_FullMethodName
=
"/memos.api.v1.AttachmentService/DeleteAttachment"
AttachmentService_DeleteAttachment_FullMethodName
=
"/memos.api.v1.AttachmentService/DeleteAttachment"
AttachmentService_BatchDeleteAttachments_FullMethodName
=
"/memos.api.v1.AttachmentService/BatchDeleteAttachments"
)
)
// AttachmentServiceClient is the client API for AttachmentService service.
// AttachmentServiceClient is the client API for AttachmentService service.
...
@@ -41,6 +42,8 @@ type AttachmentServiceClient interface {
...
@@ -41,6 +42,8 @@ type AttachmentServiceClient interface {
UpdateAttachment
(
ctx
context
.
Context
,
in
*
UpdateAttachmentRequest
,
opts
...
grpc
.
CallOption
)
(
*
Attachment
,
error
)
UpdateAttachment
(
ctx
context
.
Context
,
in
*
UpdateAttachmentRequest
,
opts
...
grpc
.
CallOption
)
(
*
Attachment
,
error
)
// DeleteAttachment deletes an attachment by name.
// DeleteAttachment deletes an attachment by name.
DeleteAttachment
(
ctx
context
.
Context
,
in
*
DeleteAttachmentRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
DeleteAttachment
(
ctx
context
.
Context
,
in
*
DeleteAttachmentRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
// BatchDeleteAttachments deletes multiple attachments in one request.
BatchDeleteAttachments
(
ctx
context
.
Context
,
in
*
BatchDeleteAttachmentsRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
}
}
type
attachmentServiceClient
struct
{
type
attachmentServiceClient
struct
{
...
@@ -101,6 +104,16 @@ func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, in *Dele
...
@@ -101,6 +104,16 @@ func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, in *Dele
return
out
,
nil
return
out
,
nil
}
}
func
(
c
*
attachmentServiceClient
)
BatchDeleteAttachments
(
ctx
context
.
Context
,
in
*
BatchDeleteAttachmentsRequest
,
opts
...
grpc
.
CallOption
)
(
*
emptypb
.
Empty
,
error
)
{
cOpts
:=
append
([]
grpc
.
CallOption
{
grpc
.
StaticMethod
()},
opts
...
)
out
:=
new
(
emptypb
.
Empty
)
err
:=
c
.
cc
.
Invoke
(
ctx
,
AttachmentService_BatchDeleteAttachments_FullMethodName
,
in
,
out
,
cOpts
...
)
if
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
// AttachmentServiceServer is the server API for AttachmentService service.
// AttachmentServiceServer is the server API for AttachmentService service.
// All implementations must embed UnimplementedAttachmentServiceServer
// All implementations must embed UnimplementedAttachmentServiceServer
// for forward compatibility.
// for forward compatibility.
...
@@ -115,6 +128,8 @@ type AttachmentServiceServer interface {
...
@@ -115,6 +128,8 @@ type AttachmentServiceServer interface {
UpdateAttachment
(
context
.
Context
,
*
UpdateAttachmentRequest
)
(
*
Attachment
,
error
)
UpdateAttachment
(
context
.
Context
,
*
UpdateAttachmentRequest
)
(
*
Attachment
,
error
)
// DeleteAttachment deletes an attachment by name.
// DeleteAttachment deletes an attachment by name.
DeleteAttachment
(
context
.
Context
,
*
DeleteAttachmentRequest
)
(
*
emptypb
.
Empty
,
error
)
DeleteAttachment
(
context
.
Context
,
*
DeleteAttachmentRequest
)
(
*
emptypb
.
Empty
,
error
)
// BatchDeleteAttachments deletes multiple attachments in one request.
BatchDeleteAttachments
(
context
.
Context
,
*
BatchDeleteAttachmentsRequest
)
(
*
emptypb
.
Empty
,
error
)
mustEmbedUnimplementedAttachmentServiceServer
()
mustEmbedUnimplementedAttachmentServiceServer
()
}
}
...
@@ -140,6 +155,9 @@ func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *U
...
@@ -140,6 +155,9 @@ func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *U
func
(
UnimplementedAttachmentServiceServer
)
DeleteAttachment
(
context
.
Context
,
*
DeleteAttachmentRequest
)
(
*
emptypb
.
Empty
,
error
)
{
func
(
UnimplementedAttachmentServiceServer
)
DeleteAttachment
(
context
.
Context
,
*
DeleteAttachmentRequest
)
(
*
emptypb
.
Empty
,
error
)
{
return
nil
,
status
.
Error
(
codes
.
Unimplemented
,
"method DeleteAttachment not implemented"
)
return
nil
,
status
.
Error
(
codes
.
Unimplemented
,
"method DeleteAttachment not implemented"
)
}
}
func
(
UnimplementedAttachmentServiceServer
)
BatchDeleteAttachments
(
context
.
Context
,
*
BatchDeleteAttachmentsRequest
)
(
*
emptypb
.
Empty
,
error
)
{
return
nil
,
status
.
Error
(
codes
.
Unimplemented
,
"method BatchDeleteAttachments not implemented"
)
}
func
(
UnimplementedAttachmentServiceServer
)
mustEmbedUnimplementedAttachmentServiceServer
()
{}
func
(
UnimplementedAttachmentServiceServer
)
mustEmbedUnimplementedAttachmentServiceServer
()
{}
func
(
UnimplementedAttachmentServiceServer
)
testEmbeddedByValue
()
{}
func
(
UnimplementedAttachmentServiceServer
)
testEmbeddedByValue
()
{}
...
@@ -251,6 +269,24 @@ func _AttachmentService_DeleteAttachment_Handler(srv interface{}, ctx context.Co
...
@@ -251,6 +269,24 @@ func _AttachmentService_DeleteAttachment_Handler(srv interface{}, ctx context.Co
return
interceptor
(
ctx
,
in
,
info
,
handler
)
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
}
func
_AttachmentService_BatchDeleteAttachments_Handler
(
srv
interface
{},
ctx
context
.
Context
,
dec
func
(
interface
{})
error
,
interceptor
grpc
.
UnaryServerInterceptor
)
(
interface
{},
error
)
{
in
:=
new
(
BatchDeleteAttachmentsRequest
)
if
err
:=
dec
(
in
);
err
!=
nil
{
return
nil
,
err
}
if
interceptor
==
nil
{
return
srv
.
(
AttachmentServiceServer
)
.
BatchDeleteAttachments
(
ctx
,
in
)
}
info
:=
&
grpc
.
UnaryServerInfo
{
Server
:
srv
,
FullMethod
:
AttachmentService_BatchDeleteAttachments_FullMethodName
,
}
handler
:=
func
(
ctx
context
.
Context
,
req
interface
{})
(
interface
{},
error
)
{
return
srv
.
(
AttachmentServiceServer
)
.
BatchDeleteAttachments
(
ctx
,
req
.
(
*
BatchDeleteAttachmentsRequest
))
}
return
interceptor
(
ctx
,
in
,
info
,
handler
)
}
// AttachmentService_ServiceDesc is the grpc.ServiceDesc for AttachmentService service.
// AttachmentService_ServiceDesc is the grpc.ServiceDesc for AttachmentService service.
// It's only intended for direct use with grpc.RegisterService,
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
// and not to be introspected or modified (even as a copy)
...
@@ -278,6 +314,10 @@ var AttachmentService_ServiceDesc = grpc.ServiceDesc{
...
@@ -278,6 +314,10 @@ var AttachmentService_ServiceDesc = grpc.ServiceDesc{
MethodName
:
"DeleteAttachment"
,
MethodName
:
"DeleteAttachment"
,
Handler
:
_AttachmentService_DeleteAttachment_Handler
,
Handler
:
_AttachmentService_DeleteAttachment_Handler
,
},
},
{
MethodName
:
"BatchDeleteAttachments"
,
Handler
:
_AttachmentService_BatchDeleteAttachments_Handler
,
},
},
},
Streams
:
[]
grpc
.
StreamDesc
{},
Streams
:
[]
grpc
.
StreamDesc
{},
Metadata
:
"api/v1/attachment_service.proto"
,
Metadata
:
"api/v1/attachment_service.proto"
,
...
...
proto/gen/openapi.yaml
View file @
4b4e7194
...
@@ -176,6 +176,28 @@ paths:
...
@@ -176,6 +176,28 @@ paths:
application/json
:
application/json
:
schema
:
schema
:
$ref
:
'
#/components/schemas/Status'
$ref
:
'
#/components/schemas/Status'
/api/v1/attachments:batchDelete
:
post
:
tags
:
-
AttachmentService
description
:
BatchDeleteAttachments deletes multiple attachments in one request.
operationId
:
AttachmentService_BatchDeleteAttachments
requestBody
:
content
:
application/json
:
schema
:
$ref
:
'
#/components/schemas/BatchDeleteAttachmentsRequest'
required
:
true
responses
:
"
200"
:
description
:
OK
content
:
{}
default
:
description
:
Default error response
content
:
application/json
:
schema
:
$ref
:
'
#/components/schemas/Status'
/api/v1/auth/me
:
/api/v1/auth/me
:
get
:
get
:
tags
:
tags
:
...
@@ -2015,6 +2037,19 @@ components:
...
@@ -2015,6 +2037,19 @@ components:
description
:
|-
description
:
|-
Optional. The related memo. Refer to `Memo.name`.
Optional. The related memo. Refer to `Memo.name`.
Format: memos/{memo}
Format: memos/{memo}
motionMedia
:
allOf
:
-
$ref
:
'
#/components/schemas/MotionMedia'
description
:
Optional. Motion media metadata.
BatchDeleteAttachmentsRequest
:
required
:
-
names
type
:
object
properties
:
names
:
type
:
array
items
:
type
:
string
Color
:
Color
:
type
:
object
type
:
object
properties
:
properties
:
...
@@ -2781,6 +2816,30 @@ components:
...
@@ -2781,6 +2816,30 @@ components:
type
:
string
type
:
string
description
:
The title extracted from the first H1 heading, if present.
description
:
The title extracted from the first H1 heading, if present.
description
:
Computed properties of a memo.
description
:
Computed properties of a memo.
MotionMedia
:
type
:
object
properties
:
family
:
enum
:
-
MOTION_MEDIA_FAMILY_UNSPECIFIED
-
APPLE_LIVE_PHOTO
-
ANDROID_MOTION_PHOTO
type
:
string
format
:
enum
role
:
enum
:
-
MOTION_MEDIA_ROLE_UNSPECIFIED
-
STILL
-
VIDEO
-
CONTAINER
type
:
string
format
:
enum
groupId
:
type
:
string
presentationTimestampUs
:
type
:
string
hasEmbeddedVideo
:
type
:
boolean
NotificationSetting_EmailSetting
:
NotificationSetting_EmailSetting
:
type
:
object
type
:
object
properties
:
properties
:
...
...
proto/gen/store/attachment.pb.go
View file @
4b4e7194
...
@@ -77,19 +77,197 @@ func (AttachmentStorageType) EnumDescriptor() ([]byte, []int) {
...
@@ -77,19 +77,197 @@ func (AttachmentStorageType) EnumDescriptor() ([]byte, []int) {
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
0
}
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
0
}
}
}
type
MotionMediaFamily
int32
const
(
MotionMediaFamily_MOTION_MEDIA_FAMILY_UNSPECIFIED
MotionMediaFamily
=
0
MotionMediaFamily_APPLE_LIVE_PHOTO
MotionMediaFamily
=
1
MotionMediaFamily_ANDROID_MOTION_PHOTO
MotionMediaFamily
=
2
)
// Enum value maps for MotionMediaFamily.
var
(
MotionMediaFamily_name
=
map
[
int32
]
string
{
0
:
"MOTION_MEDIA_FAMILY_UNSPECIFIED"
,
1
:
"APPLE_LIVE_PHOTO"
,
2
:
"ANDROID_MOTION_PHOTO"
,
}
MotionMediaFamily_value
=
map
[
string
]
int32
{
"MOTION_MEDIA_FAMILY_UNSPECIFIED"
:
0
,
"APPLE_LIVE_PHOTO"
:
1
,
"ANDROID_MOTION_PHOTO"
:
2
,
}
)
func
(
x
MotionMediaFamily
)
Enum
()
*
MotionMediaFamily
{
p
:=
new
(
MotionMediaFamily
)
*
p
=
x
return
p
}
func
(
x
MotionMediaFamily
)
String
()
string
{
return
protoimpl
.
X
.
EnumStringOf
(
x
.
Descriptor
(),
protoreflect
.
EnumNumber
(
x
))
}
func
(
MotionMediaFamily
)
Descriptor
()
protoreflect
.
EnumDescriptor
{
return
file_store_attachment_proto_enumTypes
[
1
]
.
Descriptor
()
}
func
(
MotionMediaFamily
)
Type
()
protoreflect
.
EnumType
{
return
&
file_store_attachment_proto_enumTypes
[
1
]
}
func
(
x
MotionMediaFamily
)
Number
()
protoreflect
.
EnumNumber
{
return
protoreflect
.
EnumNumber
(
x
)
}
// Deprecated: Use MotionMediaFamily.Descriptor instead.
func
(
MotionMediaFamily
)
EnumDescriptor
()
([]
byte
,
[]
int
)
{
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
1
}
}
type
MotionMediaRole
int32
const
(
MotionMediaRole_MOTION_MEDIA_ROLE_UNSPECIFIED
MotionMediaRole
=
0
MotionMediaRole_STILL
MotionMediaRole
=
1
MotionMediaRole_VIDEO
MotionMediaRole
=
2
MotionMediaRole_CONTAINER
MotionMediaRole
=
3
)
// Enum value maps for MotionMediaRole.
var
(
MotionMediaRole_name
=
map
[
int32
]
string
{
0
:
"MOTION_MEDIA_ROLE_UNSPECIFIED"
,
1
:
"STILL"
,
2
:
"VIDEO"
,
3
:
"CONTAINER"
,
}
MotionMediaRole_value
=
map
[
string
]
int32
{
"MOTION_MEDIA_ROLE_UNSPECIFIED"
:
0
,
"STILL"
:
1
,
"VIDEO"
:
2
,
"CONTAINER"
:
3
,
}
)
func
(
x
MotionMediaRole
)
Enum
()
*
MotionMediaRole
{
p
:=
new
(
MotionMediaRole
)
*
p
=
x
return
p
}
func
(
x
MotionMediaRole
)
String
()
string
{
return
protoimpl
.
X
.
EnumStringOf
(
x
.
Descriptor
(),
protoreflect
.
EnumNumber
(
x
))
}
func
(
MotionMediaRole
)
Descriptor
()
protoreflect
.
EnumDescriptor
{
return
file_store_attachment_proto_enumTypes
[
2
]
.
Descriptor
()
}
func
(
MotionMediaRole
)
Type
()
protoreflect
.
EnumType
{
return
&
file_store_attachment_proto_enumTypes
[
2
]
}
func
(
x
MotionMediaRole
)
Number
()
protoreflect
.
EnumNumber
{
return
protoreflect
.
EnumNumber
(
x
)
}
// Deprecated: Use MotionMediaRole.Descriptor instead.
func
(
MotionMediaRole
)
EnumDescriptor
()
([]
byte
,
[]
int
)
{
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
2
}
}
type
MotionMedia
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
Family
MotionMediaFamily
`protobuf:"varint,1,opt,name=family,proto3,enum=memos.store.MotionMediaFamily" json:"family,omitempty"`
Role
MotionMediaRole
`protobuf:"varint,2,opt,name=role,proto3,enum=memos.store.MotionMediaRole" json:"role,omitempty"`
GroupId
string
`protobuf:"bytes,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
PresentationTimestampUs
int64
`protobuf:"varint,4,opt,name=presentation_timestamp_us,json=presentationTimestampUs,proto3" json:"presentation_timestamp_us,omitempty"`
HasEmbeddedVideo
bool
`protobuf:"varint,5,opt,name=has_embedded_video,json=hasEmbeddedVideo,proto3" json:"has_embedded_video,omitempty"`
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
}
func
(
x
*
MotionMedia
)
Reset
()
{
*
x
=
MotionMedia
{}
mi
:=
&
file_store_attachment_proto_msgTypes
[
0
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
}
func
(
x
*
MotionMedia
)
String
()
string
{
return
protoimpl
.
X
.
MessageStringOf
(
x
)
}
func
(
*
MotionMedia
)
ProtoMessage
()
{}
func
(
x
*
MotionMedia
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_store_attachment_proto_msgTypes
[
0
]
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
ms
.
StoreMessageInfo
(
mi
)
}
return
ms
}
return
mi
.
MessageOf
(
x
)
}
// Deprecated: Use MotionMedia.ProtoReflect.Descriptor instead.
func
(
*
MotionMedia
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
0
}
}
func
(
x
*
MotionMedia
)
GetFamily
()
MotionMediaFamily
{
if
x
!=
nil
{
return
x
.
Family
}
return
MotionMediaFamily_MOTION_MEDIA_FAMILY_UNSPECIFIED
}
func
(
x
*
MotionMedia
)
GetRole
()
MotionMediaRole
{
if
x
!=
nil
{
return
x
.
Role
}
return
MotionMediaRole_MOTION_MEDIA_ROLE_UNSPECIFIED
}
func
(
x
*
MotionMedia
)
GetGroupId
()
string
{
if
x
!=
nil
{
return
x
.
GroupId
}
return
""
}
func
(
x
*
MotionMedia
)
GetPresentationTimestampUs
()
int64
{
if
x
!=
nil
{
return
x
.
PresentationTimestampUs
}
return
0
}
func
(
x
*
MotionMedia
)
GetHasEmbeddedVideo
()
bool
{
if
x
!=
nil
{
return
x
.
HasEmbeddedVideo
}
return
false
}
type
AttachmentPayload
struct
{
type
AttachmentPayload
struct
{
state
protoimpl
.
MessageState
`protogen:"open.v1"`
state
protoimpl
.
MessageState
`protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
// Types that are valid to be assigned to Payload:
//
//
// *AttachmentPayload_S3Object_
// *AttachmentPayload_S3Object_
Payload
isAttachmentPayload_Payload
`protobuf_oneof:"payload"`
Payload
isAttachmentPayload_Payload
`protobuf_oneof:"payload"`
MotionMedia
*
MotionMedia
`protobuf:"bytes,10,opt,name=motion_media,json=motionMedia,proto3" json:"motion_media,omitempty"`
unknownFields
protoimpl
.
UnknownFields
unknownFields
protoimpl
.
UnknownFields
sizeCache
protoimpl
.
SizeCache
sizeCache
protoimpl
.
SizeCache
}
}
func
(
x
*
AttachmentPayload
)
Reset
()
{
func
(
x
*
AttachmentPayload
)
Reset
()
{
*
x
=
AttachmentPayload
{}
*
x
=
AttachmentPayload
{}
mi
:=
&
file_store_attachment_proto_msgTypes
[
0
]
mi
:=
&
file_store_attachment_proto_msgTypes
[
1
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -101,7 +279,7 @@ func (x *AttachmentPayload) String() string {
...
@@ -101,7 +279,7 @@ func (x *AttachmentPayload) String() string {
func
(
*
AttachmentPayload
)
ProtoMessage
()
{}
func
(
*
AttachmentPayload
)
ProtoMessage
()
{}
func
(
x
*
AttachmentPayload
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
AttachmentPayload
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_store_attachment_proto_msgTypes
[
0
]
mi
:=
&
file_store_attachment_proto_msgTypes
[
1
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -114,7 +292,7 @@ func (x *AttachmentPayload) ProtoReflect() protoreflect.Message {
...
@@ -114,7 +292,7 @@ func (x *AttachmentPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use AttachmentPayload.ProtoReflect.Descriptor instead.
// Deprecated: Use AttachmentPayload.ProtoReflect.Descriptor instead.
func
(
*
AttachmentPayload
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
AttachmentPayload
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
0
}
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
1
}
}
}
func
(
x
*
AttachmentPayload
)
GetPayload
()
isAttachmentPayload_Payload
{
func
(
x
*
AttachmentPayload
)
GetPayload
()
isAttachmentPayload_Payload
{
...
@@ -133,6 +311,13 @@ func (x *AttachmentPayload) GetS3Object() *AttachmentPayload_S3Object {
...
@@ -133,6 +311,13 @@ func (x *AttachmentPayload) GetS3Object() *AttachmentPayload_S3Object {
return
nil
return
nil
}
}
func
(
x
*
AttachmentPayload
)
GetMotionMedia
()
*
MotionMedia
{
if
x
!=
nil
{
return
x
.
MotionMedia
}
return
nil
}
type
isAttachmentPayload_Payload
interface
{
type
isAttachmentPayload_Payload
interface
{
isAttachmentPayload_Payload
()
isAttachmentPayload_Payload
()
}
}
...
@@ -157,7 +342,7 @@ type AttachmentPayload_S3Object struct {
...
@@ -157,7 +342,7 @@ type AttachmentPayload_S3Object struct {
func
(
x
*
AttachmentPayload_S3Object
)
Reset
()
{
func
(
x
*
AttachmentPayload_S3Object
)
Reset
()
{
*
x
=
AttachmentPayload_S3Object
{}
*
x
=
AttachmentPayload_S3Object
{}
mi
:=
&
file_store_attachment_proto_msgTypes
[
1
]
mi
:=
&
file_store_attachment_proto_msgTypes
[
2
]
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
.
StoreMessageInfo
(
mi
)
ms
.
StoreMessageInfo
(
mi
)
}
}
...
@@ -169,7 +354,7 @@ func (x *AttachmentPayload_S3Object) String() string {
...
@@ -169,7 +354,7 @@ func (x *AttachmentPayload_S3Object) String() string {
func
(
*
AttachmentPayload_S3Object
)
ProtoMessage
()
{}
func
(
*
AttachmentPayload_S3Object
)
ProtoMessage
()
{}
func
(
x
*
AttachmentPayload_S3Object
)
ProtoReflect
()
protoreflect
.
Message
{
func
(
x
*
AttachmentPayload_S3Object
)
ProtoReflect
()
protoreflect
.
Message
{
mi
:=
&
file_store_attachment_proto_msgTypes
[
1
]
mi
:=
&
file_store_attachment_proto_msgTypes
[
2
]
if
x
!=
nil
{
if
x
!=
nil
{
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
ms
:=
protoimpl
.
X
.
MessageStateOf
(
protoimpl
.
Pointer
(
x
))
if
ms
.
LoadMessageInfo
()
==
nil
{
if
ms
.
LoadMessageInfo
()
==
nil
{
...
@@ -182,7 +367,7 @@ func (x *AttachmentPayload_S3Object) ProtoReflect() protoreflect.Message {
...
@@ -182,7 +367,7 @@ func (x *AttachmentPayload_S3Object) ProtoReflect() protoreflect.Message {
// Deprecated: Use AttachmentPayload_S3Object.ProtoReflect.Descriptor instead.
// Deprecated: Use AttachmentPayload_S3Object.ProtoReflect.Descriptor instead.
func
(
*
AttachmentPayload_S3Object
)
Descriptor
()
([]
byte
,
[]
int
)
{
func
(
*
AttachmentPayload_S3Object
)
Descriptor
()
([]
byte
,
[]
int
)
{
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
0
,
0
}
return
file_store_attachment_proto_rawDescGZIP
(),
[]
int
{
1
,
0
}
}
}
func
(
x
*
AttachmentPayload_S3Object
)
GetS3Config
()
*
StorageS3Config
{
func
(
x
*
AttachmentPayload_S3Object
)
GetS3Config
()
*
StorageS3Config
{
...
@@ -210,9 +395,17 @@ var File_store_attachment_proto protoreflect.FileDescriptor
...
@@ -210,9 +395,17 @@ var File_store_attachment_proto protoreflect.FileDescriptor
const
file_store_attachment_proto_rawDesc
=
""
+
const
file_store_attachment_proto_rawDesc
=
""
+
"
\n
"
+
"
\n
"
+
"
\x16
store/attachment.proto
\x12\v
memos.store
\x1a\x1f
google/protobuf/timestamp.proto
\x1a\x1c
store/instance_setting.proto
\"\x8c\x02\n
"
+
"
\x16
store/attachment.proto
\x12\v
memos.store
\x1a\x1f
google/protobuf/timestamp.proto
\x1a\x1c
store/instance_setting.proto
\"\xfc\x01\n
"
+
"
\v
MotionMedia
\x12
6
\n
"
+
"
\x06
family
\x18\x01
\x01
(
\x0e
2
\x1e
.memos.store.MotionMediaFamilyR
\x06
family
\x12
0
\n
"
+
"
\x04
role
\x18\x02
\x01
(
\x0e
2
\x1c
.memos.store.MotionMediaRoleR
\x04
role
\x12\x19\n
"
+
"
\b
group_id
\x18\x03
\x01
(
\t
R
\a
groupId
\x12
:
\n
"
+
"
\x19
presentation_timestamp_us
\x18\x04
\x01
(
\x03
R
\x17
presentationTimestampUs
\x12
,
\n
"
+
"
\x12
has_embedded_video
\x18\x05
\x01
(
\b
R
\x10
hasEmbeddedVideo
\"\xc9\x02\n
"
+
"
\x11
AttachmentPayload
\x12
F
\n
"
+
"
\x11
AttachmentPayload
\x12
F
\n
"
+
"
\t
s3_object
\x18\x01
\x01
(
\v
2'.memos.store.AttachmentPayload.S3ObjectH
\x00
R
\b
s3Object
\x1a\xa3\x01\n
"
+
"
\t
s3_object
\x18\x01
\x01
(
\v
2'.memos.store.AttachmentPayload.S3ObjectH
\x00
R
\b
s3Object
\x12
;
\n
"
+
"
\f
motion_media
\x18\n
"
+
"
\x01
(
\v
2
\x18
.memos.store.MotionMediaR
\v
motionMedia
\x1a\xa3\x01\n
"
+
"
\b
S3Object
\x12
9
\n
"
+
"
\b
S3Object
\x12
9
\n
"
+
"
\t
s3_config
\x18\x01
\x01
(
\v
2
\x1c
.memos.store.StorageS3ConfigR
\b
s3Config
\x12\x10\n
"
+
"
\t
s3_config
\x18\x01
\x01
(
\v
2
\x1c
.memos.store.StorageS3ConfigR
\b
s3Config
\x12\x10\n
"
+
"
\x03
key
\x18\x02
\x01
(
\t
R
\x03
key
\x12
J
\n
"
+
"
\x03
key
\x18\x02
\x01
(
\t
R
\x03
key
\x12
J
\n
"
+
...
@@ -222,7 +415,16 @@ const file_store_attachment_proto_rawDesc = "" +
...
@@ -222,7 +415,16 @@ const file_store_attachment_proto_rawDesc = "" +
"#ATTACHMENT_STORAGE_TYPE_UNSPECIFIED
\x10\x00\x12\t\n
"
+
"#ATTACHMENT_STORAGE_TYPE_UNSPECIFIED
\x10\x00\x12\t\n
"
+
"
\x05
LOCAL
\x10\x01\x12\x06\n
"
+
"
\x05
LOCAL
\x10\x01\x12\x06\n
"
+
"
\x02
S3
\x10\x02\x12\f\n
"
+
"
\x02
S3
\x10\x02\x12\f\n
"
+
"
\b
EXTERNAL
\x10\x03
B
\x9a\x01\n
"
+
"
\b
EXTERNAL
\x10\x03
*h
\n
"
+
"
\x11
MotionMediaFamily
\x12
#
\n
"
+
"
\x1f
MOTION_MEDIA_FAMILY_UNSPECIFIED
\x10\x00\x12\x14\n
"
+
"
\x10
APPLE_LIVE_PHOTO
\x10\x01\x12\x18\n
"
+
"
\x14
ANDROID_MOTION_PHOTO
\x10\x02
*Y
\n
"
+
"
\x0f
MotionMediaRole
\x12
!
\n
"
+
"
\x1d
MOTION_MEDIA_ROLE_UNSPECIFIED
\x10\x00\x12\t\n
"
+
"
\x05
STILL
\x10\x01\x12\t\n
"
+
"
\x05
VIDEO
\x10\x02\x12\r\n
"
+
"
\t
CONTAINER
\x10\x03
B
\x9a\x01\n
"
+
"
\x0f
com.memos.storeB
\x0f
AttachmentProtoP
\x01
Z)github.com/usememos/memos/proto/gen/store
\xa2\x02\x03
MSX
\xaa\x02\v
Memos.Store
\xca\x02\v
Memos
\\
Store
\xe2\x02\x17
Memos
\\
Store
\\
GPBMetadata
\xea\x02\f
Memos::Storeb
\x06
proto3"
"
\x0f
com.memos.storeB
\x0f
AttachmentProtoP
\x01
Z)github.com/usememos/memos/proto/gen/store
\xa2\x02\x03
MSX
\xaa\x02\v
Memos.Store
\xca\x02\v
Memos
\\
Store
\xe2\x02\x17
Memos
\\
Store
\\
GPBMetadata
\xea\x02\f
Memos::Storeb
\x06
proto3"
var
(
var
(
...
@@ -237,24 +439,30 @@ func file_store_attachment_proto_rawDescGZIP() []byte {
...
@@ -237,24 +439,30 @@ func file_store_attachment_proto_rawDescGZIP() []byte {
return
file_store_attachment_proto_rawDescData
return
file_store_attachment_proto_rawDescData
}
}
var
file_store_attachment_proto_enumTypes
=
make
([]
protoimpl
.
EnumInfo
,
1
)
var
file_store_attachment_proto_enumTypes
=
make
([]
protoimpl
.
EnumInfo
,
3
)
var
file_store_attachment_proto_msgTypes
=
make
([]
protoimpl
.
MessageInfo
,
2
)
var
file_store_attachment_proto_msgTypes
=
make
([]
protoimpl
.
MessageInfo
,
3
)
var
file_store_attachment_proto_goTypes
=
[]
any
{
var
file_store_attachment_proto_goTypes
=
[]
any
{
(
AttachmentStorageType
)(
0
),
// 0: memos.store.AttachmentStorageType
(
AttachmentStorageType
)(
0
),
// 0: memos.store.AttachmentStorageType
(
*
AttachmentPayload
)(
nil
),
// 1: memos.store.AttachmentPayload
(
MotionMediaFamily
)(
0
),
// 1: memos.store.MotionMediaFamily
(
*
AttachmentPayload_S3Object
)(
nil
),
// 2: memos.store.AttachmentPayload.S3Object
(
MotionMediaRole
)(
0
),
// 2: memos.store.MotionMediaRole
(
*
StorageS3Config
)(
nil
),
// 3: memos.store.StorageS3Config
(
*
MotionMedia
)(
nil
),
// 3: memos.store.MotionMedia
(
*
timestamppb
.
Timestamp
)(
nil
),
// 4: google.protobuf.Timestamp
(
*
AttachmentPayload
)(
nil
),
// 4: memos.store.AttachmentPayload
(
*
AttachmentPayload_S3Object
)(
nil
),
// 5: memos.store.AttachmentPayload.S3Object
(
*
StorageS3Config
)(
nil
),
// 6: memos.store.StorageS3Config
(
*
timestamppb
.
Timestamp
)(
nil
),
// 7: google.protobuf.Timestamp
}
}
var
file_store_attachment_proto_depIdxs
=
[]
int32
{
var
file_store_attachment_proto_depIdxs
=
[]
int32
{
2
,
// 0: memos.store.AttachmentPayload.s3_object:type_name -> memos.store.AttachmentPayload.S3Object
1
,
// 0: memos.store.MotionMedia.family:type_name -> memos.store.MotionMediaFamily
3
,
// 1: memos.store.AttachmentPayload.S3Object.s3_config:type_name -> memos.store.StorageS3Config
2
,
// 1: memos.store.MotionMedia.role:type_name -> memos.store.MotionMediaRole
4
,
// 2: memos.store.AttachmentPayload.S3Object.last_presigned_time:type_name -> google.protobuf.Timestamp
5
,
// 2: memos.store.AttachmentPayload.s3_object:type_name -> memos.store.AttachmentPayload.S3Object
3
,
// [3:3] is the sub-list for method output_type
3
,
// 3: memos.store.AttachmentPayload.motion_media:type_name -> memos.store.MotionMedia
3
,
// [3:3] is the sub-list for method input_type
6
,
// 4: memos.store.AttachmentPayload.S3Object.s3_config:type_name -> memos.store.StorageS3Config
3
,
// [3:3] is the sub-list for extension type_name
7
,
// 5: memos.store.AttachmentPayload.S3Object.last_presigned_time:type_name -> google.protobuf.Timestamp
3
,
// [3:3] is the sub-list for extension extendee
6
,
// [6:6] is the sub-list for method output_type
0
,
// [0:3] is the sub-list for field type_name
6
,
// [6:6] is the sub-list for method input_type
6
,
// [6:6] is the sub-list for extension type_name
6
,
// [6:6] is the sub-list for extension extendee
0
,
// [0:6] is the sub-list for field type_name
}
}
func
init
()
{
file_store_attachment_proto_init
()
}
func
init
()
{
file_store_attachment_proto_init
()
}
...
@@ -263,7 +471,7 @@ func file_store_attachment_proto_init() {
...
@@ -263,7 +471,7 @@ func file_store_attachment_proto_init() {
return
return
}
}
file_store_instance_setting_proto_init
()
file_store_instance_setting_proto_init
()
file_store_attachment_proto_msgTypes
[
0
]
.
OneofWrappers
=
[]
any
{
file_store_attachment_proto_msgTypes
[
1
]
.
OneofWrappers
=
[]
any
{
(
*
AttachmentPayload_S3Object_
)(
nil
),
(
*
AttachmentPayload_S3Object_
)(
nil
),
}
}
type
x
struct
{}
type
x
struct
{}
...
@@ -271,8 +479,8 @@ func file_store_attachment_proto_init() {
...
@@ -271,8 +479,8 @@ func file_store_attachment_proto_init() {
File
:
protoimpl
.
DescBuilder
{
File
:
protoimpl
.
DescBuilder
{
GoPackagePath
:
reflect
.
TypeOf
(
x
{})
.
PkgPath
(),
GoPackagePath
:
reflect
.
TypeOf
(
x
{})
.
PkgPath
(),
RawDescriptor
:
unsafe
.
Slice
(
unsafe
.
StringData
(
file_store_attachment_proto_rawDesc
),
len
(
file_store_attachment_proto_rawDesc
)),
RawDescriptor
:
unsafe
.
Slice
(
unsafe
.
StringData
(
file_store_attachment_proto_rawDesc
),
len
(
file_store_attachment_proto_rawDesc
)),
NumEnums
:
1
,
NumEnums
:
3
,
NumMessages
:
2
,
NumMessages
:
3
,
NumExtensions
:
0
,
NumExtensions
:
0
,
NumServices
:
0
,
NumServices
:
0
,
},
},
...
...
proto/store/attachment.proto
View file @
4b4e7194
...
@@ -17,11 +17,34 @@ enum AttachmentStorageType {
...
@@ -17,11 +17,34 @@ enum AttachmentStorageType {
EXTERNAL
=
3
;
EXTERNAL
=
3
;
}
}
enum
MotionMediaFamily
{
MOTION_MEDIA_FAMILY_UNSPECIFIED
=
0
;
APPLE_LIVE_PHOTO
=
1
;
ANDROID_MOTION_PHOTO
=
2
;
}
enum
MotionMediaRole
{
MOTION_MEDIA_ROLE_UNSPECIFIED
=
0
;
STILL
=
1
;
VIDEO
=
2
;
CONTAINER
=
3
;
}
message
MotionMedia
{
MotionMediaFamily
family
=
1
;
MotionMediaRole
role
=
2
;
string
group_id
=
3
;
int64
presentation_timestamp_us
=
4
;
bool
has_embedded_video
=
5
;
}
message
AttachmentPayload
{
message
AttachmentPayload
{
oneof
payload
{
oneof
payload
{
S3Object
s3_object
=
1
;
S3Object
s3_object
=
1
;
}
}
MotionMedia
motion_media
=
10
;
message
S3Object
{
message
S3Object
{
StorageS3Config
s3_config
=
1
;
StorageS3Config
s3_config
=
1
;
// key is the S3 object key.
// key is the S3 object key.
...
...
server/router/api/v1/attachment_motion.go
0 → 100644
View file @
4b4e7194
package
v1
import
(
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
storepb
"github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func
convertMotionMediaFromStore
(
motion
*
storepb
.
MotionMedia
)
*
v1pb
.
MotionMedia
{
if
motion
==
nil
{
return
nil
}
return
&
v1pb
.
MotionMedia
{
Family
:
v1pb
.
MotionMediaFamily
(
motion
.
Family
),
Role
:
v1pb
.
MotionMediaRole
(
motion
.
Role
),
GroupId
:
motion
.
GroupId
,
PresentationTimestampUs
:
motion
.
PresentationTimestampUs
,
HasEmbeddedVideo
:
motion
.
HasEmbeddedVideo
,
}
}
func
convertMotionMediaToStore
(
motion
*
v1pb
.
MotionMedia
)
*
storepb
.
MotionMedia
{
if
motion
==
nil
{
return
nil
}
return
&
storepb
.
MotionMedia
{
Family
:
storepb
.
MotionMediaFamily
(
motion
.
Family
),
Role
:
storepb
.
MotionMediaRole
(
motion
.
Role
),
GroupId
:
motion
.
GroupId
,
PresentationTimestampUs
:
motion
.
PresentationTimestampUs
,
HasEmbeddedVideo
:
motion
.
HasEmbeddedVideo
,
}
}
func
getAttachmentMotionMedia
(
attachment
*
store
.
Attachment
)
*
storepb
.
MotionMedia
{
if
attachment
==
nil
||
attachment
.
Payload
==
nil
{
return
nil
}
return
attachment
.
Payload
.
MotionMedia
}
func
isAndroidMotionContainer
(
motion
*
storepb
.
MotionMedia
)
bool
{
return
motion
!=
nil
&&
motion
.
Family
==
storepb
.
MotionMediaFamily_ANDROID_MOTION_PHOTO
&&
motion
.
Role
==
storepb
.
MotionMediaRole_CONTAINER
&&
motion
.
HasEmbeddedVideo
}
func
ensureAttachmentPayload
(
payload
*
storepb
.
AttachmentPayload
)
*
storepb
.
AttachmentPayload
{
if
payload
!=
nil
{
return
payload
}
return
&
storepb
.
AttachmentPayload
{}
}
func
isMultiMemberMotionGroup
(
attachments
[]
*
store
.
Attachment
)
bool
{
if
len
(
attachments
)
<
2
{
return
false
}
for
_
,
attachment
:=
range
attachments
{
motion
:=
getAttachmentMotionMedia
(
attachment
)
if
motion
==
nil
||
motion
.
GroupId
==
""
{
return
false
}
}
return
true
}
server/router/api/v1/attachment_service.go
View file @
4b4e7194
...
@@ -22,6 +22,7 @@ import (
...
@@ -22,6 +22,7 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/internal/motionphoto"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/filter"
"github.com/usememos/memos/plugin/filter"
...
@@ -43,6 +44,7 @@ const (
...
@@ -43,6 +44,7 @@ const (
// defaultJPEGQuality is the JPEG quality used when re-encoding images for EXIF stripping.
// defaultJPEGQuality is the JPEG quality used when re-encoding images for EXIF stripping.
// Quality 95 maintains visual quality while ensuring metadata is removed.
// Quality 95 maintains visual quality while ensuring metadata is removed.
defaultJPEGQuality
=
95
defaultJPEGQuality
=
95
maxBatchDeleteAttachments
=
100
)
)
var
SupportedThumbnailMimeTypes
=
[]
string
{
var
SupportedThumbnailMimeTypes
=
[]
string
{
...
@@ -111,6 +113,15 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
...
@@ -111,6 +113,15 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
Type
:
request
.
Attachment
.
Type
,
Type
:
request
.
Attachment
.
Type
,
}
}
inputMotionMedia
,
err
:=
validateClientMotionMedia
(
request
.
Attachment
.
MotionMedia
,
attachmentUID
)
if
err
!=
nil
{
return
nil
,
err
}
if
inputMotionMedia
!=
nil
{
create
.
Payload
=
ensureAttachmentPayload
(
create
.
Payload
)
create
.
Payload
.
MotionMedia
=
inputMotionMedia
}
instanceStorageSetting
,
err
:=
s
.
Store
.
GetInstanceStorageSetting
(
ctx
)
instanceStorageSetting
,
err
:=
s
.
Store
.
GetInstanceStorageSetting
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get instance storage setting: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get instance storage setting: %v"
,
err
)
...
@@ -126,9 +137,16 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
...
@@ -126,9 +137,16 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
create
.
Size
=
int64
(
size
)
create
.
Size
=
int64
(
size
)
create
.
Blob
=
request
.
Attachment
.
Content
create
.
Blob
=
request
.
Attachment
.
Content
if
create
.
Payload
==
nil
||
create
.
Payload
.
MotionMedia
==
nil
{
if
detectedMotion
:=
detectAndroidMotionMedia
(
create
.
Blob
,
create
.
Type
,
attachmentUID
);
detectedMotion
!=
nil
{
create
.
Payload
=
ensureAttachmentPayload
(
create
.
Payload
)
create
.
Payload
.
MotionMedia
=
detectedMotion
}
}
// Strip EXIF metadata from images for privacy protection.
// Strip EXIF metadata from images for privacy protection.
// This removes sensitive information like GPS location, device details, etc.
// This removes sensitive information like GPS location, device details, etc.
if
shouldStripExif
(
create
.
Type
)
{
if
shouldStripExif
(
create
.
Type
)
&&
!
isAndroidMotionContainer
(
create
.
Payload
.
GetMotionMedia
())
{
if
strippedBlob
,
err
:=
stripImageExif
(
create
.
Blob
,
create
.
Type
);
err
!=
nil
{
if
strippedBlob
,
err
:=
stripImageExif
(
create
.
Blob
,
create
.
Type
);
err
!=
nil
{
// Log warning but continue with original image to ensure uploads don't fail.
// Log warning but continue with original image to ensure uploads don't fail.
slog
.
Warn
(
"failed to strip EXIF metadata from image"
,
slog
.
Warn
(
"failed to strip EXIF metadata from image"
,
...
@@ -333,6 +351,56 @@ func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.Delet
...
@@ -333,6 +351,56 @@ func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.Delet
return
&
emptypb
.
Empty
{},
nil
return
&
emptypb
.
Empty
{},
nil
}
}
func
(
s
*
APIV1Service
)
BatchDeleteAttachments
(
ctx
context
.
Context
,
request
*
v1pb
.
BatchDeleteAttachmentsRequest
)
(
*
emptypb
.
Empty
,
error
)
{
user
,
err
:=
s
.
fetchCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
}
if
user
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"user not authenticated"
)
}
if
len
(
request
.
Names
)
==
0
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"attachment names are required"
)
}
if
len
(
request
.
Names
)
>
maxBatchDeleteAttachments
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"too many attachment names; max %d"
,
maxBatchDeleteAttachments
)
}
attachments
:=
make
([]
*
store
.
Attachment
,
0
,
len
(
request
.
Names
))
seen
:=
make
(
map
[
string
]
bool
,
len
(
request
.
Names
))
for
_
,
name
:=
range
request
.
Names
{
if
name
==
""
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"attachment name is required"
)
}
if
seen
[
name
]
{
continue
}
seen
[
name
]
=
true
attachmentUID
,
err
:=
ExtractAttachmentUIDFromName
(
name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid attachment id: %v"
,
err
)
}
attachment
,
err
:=
s
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
attachmentUID
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get attachment: %v"
,
err
)
}
if
attachment
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"attachment not found"
)
}
if
attachment
.
CreatorID
!=
user
.
ID
&&
!
isSuperUser
(
user
)
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
attachments
=
append
(
attachments
,
attachment
)
}
if
err
:=
s
.
Store
.
DeleteAttachments
(
ctx
,
attachments
);
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to delete attachments: %v"
,
err
)
}
return
&
emptypb
.
Empty
{},
nil
}
func
convertAttachmentFromStore
(
attachment
*
store
.
Attachment
)
*
v1pb
.
Attachment
{
func
convertAttachmentFromStore
(
attachment
*
store
.
Attachment
)
*
v1pb
.
Attachment
{
attachmentMessage
:=
&
v1pb
.
Attachment
{
attachmentMessage
:=
&
v1pb
.
Attachment
{
Name
:
fmt
.
Sprintf
(
"%s%s"
,
AttachmentNamePrefix
,
attachment
.
UID
),
Name
:
fmt
.
Sprintf
(
"%s%s"
,
AttachmentNamePrefix
,
attachment
.
UID
),
...
@@ -340,6 +408,7 @@ func convertAttachmentFromStore(attachment *store.Attachment) *v1pb.Attachment {
...
@@ -340,6 +408,7 @@ func convertAttachmentFromStore(attachment *store.Attachment) *v1pb.Attachment {
Filename
:
attachment
.
Filename
,
Filename
:
attachment
.
Filename
,
Type
:
attachment
.
Type
,
Type
:
attachment
.
Type
,
Size
:
attachment
.
Size
,
Size
:
attachment
.
Size
,
MotionMedia
:
convertMotionMediaFromStore
(
getAttachmentMotionMedia
(
attachment
)),
}
}
if
attachment
.
MemoUID
!=
nil
&&
*
attachment
.
MemoUID
!=
""
{
if
attachment
.
MemoUID
!=
nil
&&
*
attachment
.
MemoUID
!=
""
{
memoName
:=
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
*
attachment
.
MemoUID
)
memoName
:=
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
*
attachment
.
MemoUID
)
...
@@ -425,15 +494,15 @@ func SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *s
...
@@ -425,15 +494,15 @@ func SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *s
create
.
Reference
=
presignURL
create
.
Reference
=
presignURL
create
.
Blob
=
nil
create
.
Blob
=
nil
create
.
StorageType
=
storepb
.
AttachmentStorageType_S3
create
.
StorageType
=
storepb
.
AttachmentStorageType_S3
create
.
Payload
=
&
storepb
.
AttachmentPayload
{
payload
:=
ensureAttachmentPayload
(
create
.
Payload
)
Payload
:
&
storepb
.
AttachmentPayload_S3Object_
{
payload
.
Payload
=
&
storepb
.
AttachmentPayload_S3Object_
{
S3Object
:
&
storepb
.
AttachmentPayload_S3Object
{
S3Object
:
&
storepb
.
AttachmentPayload_S3Object
{
S3Config
:
s3Config
,
S3Config
:
s3Config
,
Key
:
key
,
Key
:
key
,
LastPresignedTime
:
timestamppb
.
New
(
time
.
Now
()),
LastPresignedTime
:
timestamppb
.
New
(
time
.
Now
()),
},
},
},
}
}
create
.
Payload
=
payload
}
}
return
nil
return
nil
...
@@ -624,6 +693,48 @@ func (s *APIV1Service) checkAttachmentAccess(ctx context.Context, attachment *st
...
@@ -624,6 +693,48 @@ func (s *APIV1Service) checkAttachmentAccess(ctx context.Context, attachment *st
return
nil
return
nil
}
}
func
validateClientMotionMedia
(
motion
*
v1pb
.
MotionMedia
,
attachmentUID
string
)
(
*
storepb
.
MotionMedia
,
error
)
{
if
motion
==
nil
{
return
nil
,
nil
}
if
motion
.
Family
!=
v1pb
.
MotionMediaFamily_APPLE_LIVE_PHOTO
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"only Apple Live Photo motion metadata can be provided by clients"
)
}
if
motion
.
Role
!=
v1pb
.
MotionMediaRole_STILL
&&
motion
.
Role
!=
v1pb
.
MotionMediaRole_VIDEO
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid Apple Live Photo motion role"
)
}
storeMotion
:=
convertMotionMediaToStore
(
motion
)
if
storeMotion
.
GroupId
==
""
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"motion media group_id is required"
)
}
if
storeMotion
.
Family
==
storepb
.
MotionMediaFamily_ANDROID_MOTION_PHOTO
&&
storeMotion
.
GroupId
==
""
{
storeMotion
.
GroupId
=
attachmentUID
}
return
storeMotion
,
nil
}
func
detectAndroidMotionMedia
(
blob
[]
byte
,
mimeType
,
attachmentUID
string
)
*
storepb
.
MotionMedia
{
if
mimeType
!=
"image/jpeg"
&&
mimeType
!=
"image/jpg"
{
return
nil
}
detection
:=
motionphoto
.
DetectJPEG
(
blob
)
if
detection
==
nil
{
return
nil
}
return
&
storepb
.
MotionMedia
{
Family
:
storepb
.
MotionMediaFamily_ANDROID_MOTION_PHOTO
,
Role
:
storepb
.
MotionMediaRole_CONTAINER
,
GroupId
:
attachmentUID
,
PresentationTimestampUs
:
detection
.
PresentationTimestampUs
,
HasEmbeddedVideo
:
true
,
}
}
// shouldStripExif checks if the MIME type is an image format that may contain EXIF metadata.
// shouldStripExif checks if the MIME type is an image format that may contain EXIF metadata.
// Returns true for formats like JPEG, TIFF, WebP, HEIC, and HEIF which commonly contain
// Returns true for formats like JPEG, TIFF, WebP, HEIC, and HEIF which commonly contain
// privacy-sensitive metadata such as GPS coordinates, camera settings, and device information.
// privacy-sensitive metadata such as GPS coordinates, camera settings, and device information.
...
...
server/router/api/v1/connect_services.go
View file @
4b4e7194
...
@@ -419,6 +419,14 @@ func (s *ConnectServiceHandler) DeleteAttachment(ctx context.Context, req *conne
...
@@ -419,6 +419,14 @@ func (s *ConnectServiceHandler) DeleteAttachment(ctx context.Context, req *conne
return
connect
.
NewResponse
(
resp
),
nil
return
connect
.
NewResponse
(
resp
),
nil
}
}
func
(
s
*
ConnectServiceHandler
)
BatchDeleteAttachments
(
ctx
context
.
Context
,
req
*
connect
.
Request
[
v1pb
.
BatchDeleteAttachmentsRequest
])
(
*
connect
.
Response
[
emptypb
.
Empty
],
error
)
{
resp
,
err
:=
s
.
APIV1Service
.
BatchDeleteAttachments
(
ctx
,
req
.
Msg
)
if
err
!=
nil
{
return
nil
,
convertGRPCError
(
err
)
}
return
connect
.
NewResponse
(
resp
),
nil
}
// ShortcutService
// ShortcutService
func
(
s
*
ConnectServiceHandler
)
ListShortcuts
(
ctx
context
.
Context
,
req
*
connect
.
Request
[
v1pb
.
ListShortcutsRequest
])
(
*
connect
.
Response
[
v1pb
.
ListShortcutsResponse
],
error
)
{
func
(
s
*
ConnectServiceHandler
)
ListShortcuts
(
ctx
context
.
Context
,
req
*
connect
.
Request
[
v1pb
.
ListShortcutsRequest
])
(
*
connect
.
Response
[
v1pb
.
ListShortcutsResponse
],
error
)
{
...
...
server/router/api/v1/memo_attachment_service.go
View file @
4b4e7194
...
@@ -51,27 +51,26 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set
...
@@ -51,27 +51,26 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set
}
}
func
(
s
*
APIV1Service
)
setMemoAttachmentsInternal
(
ctx
context
.
Context
,
memo
*
store
.
Memo
,
requestAttachments
[]
*
v1pb
.
Attachment
)
error
{
func
(
s
*
APIV1Service
)
setMemoAttachmentsInternal
(
ctx
context
.
Context
,
memo
*
store
.
Memo
,
requestAttachments
[]
*
v1pb
.
Attachment
)
error
{
a
ttachments
,
err
:=
s
.
Store
.
ListAttachments
(
ctx
,
&
store
.
FindAttachment
{
currentA
ttachments
,
err
:=
s
.
Store
.
ListAttachments
(
ctx
,
&
store
.
FindAttachment
{
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
return
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
return
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
}
}
// Delete attachments that are not in the request.
normalizedAttachments
,
err
:=
s
.
normalizeMemoAttachmentRequest
(
ctx
,
currentAttachments
,
requestAttachments
)
for
_
,
attachment
:=
range
attachments
{
found
:=
false
for
_
,
requestAttachment
:=
range
requestAttachments
{
requestAttachmentUID
,
err
:=
ExtractAttachmentUIDFromName
(
requestAttachment
.
Name
)
if
err
!=
nil
{
if
err
!=
nil
{
return
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid attachment name: %v"
,
err
)
return
err
}
if
attachment
.
UID
==
requestAttachmentUID
{
found
=
true
break
}
}
requestedIDs
:=
make
(
map
[
int32
]
bool
,
len
(
normalizedAttachments
))
for
_
,
attachment
:=
range
normalizedAttachments
{
requestedIDs
[
attachment
.
ID
]
=
true
}
}
if
!
found
{
// Delete attachments that are not in the request.
for
_
,
attachment
:=
range
currentAttachments
{
if
!
requestedIDs
[
attachment
.
ID
]
{
if
err
=
s
.
Store
.
DeleteAttachment
(
ctx
,
&
store
.
DeleteAttachment
{
if
err
=
s
.
Store
.
DeleteAttachment
(
ctx
,
&
store
.
DeleteAttachment
{
ID
:
int32
(
attachment
.
ID
),
ID
:
int32
(
attachment
.
ID
),
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
...
@@ -81,23 +80,12 @@ func (s *APIV1Service) setMemoAttachmentsInternal(ctx context.Context, memo *sto
...
@@ -81,23 +80,12 @@ func (s *APIV1Service) setMemoAttachmentsInternal(ctx context.Context, memo *sto
}
}
}
}
slices
.
Reverse
(
request
Attachments
)
slices
.
Reverse
(
normalized
Attachments
)
// Update attachments' memo_id in the request.
// Update attachments' memo_id in the request.
for
index
,
attachment
:=
range
requestAttachments
{
for
index
,
attachment
:=
range
normalizedAttachments
{
attachmentUID
,
err
:=
ExtractAttachmentUIDFromName
(
attachment
.
Name
)
if
err
!=
nil
{
return
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid attachment name: %v"
,
err
)
}
tempAttachment
,
err
:=
s
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
attachmentUID
})
if
err
!=
nil
{
return
status
.
Errorf
(
codes
.
Internal
,
"failed to get attachment: %v"
,
err
)
}
if
tempAttachment
==
nil
{
return
status
.
Errorf
(
codes
.
NotFound
,
"attachment not found: %s"
,
attachmentUID
)
}
updatedTs
:=
time
.
Now
()
.
Unix
()
+
int64
(
index
)
updatedTs
:=
time
.
Now
()
.
Unix
()
+
int64
(
index
)
if
err
:=
s
.
Store
.
UpdateAttachment
(
ctx
,
&
store
.
UpdateAttachment
{
if
err
:=
s
.
Store
.
UpdateAttachment
(
ctx
,
&
store
.
UpdateAttachment
{
ID
:
tempA
ttachment
.
ID
,
ID
:
a
ttachment
.
ID
,
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
UpdatedTs
:
&
updatedTs
,
UpdatedTs
:
&
updatedTs
,
});
err
!=
nil
{
});
err
!=
nil
{
...
@@ -108,6 +96,100 @@ func (s *APIV1Service) setMemoAttachmentsInternal(ctx context.Context, memo *sto
...
@@ -108,6 +96,100 @@ func (s *APIV1Service) setMemoAttachmentsInternal(ctx context.Context, memo *sto
return
nil
return
nil
}
}
func
(
s
*
APIV1Service
)
normalizeMemoAttachmentRequest
(
ctx
context
.
Context
,
currentAttachments
[]
*
store
.
Attachment
,
requestAttachments
[]
*
v1pb
.
Attachment
,
)
([]
*
store
.
Attachment
,
error
)
{
requestedAttachments
:=
make
([]
*
store
.
Attachment
,
0
,
len
(
requestAttachments
))
for
_
,
requestAttachment
:=
range
requestAttachments
{
attachmentUID
,
err
:=
ExtractAttachmentUIDFromName
(
requestAttachment
.
Name
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid attachment name: %v"
,
err
)
}
attachment
,
err
:=
s
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
attachmentUID
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get attachment: %v"
,
err
)
}
if
attachment
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"attachment not found: %s"
,
attachmentUID
)
}
requestedAttachments
=
append
(
requestedAttachments
,
attachment
)
}
currentGroups
:=
make
(
map
[
string
][]
*
store
.
Attachment
)
for
_
,
attachment
:=
range
currentAttachments
{
motion
:=
getAttachmentMotionMedia
(
attachment
)
if
motion
==
nil
||
motion
.
GroupId
==
""
{
continue
}
currentGroups
[
motion
.
GroupId
]
=
append
(
currentGroups
[
motion
.
GroupId
],
attachment
)
}
requestGroups
:=
make
(
map
[
string
][]
*
store
.
Attachment
)
requestNamesByGroup
:=
make
(
map
[
string
]
map
[
string
]
bool
)
for
_
,
attachment
:=
range
requestedAttachments
{
motion
:=
getAttachmentMotionMedia
(
attachment
)
if
motion
==
nil
||
motion
.
GroupId
==
""
{
continue
}
requestGroups
[
motion
.
GroupId
]
=
append
(
requestGroups
[
motion
.
GroupId
],
attachment
)
if
requestNamesByGroup
[
motion
.
GroupId
]
==
nil
{
requestNamesByGroup
[
motion
.
GroupId
]
=
make
(
map
[
string
]
bool
)
}
requestNamesByGroup
[
motion
.
GroupId
][
attachment
.
UID
]
=
true
}
normalized
:=
make
([]
*
store
.
Attachment
,
0
,
len
(
requestedAttachments
))
appendedGroups
:=
make
(
map
[
string
]
bool
)
appendedAttachments
:=
make
(
map
[
string
]
bool
)
for
_
,
attachment
:=
range
requestedAttachments
{
motion
:=
getAttachmentMotionMedia
(
attachment
)
if
motion
==
nil
||
motion
.
GroupId
==
""
{
if
!
appendedAttachments
[
attachment
.
UID
]
{
normalized
=
append
(
normalized
,
attachment
)
appendedAttachments
[
attachment
.
UID
]
=
true
}
continue
}
groupID
:=
motion
.
GroupId
if
appendedGroups
[
groupID
]
{
continue
}
currentGroup
:=
currentGroups
[
groupID
]
if
isMultiMemberMotionGroup
(
currentGroup
)
&&
!
allGroupMembersRequested
(
currentGroup
,
requestNamesByGroup
[
groupID
])
{
appendedGroups
[
groupID
]
=
true
continue
}
for
_
,
groupAttachment
:=
range
requestGroups
[
groupID
]
{
if
appendedAttachments
[
groupAttachment
.
UID
]
{
continue
}
normalized
=
append
(
normalized
,
groupAttachment
)
appendedAttachments
[
groupAttachment
.
UID
]
=
true
}
appendedGroups
[
groupID
]
=
true
}
return
normalized
,
nil
}
func
allGroupMembersRequested
(
group
[]
*
store
.
Attachment
,
requestedNames
map
[
string
]
bool
)
bool
{
if
len
(
group
)
==
0
{
return
false
}
for
_
,
attachment
:=
range
group
{
if
!
requestedNames
[
attachment
.
UID
]
{
return
false
}
}
return
true
}
func
(
s
*
APIV1Service
)
ListMemoAttachments
(
ctx
context
.
Context
,
request
*
v1pb
.
ListMemoAttachmentsRequest
)
(
*
v1pb
.
ListMemoAttachmentsResponse
,
error
)
{
func
(
s
*
APIV1Service
)
ListMemoAttachments
(
ctx
context
.
Context
,
request
*
v1pb
.
ListMemoAttachmentsRequest
)
(
*
v1pb
.
ListMemoAttachmentsResponse
,
error
)
{
memoUID
,
err
:=
ExtractMemoUIDFromName
(
request
.
Name
)
memoUID
,
err
:=
ExtractMemoUIDFromName
(
request
.
Name
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
server/router/api/v1/test/attachment_service_test.go
View file @
4b4e7194
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/internal/testutil"
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
storepb
"github.com/usememos/memos/proto/gen/store"
storepb
"github.com/usememos/memos/proto/gen/store"
apiv1
"github.com/usememos/memos/server/router/api/v1"
apiv1
"github.com/usememos/memos/server/router/api/v1"
...
@@ -112,3 +113,118 @@ func TestCreateAttachment(t *testing.T) {
...
@@ -112,3 +113,118 @@ func TestCreateAttachment(t *testing.T) {
require
.
Equal
(
t
,
[]
byte
(
"second-image"
),
secondBlob
)
require
.
Equal
(
t
,
[]
byte
(
"second-image"
),
secondBlob
)
})
})
}
}
func
TestCreateAttachmentMotionMedia
(
t
*
testing
.
T
)
{
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
ctx
:=
context
.
Background
()
user
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"motion_user"
)
require
.
NoError
(
t
,
err
)
userCtx
:=
ts
.
CreateUserContext
(
ctx
,
user
.
ID
)
t
.
Run
(
"Apple live photo metadata roundtrip"
,
func
(
t
*
testing
.
T
)
{
attachment
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
v1pb
.
CreateAttachmentRequest
{
Attachment
:
&
v1pb
.
Attachment
{
Filename
:
"live.heic"
,
Type
:
"image/heic"
,
Content
:
[]
byte
(
"fake-heic-still"
),
MotionMedia
:
&
v1pb
.
MotionMedia
{
Family
:
v1pb
.
MotionMediaFamily_APPLE_LIVE_PHOTO
,
Role
:
v1pb
.
MotionMediaRole_STILL
,
GroupId
:
"apple-group-1"
,
},
},
})
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
attachment
.
MotionMedia
)
require
.
Equal
(
t
,
v1pb
.
MotionMediaFamily_APPLE_LIVE_PHOTO
,
attachment
.
MotionMedia
.
Family
)
require
.
Equal
(
t
,
v1pb
.
MotionMediaRole_STILL
,
attachment
.
MotionMedia
.
Role
)
require
.
Equal
(
t
,
"apple-group-1"
,
attachment
.
MotionMedia
.
GroupId
)
})
t
.
Run
(
"Android motion photo detection"
,
func
(
t
*
testing
.
T
)
{
attachment
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
v1pb
.
CreateAttachmentRequest
{
Attachment
:
&
v1pb
.
Attachment
{
Filename
:
"motion.jpg"
,
Type
:
"image/jpeg"
,
Content
:
testutil
.
BuildMotionPhotoJPEG
(),
},
})
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
attachment
.
MotionMedia
)
require
.
Equal
(
t
,
v1pb
.
MotionMediaFamily_ANDROID_MOTION_PHOTO
,
attachment
.
MotionMedia
.
Family
)
require
.
Equal
(
t
,
v1pb
.
MotionMediaRole_CONTAINER
,
attachment
.
MotionMedia
.
Role
)
require
.
True
(
t
,
attachment
.
MotionMedia
.
HasEmbeddedVideo
)
})
}
func
TestBatchDeleteAttachments
(
t
*
testing
.
T
)
{
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
ctx
:=
context
.
Background
()
user
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"delete_user"
)
require
.
NoError
(
t
,
err
)
userCtx
:=
ts
.
CreateUserContext
(
ctx
,
user
.
ID
)
first
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
v1pb
.
CreateAttachmentRequest
{
Attachment
:
&
v1pb
.
Attachment
{
Filename
:
"one.txt"
,
Type
:
"text/plain"
,
Content
:
[]
byte
(
"one"
)},
})
require
.
NoError
(
t
,
err
)
second
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
v1pb
.
CreateAttachmentRequest
{
Attachment
:
&
v1pb
.
Attachment
{
Filename
:
"two.txt"
,
Type
:
"text/plain"
,
Content
:
[]
byte
(
"two"
)},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
BatchDeleteAttachments
(
userCtx
,
&
v1pb
.
BatchDeleteAttachmentsRequest
{
Names
:
[]
string
{
first
.
Name
,
second
.
Name
},
})
require
.
NoError
(
t
,
err
)
firstUID
,
err
:=
apiv1
.
ExtractAttachmentUIDFromName
(
first
.
Name
)
require
.
NoError
(
t
,
err
)
secondUID
,
err
:=
apiv1
.
ExtractAttachmentUIDFromName
(
second
.
Name
)
require
.
NoError
(
t
,
err
)
storedFirst
,
err
:=
ts
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
firstUID
})
require
.
NoError
(
t
,
err
)
storedSecond
,
err
:=
ts
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
secondUID
})
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedFirst
)
require
.
Nil
(
t
,
storedSecond
)
t
.
Run
(
"deduplicates duplicate names"
,
func
(
t
*
testing
.
T
)
{
third
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
v1pb
.
CreateAttachmentRequest
{
Attachment
:
&
v1pb
.
Attachment
{
Filename
:
"three.txt"
,
Type
:
"text/plain"
,
Content
:
[]
byte
(
"three"
)},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
BatchDeleteAttachments
(
userCtx
,
&
v1pb
.
BatchDeleteAttachmentsRequest
{
Names
:
[]
string
{
third
.
Name
,
third
.
Name
},
})
require
.
NoError
(
t
,
err
)
thirdUID
,
err
:=
apiv1
.
ExtractAttachmentUIDFromName
(
third
.
Name
)
require
.
NoError
(
t
,
err
)
storedThird
,
err
:=
ts
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
thirdUID
})
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedThird
)
})
t
.
Run
(
"rejects unauthorized deletes"
,
func
(
t
*
testing
.
T
)
{
ownerAttachment
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
v1pb
.
CreateAttachmentRequest
{
Attachment
:
&
v1pb
.
Attachment
{
Filename
:
"private.txt"
,
Type
:
"text/plain"
,
Content
:
[]
byte
(
"private"
)},
})
require
.
NoError
(
t
,
err
)
otherUser
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"other_delete_user"
)
require
.
NoError
(
t
,
err
)
otherCtx
:=
ts
.
CreateUserContext
(
ctx
,
otherUser
.
ID
)
_
,
err
=
ts
.
Service
.
BatchDeleteAttachments
(
otherCtx
,
&
v1pb
.
BatchDeleteAttachmentsRequest
{
Names
:
[]
string
{
ownerAttachment
.
Name
},
})
require
.
Error
(
t
,
err
)
require
.
Contains
(
t
,
err
.
Error
(),
"permission denied"
)
})
}
server/router/api/v1/test/memo_attachment_service_test.go
View file @
4b4e7194
...
@@ -163,4 +163,64 @@ func TestSetMemoAttachments(t *testing.T) {
...
@@ -163,4 +163,64 @@ func TestSetMemoAttachments(t *testing.T) {
require
.
Error
(
t
,
err
)
require
.
Error
(
t
,
err
)
require
.
Contains
(
t
,
err
.
Error
(),
"not found"
)
require
.
Contains
(
t
,
err
.
Error
(),
"not found"
)
})
})
t
.
Run
(
"SetMemoAttachments removes incomplete live photo groups"
,
func
(
t
*
testing
.
T
)
{
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
user
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"live_group_user"
)
require
.
NoError
(
t
,
err
)
userCtx
:=
ts
.
CreateUserContext
(
ctx
,
user
.
ID
)
still
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
apiv1
.
CreateAttachmentRequest
{
Attachment
:
&
apiv1
.
Attachment
{
Filename
:
"live.heic"
,
Type
:
"image/heic"
,
Content
:
[]
byte
(
"still"
),
MotionMedia
:
&
apiv1
.
MotionMedia
{
Family
:
apiv1
.
MotionMediaFamily_APPLE_LIVE_PHOTO
,
Role
:
apiv1
.
MotionMediaRole_STILL
,
GroupId
:
"memo-live-group"
,
},
},
})
require
.
NoError
(
t
,
err
)
video
,
err
:=
ts
.
Service
.
CreateAttachment
(
userCtx
,
&
apiv1
.
CreateAttachmentRequest
{
Attachment
:
&
apiv1
.
Attachment
{
Filename
:
"live.mov"
,
Type
:
"video/quicktime"
,
Content
:
[]
byte
(
"video"
),
MotionMedia
:
&
apiv1
.
MotionMedia
{
Family
:
apiv1
.
MotionMediaFamily_APPLE_LIVE_PHOTO
,
Role
:
apiv1
.
MotionMediaRole_VIDEO
,
GroupId
:
"memo-live-group"
,
},
},
})
require
.
NoError
(
t
,
err
)
memo
,
err
:=
ts
.
Service
.
CreateMemo
(
userCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"memo with live photo"
,
Visibility
:
apiv1
.
Visibility_PRIVATE
,
Attachments
:
[]
*
apiv1
.
Attachment
{
{
Name
:
still
.
Name
},
{
Name
:
video
.
Name
},
},
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
SetMemoAttachments
(
userCtx
,
&
apiv1
.
SetMemoAttachmentsRequest
{
Name
:
memo
.
Name
,
Attachments
:
[]
*
apiv1
.
Attachment
{
{
Name
:
still
.
Name
},
},
})
require
.
NoError
(
t
,
err
)
response
,
err
:=
ts
.
Service
.
ListMemoAttachments
(
userCtx
,
&
apiv1
.
ListMemoAttachmentsRequest
{
Name
:
memo
.
Name
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
response
.
Attachments
,
0
)
})
}
}
server/router/fileserver/fileserver.go
View file @
4b4e7194
...
@@ -19,6 +19,7 @@ import (
...
@@ -19,6 +19,7 @@ import (
"github.com/pkg/errors"
"github.com/pkg/errors"
"golang.org/x/sync/semaphore"
"golang.org/x/sync/semaphore"
"github.com/usememos/memos/internal/motionphoto"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/plugin/storage/s3"
"github.com/usememos/memos/plugin/storage/s3"
storepb
"github.com/usememos/memos/proto/gen/store"
storepb
"github.com/usememos/memos/proto/gen/store"
...
@@ -31,6 +32,9 @@ const (
...
@@ -31,6 +32,9 @@ const (
// ThumbnailCacheFolder is the folder name where thumbnail images are stored.
// ThumbnailCacheFolder is the folder name where thumbnail images are stored.
ThumbnailCacheFolder
=
".thumbnail_cache"
ThumbnailCacheFolder
=
".thumbnail_cache"
// MotionCacheFolder is the folder name where extracted motion clips are stored.
MotionCacheFolder
=
".motion_cache"
// thumbnailMaxSize is the maximum dimension (width or height) for thumbnails.
// thumbnailMaxSize is the maximum dimension (width or height) for thumbnails.
thumbnailMaxSize
=
600
thumbnailMaxSize
=
600
...
@@ -122,6 +126,7 @@ func (s *FileServerService) serveAttachmentFile(c *echo.Context) error {
...
@@ -122,6 +126,7 @@ func (s *FileServerService) serveAttachmentFile(c *echo.Context) error {
ctx
:=
c
.
Request
()
.
Context
()
ctx
:=
c
.
Request
()
.
Context
()
uid
:=
c
.
Param
(
"uid"
)
uid
:=
c
.
Param
(
"uid"
)
wantThumbnail
:=
c
.
QueryParam
(
"thumbnail"
)
==
"true"
wantThumbnail
:=
c
.
QueryParam
(
"thumbnail"
)
==
"true"
wantMotion
:=
c
.
QueryParam
(
"motion"
)
==
"true"
attachment
,
err
:=
s
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
attachment
,
err
:=
s
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
uid
,
UID
:
&
uid
,
...
@@ -138,6 +143,10 @@ func (s *FileServerService) serveAttachmentFile(c *echo.Context) error {
...
@@ -138,6 +143,10 @@ func (s *FileServerService) serveAttachmentFile(c *echo.Context) error {
return
err
return
err
}
}
if
wantMotion
{
return
s
.
serveMotionClip
(
c
,
attachment
)
}
contentType
:=
s
.
sanitizeContentType
(
attachment
.
Type
)
contentType
:=
s
.
sanitizeContentType
(
attachment
.
Type
)
// Stream video/audio to avoid loading entire file into memory.
// Stream video/audio to avoid loading entire file into memory.
...
@@ -226,6 +235,7 @@ func (s *FileServerService) serveStaticFile(c *echo.Context, attachment *store.A
...
@@ -226,6 +235,7 @@ func (s *FileServerService) serveStaticFile(c *echo.Context, attachment *store.A
slog
.
Warn
(
"failed to get thumbnail"
,
"error"
,
err
)
slog
.
Warn
(
"failed to get thumbnail"
,
"error"
,
err
)
}
else
{
}
else
{
blob
=
thumbnailBlob
blob
=
thumbnailBlob
contentType
=
"image/jpeg"
}
}
}
}
...
@@ -411,7 +421,7 @@ func (s *FileServerService) getThumbnailPath(attachment *store.Attachment) (stri
...
@@ -411,7 +421,7 @@ func (s *FileServerService) getThumbnailPath(attachment *store.Attachment) (stri
if
err
:=
os
.
MkdirAll
(
cacheFolder
,
os
.
ModePerm
);
err
!=
nil
{
if
err
:=
os
.
MkdirAll
(
cacheFolder
,
os
.
ModePerm
);
err
!=
nil
{
return
""
,
errors
.
Wrap
(
err
,
"failed to create thumbnail cache folder"
)
return
""
,
errors
.
Wrap
(
err
,
"failed to create thumbnail cache folder"
)
}
}
filename
:=
fmt
.
Sprintf
(
"%
d%s"
,
attachment
.
ID
,
filepath
.
Ext
(
attachment
.
Filename
)
)
filename
:=
fmt
.
Sprintf
(
"%
s.jpeg"
,
attachment
.
UID
)
return
filepath
.
Join
(
cacheFolder
,
filename
),
nil
return
filepath
.
Join
(
cacheFolder
,
filename
),
nil
}
}
...
@@ -443,7 +453,13 @@ func (s *FileServerService) generateThumbnail(attachment *store.Attachment, thum
...
@@ -443,7 +453,13 @@ func (s *FileServerService) generateThumbnail(attachment *store.Attachment, thum
thumbnailImage
:=
imaging
.
Resize
(
img
,
thumbnailWidth
,
thumbnailHeight
,
imaging
.
Lanczos
)
thumbnailImage
:=
imaging
.
Resize
(
img
,
thumbnailWidth
,
thumbnailHeight
,
imaging
.
Lanczos
)
if
err
:=
imaging
.
Save
(
thumbnailImage
,
thumbnailPath
);
err
!=
nil
{
output
,
err
:=
os
.
Create
(
thumbnailPath
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to create thumbnail file"
)
}
defer
output
.
Close
()
if
err
:=
imaging
.
Encode
(
output
,
thumbnailImage
,
imaging
.
JPEG
,
imaging
.
JPEGQuality
(
90
));
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to save thumbnail"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to save thumbnail"
)
}
}
...
@@ -463,6 +479,60 @@ func calculateThumbnailDimensions(width, height int) (int, int) {
...
@@ -463,6 +479,60 @@ func calculateThumbnailDimensions(width, height int) (int, int) {
return
0
,
thumbnailMaxSize
// Portrait: constrain height.
return
0
,
thumbnailMaxSize
// Portrait: constrain height.
}
}
func
(
s
*
FileServerService
)
serveMotionClip
(
c
*
echo
.
Context
,
attachment
*
store
.
Attachment
)
error
{
motionMedia
:=
attachment
.
Payload
.
GetMotionMedia
()
if
motionMedia
==
nil
||
motionMedia
.
Family
!=
storepb
.
MotionMediaFamily_ANDROID_MOTION_PHOTO
||
!
motionMedia
.
HasEmbeddedVideo
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"attachment does not have motion clip"
)
}
clipBlob
,
err
:=
s
.
getOrExtractMotionClip
(
c
.
Request
()
.
Context
(),
attachment
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"failed to get motion clip"
)
.
Wrap
(
err
)
}
setSecurityHeaders
(
c
)
setMediaHeaders
(
c
,
"video/mp4"
,
"video/mp4"
)
modTime
:=
time
.
Unix
(
attachment
.
UpdatedTs
,
0
)
http
.
ServeContent
(
c
.
Response
(),
c
.
Request
(),
attachment
.
UID
+
".mp4"
,
modTime
,
bytes
.
NewReader
(
clipBlob
))
return
nil
}
func
(
s
*
FileServerService
)
getOrExtractMotionClip
(
_
context
.
Context
,
attachment
*
store
.
Attachment
)
([]
byte
,
error
)
{
motionPath
,
err
:=
s
.
getMotionPath
(
attachment
)
if
err
!=
nil
{
return
nil
,
err
}
if
blob
,
err
:=
s
.
readCachedThumbnail
(
motionPath
);
err
==
nil
{
return
blob
,
nil
}
blob
,
err
:=
s
.
getAttachmentBlob
(
attachment
)
if
err
!=
nil
{
return
nil
,
err
}
videoBlob
,
_
:=
motionphoto
.
ExtractVideo
(
blob
)
if
len
(
videoBlob
)
==
0
{
return
nil
,
errors
.
New
(
"motion video not found"
)
}
if
err
:=
os
.
WriteFile
(
motionPath
,
videoBlob
,
0644
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to cache motion clip"
)
}
return
videoBlob
,
nil
}
func
(
s
*
FileServerService
)
getMotionPath
(
attachment
*
store
.
Attachment
)
(
string
,
error
)
{
cacheFolder
:=
filepath
.
Join
(
s
.
Profile
.
Data
,
MotionCacheFolder
)
if
err
:=
os
.
MkdirAll
(
cacheFolder
,
os
.
ModePerm
);
err
!=
nil
{
return
""
,
errors
.
Wrap
(
err
,
"failed to create motion cache folder"
)
}
return
filepath
.
Join
(
cacheFolder
,
attachment
.
UID
+
".mp4"
),
nil
}
// =============================================================================
// =============================================================================
// Authentication & Authorization
// Authentication & Authorization
// =============================================================================
// =============================================================================
...
...
server/router/fileserver/fileserver_test.go
View file @
4b4e7194
...
@@ -12,6 +12,7 @@ import (
...
@@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/internal/testutil"
"github.com/usememos/memos/plugin/markdown"
"github.com/usememos/memos/plugin/markdown"
apiv1
"github.com/usememos/memos/proto/gen/api/v1"
apiv1
"github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/server/auth"
"github.com/usememos/memos/server/auth"
...
@@ -139,6 +140,51 @@ func TestServeAttachmentFile_ShareTokenRejectsCommentAttachment(t *testing.T) {
...
@@ -139,6 +140,51 @@ func TestServeAttachmentFile_ShareTokenRejectsCommentAttachment(t *testing.T) {
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
rec
.
Code
)
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
rec
.
Code
)
}
}
func
TestServeAttachmentFile_MotionClip
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
svc
,
fs
,
_
,
cleanup
:=
newShareAttachmentTestServices
(
ctx
,
t
)
defer
cleanup
()
creator
,
err
:=
svc
.
Store
.
CreateUser
(
ctx
,
&
store
.
User
{
Username
:
"motion-owner"
,
Role
:
store
.
RoleUser
,
Email
:
"motion-owner@example.com"
,
})
require
.
NoError
(
t
,
err
)
creatorCtx
:=
context
.
WithValue
(
ctx
,
auth
.
UserIDContextKey
,
creator
.
ID
)
attachment
,
err
:=
svc
.
CreateAttachment
(
creatorCtx
,
&
apiv1
.
CreateAttachmentRequest
{
Attachment
:
&
apiv1
.
Attachment
{
Filename
:
"motion.jpg"
,
Type
:
"image/jpeg"
,
Content
:
testutil
.
BuildMotionPhotoJPEG
(),
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
svc
.
CreateMemo
(
creatorCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"motion memo"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
Attachments
:
[]
*
apiv1
.
Attachment
{
{
Name
:
attachment
.
Name
},
},
},
})
require
.
NoError
(
t
,
err
)
e
:=
echo
.
New
()
fs
.
RegisterRoutes
(
e
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
fmt
.
Sprintf
(
"/file/%s/%s?motion=true"
,
attachment
.
Name
,
attachment
.
Filename
),
nil
)
rec
:=
httptest
.
NewRecorder
()
e
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
"video/mp4"
,
rec
.
Header
()
.
Get
(
"Content-Type"
))
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"ftyp"
)
}
func
newShareAttachmentTestServices
(
ctx
context
.
Context
,
t
*
testing
.
T
)
(
*
apiv1service
.
APIV1Service
,
*
FileServerService
,
*
store
.
Store
,
func
())
{
func
newShareAttachmentTestServices
(
ctx
context
.
Context
,
t
*
testing
.
T
)
(
*
apiv1service
.
APIV1Service
,
*
FileServerService
,
*
store
.
Store
,
func
())
{
t
.
Helper
()
t
.
Helper
()
...
...
store/attachment.go
View file @
4b4e7194
...
@@ -71,6 +71,11 @@ type DeleteAttachment struct {
...
@@ -71,6 +71,11 @@ type DeleteAttachment struct {
MemoID
*
int32
MemoID
*
int32
}
}
const
(
thumbnailCacheFolder
=
".thumbnail_cache"
motionCacheFolder
=
".motion_cache"
)
func
(
s
*
Store
)
CreateAttachment
(
ctx
context
.
Context
,
create
*
Attachment
)
(
*
Attachment
,
error
)
{
func
(
s
*
Store
)
CreateAttachment
(
ctx
context
.
Context
,
create
*
Attachment
)
(
*
Attachment
,
error
)
{
if
!
base
.
UIDMatcher
.
MatchString
(
create
.
UID
)
{
if
!
base
.
UIDMatcher
.
MatchString
(
create
.
UID
)
{
return
nil
,
errors
.
New
(
"invalid uid"
)
return
nil
,
errors
.
New
(
"invalid uid"
)
...
@@ -123,6 +128,56 @@ func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment)
...
@@ -123,6 +128,56 @@ func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment)
return
errors
.
New
(
"attachment not found"
)
return
errors
.
New
(
"attachment not found"
)
}
}
if
err
:=
s
.
DeleteAttachmentStorage
(
ctx
,
attachment
);
err
!=
nil
{
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_LOCAL
{
return
errors
.
Wrap
(
err
,
"failed to delete local file"
)
}
slog
.
Warn
(
"Failed to delete attachment storage"
,
slog
.
Any
(
"err"
,
err
))
}
return
s
.
driver
.
DeleteAttachment
(
ctx
,
delete
)
}
func
(
s
*
Store
)
DeleteAttachments
(
ctx
context
.
Context
,
attachments
[]
*
Attachment
)
error
{
if
len
(
attachments
)
==
0
{
return
nil
}
deletes
:=
make
([]
*
DeleteAttachment
,
0
,
len
(
attachments
))
for
_
,
attachment
:=
range
attachments
{
if
attachment
==
nil
{
continue
}
deletes
=
append
(
deletes
,
&
DeleteAttachment
{
ID
:
attachment
.
ID
,
MemoID
:
attachment
.
MemoID
})
}
if
len
(
deletes
)
==
0
{
return
nil
}
if
err
:=
s
.
driver
.
DeleteAttachments
(
ctx
,
deletes
);
err
!=
nil
{
return
err
}
for
_
,
attachment
:=
range
attachments
{
if
attachment
==
nil
{
continue
}
if
err
:=
s
.
DeleteAttachmentStorage
(
ctx
,
attachment
);
err
!=
nil
{
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_LOCAL
{
return
errors
.
Wrap
(
err
,
"failed to delete local file"
)
}
slog
.
Warn
(
"Failed to delete attachment storage"
,
slog
.
Any
(
"err"
,
err
))
}
}
return
nil
}
func
(
s
*
Store
)
DeleteAttachmentStorage
(
ctx
context
.
Context
,
attachment
*
Attachment
)
error
{
if
attachment
==
nil
{
return
nil
}
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_LOCAL
{
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_LOCAL
{
if
err
:=
func
()
error
{
if
err
:=
func
()
error
{
p
:=
filepath
.
FromSlash
(
attachment
.
Reference
)
p
:=
filepath
.
FromSlash
(
attachment
.
Reference
)
...
@@ -135,7 +190,7 @@ func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment)
...
@@ -135,7 +190,7 @@ func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment)
}
}
return
nil
return
nil
}();
err
!=
nil
{
}();
err
!=
nil
{
return
err
ors
.
Wrap
(
err
,
"failed to delete local file"
)
return
err
}
}
}
else
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_S3
{
}
else
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_S3
{
if
err
:=
func
()
error
{
if
err
:=
func
()
error
{
...
@@ -164,9 +219,21 @@ func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment)
...
@@ -164,9 +219,21 @@ func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment)
}
}
return
nil
return
nil
}();
err
!=
nil
{
}();
err
!=
nil
{
slog
.
Warn
(
"Failed to delete s3 object"
,
slog
.
Any
(
"err"
,
err
))
return
err
}
}
}
}
return
s
.
driver
.
DeleteAttachment
(
ctx
,
delete
)
s
.
deleteAttachmentDerivedCaches
(
attachment
)
return
nil
}
func
(
s
*
Store
)
deleteAttachmentDerivedCaches
(
attachment
*
Attachment
)
{
for
_
,
cachePath
:=
range
[]
string
{
filepath
.
Join
(
s
.
profile
.
Data
,
thumbnailCacheFolder
,
attachment
.
UID
+
".jpeg"
),
filepath
.
Join
(
s
.
profile
.
Data
,
motionCacheFolder
,
attachment
.
UID
+
".mp4"
),
}
{
if
err
:=
os
.
Remove
(
cachePath
);
err
!=
nil
&&
!
os
.
IsNotExist
(
err
)
{
slog
.
Warn
(
"Failed to delete derived attachment cache"
,
slog
.
String
(
"path"
,
cachePath
),
slog
.
Any
(
"err"
,
err
))
}
}
}
}
store/db/mysql/attachment.go
View file @
4b4e7194
...
@@ -239,3 +239,36 @@ func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachmen
...
@@ -239,3 +239,36 @@ func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachmen
return
nil
return
nil
}
}
func
(
d
*
DB
)
DeleteAttachments
(
ctx
context
.
Context
,
deletes
[]
*
store
.
DeleteAttachment
)
error
{
if
len
(
deletes
)
==
0
{
return
nil
}
tx
,
err
:=
d
.
db
.
BeginTx
(
ctx
,
nil
)
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to start attachment delete transaction"
)
}
defer
func
()
{
if
tx
!=
nil
{
_
=
tx
.
Rollback
()
}
}()
stmt
:=
"DELETE FROM `attachment` WHERE `id` = ?"
for
_
,
delete
:=
range
deletes
{
result
,
err
:=
tx
.
ExecContext
(
ctx
,
stmt
,
delete
.
ID
)
if
err
!=
nil
{
return
err
}
if
_
,
err
:=
result
.
RowsAffected
();
err
!=
nil
{
return
err
}
}
if
err
:=
tx
.
Commit
();
err
!=
nil
{
return
err
}
tx
=
nil
return
nil
}
store/db/postgres/attachment.go
View file @
4b4e7194
...
@@ -219,3 +219,36 @@ func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachmen
...
@@ -219,3 +219,36 @@ func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachmen
}
}
return
nil
return
nil
}
}
func
(
d
*
DB
)
DeleteAttachments
(
ctx
context
.
Context
,
deletes
[]
*
store
.
DeleteAttachment
)
error
{
if
len
(
deletes
)
==
0
{
return
nil
}
tx
,
err
:=
d
.
db
.
BeginTx
(
ctx
,
nil
)
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to start attachment delete transaction"
)
}
defer
func
()
{
if
tx
!=
nil
{
_
=
tx
.
Rollback
()
}
}()
stmt
:=
`DELETE FROM attachment WHERE id = $1`
for
_
,
delete
:=
range
deletes
{
result
,
err
:=
tx
.
ExecContext
(
ctx
,
stmt
,
delete
.
ID
)
if
err
!=
nil
{
return
err
}
if
_
,
err
:=
result
.
RowsAffected
();
err
!=
nil
{
return
err
}
}
if
err
:=
tx
.
Commit
();
err
!=
nil
{
return
err
}
tx
=
nil
return
nil
}
store/db/sqlite/attachment.go
View file @
4b4e7194
...
@@ -219,3 +219,36 @@ func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachmen
...
@@ -219,3 +219,36 @@ func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachmen
}
}
return
nil
return
nil
}
}
func
(
d
*
DB
)
DeleteAttachments
(
ctx
context
.
Context
,
deletes
[]
*
store
.
DeleteAttachment
)
error
{
if
len
(
deletes
)
==
0
{
return
nil
}
tx
,
err
:=
d
.
db
.
BeginTx
(
ctx
,
nil
)
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to start attachment delete transaction"
)
}
defer
func
()
{
if
tx
!=
nil
{
_
=
tx
.
Rollback
()
}
}()
stmt
:=
"DELETE FROM `attachment` WHERE `id` = ?"
for
_
,
delete
:=
range
deletes
{
result
,
err
:=
tx
.
ExecContext
(
ctx
,
stmt
,
delete
.
ID
)
if
err
!=
nil
{
return
err
}
if
_
,
err
:=
result
.
RowsAffected
();
err
!=
nil
{
return
err
}
}
if
err
:=
tx
.
Commit
();
err
!=
nil
{
return
err
}
tx
=
nil
return
nil
}
store/driver.go
View file @
4b4e7194
...
@@ -18,6 +18,7 @@ type Driver interface {
...
@@ -18,6 +18,7 @@ type Driver interface {
ListAttachments
(
ctx
context
.
Context
,
find
*
FindAttachment
)
([]
*
Attachment
,
error
)
ListAttachments
(
ctx
context
.
Context
,
find
*
FindAttachment
)
([]
*
Attachment
,
error
)
UpdateAttachment
(
ctx
context
.
Context
,
update
*
UpdateAttachment
)
error
UpdateAttachment
(
ctx
context
.
Context
,
update
*
UpdateAttachment
)
error
DeleteAttachment
(
ctx
context
.
Context
,
delete
*
DeleteAttachment
)
error
DeleteAttachment
(
ctx
context
.
Context
,
delete
*
DeleteAttachment
)
error
DeleteAttachments
(
ctx
context
.
Context
,
deletes
[]
*
DeleteAttachment
)
error
// Memo model related methods.
// Memo model related methods.
CreateMemo
(
ctx
context
.
Context
,
create
*
Memo
)
(
*
Memo
,
error
)
CreateMemo
(
ctx
context
.
Context
,
create
*
Memo
)
(
*
Memo
,
error
)
...
...
web/src/components/MemoEditor/components/EditorMetadata.tsx
View file @
4b4e7194
...
@@ -12,6 +12,7 @@ export const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => {
...
@@ -12,6 +12,7 @@ export const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => {
attachments=
{
state
.
metadata
.
attachments
}
attachments=
{
state
.
metadata
.
attachments
}
localFiles=
{
state
.
localFiles
}
localFiles=
{
state
.
localFiles
}
onAttachmentsChange=
{
(
attachments
)
=>
dispatch
(
actions
.
setMetadata
({
attachments
}))
}
onAttachmentsChange=
{
(
attachments
)
=>
dispatch
(
actions
.
setMetadata
({
attachments
}))
}
onLocalFilesChange=
{
(
localFiles
)
=>
dispatch
(
actions
.
setLocalFiles
(
localFiles
))
}
onRemoveLocalFile=
{
(
previewUrl
)
=>
dispatch
(
actions
.
removeLocalFile
(
previewUrl
))
}
onRemoveLocalFile=
{
(
previewUrl
)
=>
dispatch
(
actions
.
removeLocalFile
(
previewUrl
))
}
/>
/>
...
...
web/src/components/MemoEditor/hooks/useFileUpload.ts
View file @
4b4e7194
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
useRef
}
from
"react"
;
import
{
useRef
}
from
"react"
;
import
{
type
MotionMedia
,
MotionMediaFamily
,
MotionMediaRole
,
MotionMediaSchema
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
type
{
LocalFile
}
from
"../types/attachment"
;
import
type
{
LocalFile
}
from
"../types/attachment"
;
export
const
useFileUpload
=
(
onFilesSelected
:
(
localFiles
:
LocalFile
[])
=>
void
)
=>
{
export
const
useFileUpload
=
(
onFilesSelected
:
(
localFiles
:
LocalFile
[])
=>
void
)
=>
{
...
@@ -11,10 +13,12 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void
...
@@ -11,10 +13,12 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void
return
;
return
;
}
}
selectingFlagRef
.
current
=
true
;
selectingFlagRef
.
current
=
true
;
const
localFiles
:
LocalFile
[]
=
files
.
map
((
file
)
=>
({
const
localFiles
:
LocalFile
[]
=
pairAppleLivePhotoFiles
(
files
.
map
((
file
)
=>
({
file
,
file
,
previewUrl
:
URL
.
createObjectURL
(
file
),
previewUrl
:
URL
.
createObjectURL
(
file
),
}));
})),
);
onFilesSelected
(
localFiles
);
onFilesSelected
(
localFiles
);
selectingFlagRef
.
current
=
false
;
selectingFlagRef
.
current
=
false
;
// Optionally clear input value to allow re-selecting the same file
// Optionally clear input value to allow re-selecting the same file
...
@@ -32,3 +36,53 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void
...
@@ -32,3 +36,53 @@ export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void
handleUploadClick
,
handleUploadClick
,
};
};
};
};
const
pairAppleLivePhotoFiles
=
(
localFiles
:
LocalFile
[]):
LocalFile
[]
=>
{
const
stemMap
=
new
Map
<
string
,
LocalFile
[]
>
();
for
(
const
localFile
of
localFiles
)
{
const
stem
=
normalizeFilenameStem
(
localFile
.
file
.
name
);
const
group
=
stemMap
.
get
(
stem
)
??
[];
group
.
push
(
localFile
);
stemMap
.
set
(
stem
,
group
);
}
const
groupIds
=
new
Map
<
string
,
string
>
();
return
localFiles
.
map
((
localFile
)
=>
{
const
stem
=
normalizeFilenameStem
(
localFile
.
file
.
name
);
const
group
=
stemMap
.
get
(
stem
)
??
[];
const
images
=
group
.
filter
((
item
)
=>
item
.
file
.
type
.
startsWith
(
"image/"
));
const
videos
=
group
.
filter
((
item
)
=>
item
.
file
.
type
.
startsWith
(
"video/"
));
if
(
images
.
length
!==
1
||
videos
.
length
!==
1
)
{
return
localFile
;
}
const
image
=
images
[
0
];
const
video
=
videos
[
0
];
const
groupId
=
groupIds
.
get
(
stem
)
??
`
${
stem
}
-
${
crypto
.
randomUUID
()}
`
;
groupIds
.
set
(
stem
,
groupId
);
if
(
localFile
.
previewUrl
===
image
.
previewUrl
)
{
return
{
...
localFile
,
motionMedia
:
buildLocalMotionMedia
(
groupId
,
MotionMediaRole
.
STILL
)
};
}
if
(
localFile
.
previewUrl
===
video
.
previewUrl
)
{
return
{
...
localFile
,
motionMedia
:
buildLocalMotionMedia
(
groupId
,
MotionMediaRole
.
VIDEO
)
};
}
return
localFile
;
});
};
const
buildLocalMotionMedia
=
(
groupId
:
string
,
role
:
MotionMediaRole
):
MotionMedia
=>
create
(
MotionMediaSchema
,
{
family
:
MotionMediaFamily
.
APPLE_LIVE_PHOTO
,
role
,
groupId
,
presentationTimestampUs
:
0
n
,
hasEmbeddedVideo
:
false
,
});
const
normalizeFilenameStem
=
(
filename
:
string
):
string
=>
{
const
parts
=
filename
.
split
(
"."
);
if
(
parts
.
length
<=
1
)
{
return
filename
.
toLowerCase
();
}
return
parts
.
slice
(
0
,
-
1
).
join
(
"."
).
toLowerCase
();
};
web/src/components/MemoEditor/services/uploadService.ts
View file @
4b4e7194
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
attachmentServiceClient
}
from
"@/connect"
;
import
{
attachmentServiceClient
}
from
"@/connect"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
AttachmentSchema
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
AttachmentSchema
,
MotionMediaSchema
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
type
{
LocalFile
}
from
"../types/attachment"
;
import
type
{
LocalFile
}
from
"../types/attachment"
;
export
const
uploadService
=
{
export
const
uploadService
=
{
...
@@ -10,7 +10,8 @@ export const uploadService = {
...
@@ -10,7 +10,8 @@ export const uploadService = {
const
attachments
:
Attachment
[]
=
[];
const
attachments
:
Attachment
[]
=
[];
for
(
const
{
file
}
of
localFiles
)
{
for
(
const
localFile
of
localFiles
)
{
const
{
file
,
motionMedia
}
=
localFile
;
const
buffer
=
new
Uint8Array
(
await
file
.
arrayBuffer
());
const
buffer
=
new
Uint8Array
(
await
file
.
arrayBuffer
());
const
attachment
=
await
attachmentServiceClient
.
createAttachment
({
const
attachment
=
await
attachmentServiceClient
.
createAttachment
({
attachment
:
create
(
AttachmentSchema
,
{
attachment
:
create
(
AttachmentSchema
,
{
...
@@ -18,6 +19,7 @@ export const uploadService = {
...
@@ -18,6 +19,7 @@ export const uploadService = {
size
:
BigInt
(
file
.
size
),
size
:
BigInt
(
file
.
size
),
type
:
file
.
type
,
type
:
file
.
type
,
content
:
buffer
,
content
:
buffer
,
motionMedia
:
motionMedia
?
create
(
MotionMediaSchema
,
motionMedia
)
:
undefined
,
}),
}),
});
});
attachments
.
push
(
attachment
);
attachments
.
push
(
attachment
);
...
...
web/src/components/MemoEditor/state/actions.ts
View file @
4b4e7194
...
@@ -49,6 +49,11 @@ export const editorActions = {
...
@@ -49,6 +49,11 @@ export const editorActions = {
payload
:
previewUrl
,
payload
:
previewUrl
,
}),
}),
setLocalFiles
:
(
files
:
LocalFile
[]):
EditorAction
=>
({
type
:
"SET_LOCAL_FILES"
,
payload
:
files
,
}),
clearLocalFiles
:
():
EditorAction
=>
({
clearLocalFiles
:
():
EditorAction
=>
({
type
:
"CLEAR_LOCAL_FILES"
,
type
:
"CLEAR_LOCAL_FILES"
,
}),
}),
...
...
web/src/components/MemoEditor/state/reducer.ts
View file @
4b4e7194
...
@@ -74,6 +74,12 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
...
@@ -74,6 +74,12 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
localFiles
:
state
.
localFiles
.
filter
((
f
)
=>
f
.
previewUrl
!==
action
.
payload
),
localFiles
:
state
.
localFiles
.
filter
((
f
)
=>
f
.
previewUrl
!==
action
.
payload
),
};
};
case
"SET_LOCAL_FILES"
:
return
{
...
state
,
localFiles
:
action
.
payload
,
};
case
"CLEAR_LOCAL_FILES"
:
case
"CLEAR_LOCAL_FILES"
:
return
{
return
{
...
state
,
...
state
,
...
...
web/src/components/MemoEditor/state/types.ts
View file @
4b4e7194
...
@@ -55,6 +55,7 @@ export type EditorAction =
...
@@ -55,6 +55,7 @@ export type EditorAction =
|
{
type
:
"REMOVE_RELATION"
;
payload
:
string
}
|
{
type
:
"REMOVE_RELATION"
;
payload
:
string
}
|
{
type
:
"ADD_LOCAL_FILE"
;
payload
:
LocalFile
}
|
{
type
:
"ADD_LOCAL_FILE"
;
payload
:
LocalFile
}
|
{
type
:
"REMOVE_LOCAL_FILE"
;
payload
:
string
}
|
{
type
:
"REMOVE_LOCAL_FILE"
;
payload
:
string
}
|
{
type
:
"SET_LOCAL_FILES"
;
payload
:
LocalFile
[]
}
|
{
type
:
"CLEAR_LOCAL_FILES"
}
|
{
type
:
"CLEAR_LOCAL_FILES"
}
|
{
type
:
"TOGGLE_FOCUS_MODE"
}
|
{
type
:
"TOGGLE_FOCUS_MODE"
}
|
{
type
:
"SET_LOADING"
;
payload
:
{
key
:
LoadingKey
;
value
:
boolean
}
}
|
{
type
:
"SET_LOADING"
;
payload
:
{
key
:
LoadingKey
;
value
:
boolean
}
}
...
...
web/src/components/MemoEditor/types/attachment.ts
View file @
4b4e7194
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
type
{
Attachment
,
MotionMedia
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
MotionMediaFamily
,
MotionMediaRole
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
getAttachmentThumbnailUrl
,
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
{
getAttachmentThumbnailUrl
,
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
{
buildAttachmentVisualItems
}
from
"@/utils/media-item"
;
export
type
FileCategory
=
"image"
|
"video"
|
"audio"
|
"document"
;
export
type
FileCategory
=
"image"
|
"video"
|
"
motion"
|
"
audio"
|
"document"
;
// Unified view model for rendering attachments and local files
export
interface
AttachmentItem
{
export
interface
AttachmentItem
{
readonly
id
:
string
;
readonly
id
:
string
;
readonly
memberIds
:
string
[];
readonly
filename
:
string
;
readonly
filename
:
string
;
readonly
category
:
FileCategory
;
readonly
category
:
FileCategory
;
readonly
mimeType
:
string
;
readonly
mimeType
:
string
;
...
@@ -15,25 +17,27 @@ export interface AttachmentItem {
...
@@ -15,25 +17,27 @@ export interface AttachmentItem {
readonly
isLocal
:
boolean
;
readonly
isLocal
:
boolean
;
}
}
// For MemoEditor: local files being uploaded
export
interface
LocalFile
{
export
interface
LocalFile
{
readonly
file
:
File
;
readonly
file
:
File
;
readonly
previewUrl
:
string
;
readonly
previewUrl
:
string
;
readonly
motionMedia
?:
MotionMedia
;
}
}
function
categorizeFile
(
mimeType
:
string
):
FileCategory
{
function
categorizeFile
(
mimeType
:
string
,
motionMedia
?:
MotionMedia
):
FileCategory
{
if
(
motionMedia
)
return
"motion"
;
if
(
mimeType
.
startsWith
(
"image/"
))
return
"image"
;
if
(
mimeType
.
startsWith
(
"image/"
))
return
"image"
;
if
(
mimeType
.
startsWith
(
"video/"
))
return
"video"
;
if
(
mimeType
.
startsWith
(
"video/"
))
return
"video"
;
if
(
mimeType
.
startsWith
(
"audio/"
))
return
"audio"
;
if
(
mimeType
.
startsWith
(
"audio/"
))
return
"audio"
;
return
"document"
;
return
"document"
;
}
}
export
function
attachment
ToItem
(
attachment
:
Attachment
):
AttachmentItem
{
function
attachmentGroup
ToItem
(
attachment
:
Attachment
):
AttachmentItem
{
const
attachmentType
=
getAttachmentType
(
attachment
);
const
attachmentType
=
getAttachmentType
(
attachment
);
const
sourceUrl
=
getAttachmentUrl
(
attachment
);
const
sourceUrl
=
getAttachmentUrl
(
attachment
);
return
{
return
{
id
:
attachment
.
name
,
id
:
attachment
.
name
,
memberIds
:
[
attachment
.
name
],
filename
:
attachment
.
filename
,
filename
:
attachment
.
filename
,
category
:
categorizeFile
(
attachment
.
type
),
category
:
categorizeFile
(
attachment
.
type
),
mimeType
:
attachment
.
type
,
mimeType
:
attachment
.
type
,
...
@@ -44,21 +48,96 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem {
...
@@ -44,21 +48,96 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem {
};
};
}
}
export
function
fileToItem
(
file
:
File
,
blobUrl
:
string
):
AttachmentItem
{
function
visualItemToAttachmentItem
(
item
:
ReturnType
<
typeof
buildAttachmentVisualItems
>
[
number
]
):
AttachmentItem
{
return
{
return
{
id
:
blobUrl
,
id
:
item
.
id
,
filename
:
file
.
name
,
memberIds
:
item
.
attachmentNames
,
category
:
categorizeFile
(
file
.
type
),
filename
:
item
.
filename
,
mimeType
:
file
.
type
,
category
:
item
.
kind
===
"motion"
?
"motion"
:
item
.
kind
,
thumbnailUrl
:
blobUrl
,
mimeType
:
item
.
mimeType
,
sourceUrl
:
blobUrl
,
thumbnailUrl
:
item
.
posterUrl
,
size
:
file
.
size
,
sourceUrl
:
item
.
sourceUrl
,
size
:
item
.
attachments
.
reduce
((
total
,
attachment
)
=>
total
+
Number
(
attachment
.
size
),
0
),
isLocal
:
false
,
};
}
function
fileToItem
(
file
:
LocalFile
):
AttachmentItem
{
return
{
id
:
file
.
motionMedia
?.
groupId
||
file
.
previewUrl
,
memberIds
:
[
file
.
previewUrl
],
filename
:
file
.
file
.
name
,
category
:
categorizeFile
(
file
.
file
.
type
,
file
.
motionMedia
),
mimeType
:
file
.
file
.
type
,
thumbnailUrl
:
file
.
previewUrl
,
sourceUrl
:
file
.
previewUrl
,
size
:
file
.
file
.
size
,
isLocal
:
true
,
isLocal
:
true
,
};
};
}
}
function
toLocalMotionItems
(
localFiles
:
LocalFile
[]):
AttachmentItem
[]
{
const
grouped
=
new
Map
<
string
,
LocalFile
[]
>
();
const
singles
:
AttachmentItem
[]
=
[];
for
(
const
localFile
of
localFiles
)
{
const
groupId
=
localFile
.
motionMedia
?.
groupId
;
if
(
!
groupId
)
{
singles
.
push
(
fileToItem
(
localFile
));
continue
;
}
const
group
=
grouped
.
get
(
groupId
)
??
[];
group
.
push
(
localFile
);
grouped
.
set
(
groupId
,
group
);
}
const
groupedItems
=
Array
.
from
(
grouped
.
entries
()).
flatMap
(([
groupId
,
files
])
=>
{
const
still
=
files
.
find
(
(
file
)
=>
file
.
motionMedia
?.
family
===
MotionMediaFamily
.
APPLE_LIVE_PHOTO
&&
file
.
motionMedia
.
role
===
MotionMediaRole
.
STILL
,
);
const
video
=
files
.
find
(
(
file
)
=>
file
.
motionMedia
?.
family
===
MotionMediaFamily
.
APPLE_LIVE_PHOTO
&&
file
.
motionMedia
.
role
===
MotionMediaRole
.
VIDEO
,
);
if
(
still
&&
video
&&
files
.
length
===
2
)
{
return
[
{
id
:
groupId
,
memberIds
:
[
still
.
previewUrl
,
video
.
previewUrl
],
filename
:
still
.
file
.
name
,
category
:
"motion"
as
const
,
mimeType
:
still
.
file
.
type
,
thumbnailUrl
:
still
.
previewUrl
,
sourceUrl
:
video
.
previewUrl
,
size
:
still
.
file
.
size
+
video
.
file
.
size
,
isLocal
:
true
,
},
];
}
return
files
.
map
(
fileToItem
);
});
return
[...
groupedItems
,
...
singles
];
}
export
function
toAttachmentItems
(
attachments
:
Attachment
[],
localFiles
:
LocalFile
[]
=
[]):
AttachmentItem
[]
{
export
function
toAttachmentItems
(
attachments
:
Attachment
[],
localFiles
:
LocalFile
[]
=
[]):
AttachmentItem
[]
{
return
[...
attachments
.
map
(
attachmentToItem
),
...
localFiles
.
map
(({
file
,
previewUrl
})
=>
fileToItem
(
file
,
previewUrl
))];
const
visualAttachments
=
attachments
.
filter
((
attachment
)
=>
{
const
attachmentType
=
getAttachmentType
(
attachment
);
return
attachmentType
===
"image/*"
||
attachmentType
===
"video/*"
||
attachment
.
motionMedia
!==
undefined
;
});
const
attachmentVisualIds
=
new
Set
<
string
>
();
const
attachmentVisualItems
=
buildAttachmentVisualItems
(
visualAttachments
).
map
((
item
)
=>
{
item
.
attachmentNames
.
forEach
((
name
)
=>
attachmentVisualIds
.
add
(
name
));
return
visualItemToAttachmentItem
(
item
);
});
const
nonVisualAttachmentItems
=
attachments
.
filter
((
attachment
)
=>
!
attachmentVisualIds
.
has
(
attachment
.
name
))
.
map
(
attachmentGroupToItem
)
.
filter
((
item
)
=>
item
.
category
===
"audio"
||
item
.
category
===
"document"
);
return
[...
attachmentVisualItems
,
...
nonVisualAttachmentItems
,
...
toLocalMotionItems
(
localFiles
)];
}
}
export
function
filterByCategory
(
items
:
AttachmentItem
[],
categories
:
FileCategory
[]):
AttachmentItem
[]
{
export
function
filterByCategory
(
items
:
AttachmentItem
[],
categories
:
FileCategory
[]):
AttachmentItem
[]
{
...
@@ -71,7 +150,7 @@ export function separateMediaAndDocs(items: AttachmentItem[]): { media: Attachme
...
@@ -71,7 +150,7 @@ export function separateMediaAndDocs(items: AttachmentItem[]): { media: Attachme
const
docs
:
AttachmentItem
[]
=
[];
const
docs
:
AttachmentItem
[]
=
[];
for
(
const
item
of
items
)
{
for
(
const
item
of
items
)
{
if
(
item
.
category
===
"image"
||
item
.
category
===
"video"
)
{
if
(
item
.
category
===
"image"
||
item
.
category
===
"video"
||
item
.
category
===
"motion"
)
{
media
.
push
(
item
);
media
.
push
(
item
);
}
else
{
}
else
{
docs
.
push
(
item
);
docs
.
push
(
item
);
...
...
web/src/components/MemoMetadata/Attachment/AttachmentListEditor.tsx
View file @
4b4e7194
...
@@ -11,6 +11,7 @@ interface AttachmentListEditorProps {
...
@@ -11,6 +11,7 @@ interface AttachmentListEditorProps {
attachments
:
Attachment
[];
attachments
:
Attachment
[];
localFiles
?:
LocalFile
[];
localFiles
?:
LocalFile
[];
onAttachmentsChange
?:
(
attachments
:
Attachment
[])
=>
void
;
onAttachmentsChange
?:
(
attachments
:
Attachment
[])
=>
void
;
onLocalFilesChange
?:
(
localFiles
:
LocalFile
[])
=>
void
;
onRemoveLocalFile
?:
(
previewUrl
:
string
)
=>
void
;
onRemoveLocalFile
?:
(
previewUrl
:
string
)
=>
void
;
}
}
...
@@ -23,19 +24,24 @@ const AttachmentItemCard: FC<{
...
@@ -23,19 +24,24 @@ const AttachmentItemCard: FC<{
canMoveDown
?:
boolean
;
canMoveDown
?:
boolean
;
}
>
=
({
item
,
onRemove
,
onMoveUp
,
onMoveDown
,
canMoveUp
=
true
,
canMoveDown
=
true
})
=>
{
}
>
=
({
item
,
onRemove
,
onMoveUp
,
onMoveDown
,
canMoveUp
=
true
,
canMoveDown
=
true
})
=>
{
const
{
category
,
filename
,
thumbnailUrl
,
mimeType
,
size
}
=
item
;
const
{
category
,
filename
,
thumbnailUrl
,
mimeType
,
size
}
=
item
;
const
fileTypeLabel
=
getFileTypeLabel
(
mimeType
);
const
fileTypeLabel
=
item
.
category
===
"motion"
?
"Live Photo"
:
getFileTypeLabel
(
mimeType
);
const
fileSizeLabel
=
size
?
formatFileSize
(
size
)
:
undefined
;
const
fileSizeLabel
=
size
?
formatFileSize
(
size
)
:
undefined
;
const
displayName
=
category
===
"audio"
&&
/^voice-
(
recording|note
)
-/i
.
test
(
filename
)
?
"Voice note"
:
filename
;
const
displayName
=
category
===
"audio"
&&
/^voice-
(
recording|note
)
-/i
.
test
(
filename
)
?
"Voice note"
:
filename
;
return
(
return
(
<
div
className=
"relative rounded border border-transparent px-1.5 py-1 transition-all hover:border-border hover:bg-accent/20"
>
<
div
className=
"relative rounded border border-transparent px-1.5 py-1 transition-all hover:border-border hover:bg-accent/20"
>
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded bg-muted/40"
>
<
div
className=
"
relative
flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded bg-muted/40"
>
{
category
===
"image"
&&
thumbnailUrl
?
(
{
(
category
===
"image"
||
category
===
"motion"
)
&&
thumbnailUrl
?
(
<
img
src=
{
thumbnailUrl
}
alt=
""
className=
"h-full w-full object-cover"
/>
<
img
src=
{
thumbnailUrl
}
alt=
""
className=
"h-full w-full object-cover"
/>
)
:
(
)
:
(
<
FileIcon
className=
"h-3.5 w-3.5 text-muted-foreground"
/>
<
FileIcon
className=
"h-3.5 w-3.5 text-muted-foreground"
/>
)
}
)
}
{
category
===
"motion"
&&
(
<
span
className=
"absolute inset-x-0 bottom-0 bg-black/70 text-center text-[7px] font-semibold uppercase tracking-wide text-white"
>
Live
</
span
>
)
}
</
div
>
</
div
>
<
div
className=
"min-w-0 flex-1 flex flex-col gap-0.5 sm:flex-row sm:items-baseline sm:gap-1.5"
>
<
div
className=
"min-w-0 flex-1 flex flex-col gap-0.5 sm:flex-row sm:items-baseline sm:gap-1.5"
>
...
@@ -104,58 +110,87 @@ const AttachmentItemCard: FC<{
...
@@ -104,58 +110,87 @@ const AttachmentItemCard: FC<{
);
);
};
};
const
AttachmentListEditor
:
FC
<
AttachmentListEditorProps
>
=
({
attachments
,
localFiles
=
[],
onAttachmentsChange
,
onRemoveLocalFile
})
=>
{
const
AttachmentListEditor
:
FC
<
AttachmentListEditorProps
>
=
({
attachments
,
localFiles
=
[],
onAttachmentsChange
,
onLocalFilesChange
,
onRemoveLocalFile
,
})
=>
{
if
(
attachments
.
length
===
0
&&
localFiles
.
length
===
0
)
{
if
(
attachments
.
length
===
0
&&
localFiles
.
length
===
0
)
{
return
null
;
return
null
;
}
}
const
items
=
toAttachmentItems
(
attachments
,
localFiles
);
const
items
=
toAttachmentItems
(
attachments
,
localFiles
);
const
attachmentItems
=
items
.
filter
((
item
)
=>
!
item
.
isLocal
);
const
localItems
=
items
.
filter
((
item
)
=>
item
.
isLocal
);
const
handleMove
Up
=
(
index
:
number
)
=>
{
const
handleMove
Attachments
=
(
itemId
:
string
,
direction
:
-
1
|
1
)
=>
{
if
(
index
===
0
||
!
onAttachmentsChange
)
return
;
if
(
!
onAttachmentsChange
)
return
;
const
newAttachments
=
[...
attachments
];
const
itemIndex
=
attachmentItems
.
findIndex
((
item
)
=>
item
.
id
===
itemId
);
[
newAttachments
[
index
-
1
],
newAttachments
[
index
]]
=
[
newAttachments
[
index
],
newAttachments
[
index
-
1
]];
const
targetIndex
=
itemIndex
+
direction
;
onAttachmentsChange
(
newAttachments
);
if
(
itemIndex
<
0
||
targetIndex
<
0
||
targetIndex
>=
attachmentItems
.
length
)
{
};
return
;
}
const
handleMoveDown
=
(
index
:
number
)
=>
{
const
reorderedItems
=
[...
attachmentItems
];
if
(
index
===
attachments
.
length
-
1
||
!
onAttachmentsChange
)
return
;
[
reorderedItems
[
itemIndex
],
reorderedItems
[
targetIndex
]]
=
[
reorderedItems
[
targetIndex
],
reorderedItems
[
itemIndex
]]
;
const
newAttachments
=
[...
attachments
];
const
attachmentMap
=
new
Map
(
attachments
.
map
((
attachment
)
=>
[
attachment
.
name
,
attachment
]));
[
newAttachments
[
index
],
newAttachments
[
index
+
1
]]
=
[
newAttachments
[
index
+
1
],
newAttachments
[
index
]];
onAttachmentsChange
(
onAttachmentsChange
(
newAttachments
);
reorderedItems
.
flatMap
((
item
)
=>
item
.
memberIds
.
map
((
memberId
)
=>
attachmentMap
.
get
(
memberId
)).
filter
(
Boolean
)
as
Attachment
[]),
);
};
};
const
handleRemoveAttachment
=
(
name
:
string
)
=>
{
const
handleMoveLocalFiles
=
(
itemId
:
string
,
direction
:
-
1
|
1
)
=>
{
if
(
onAttachmentsChange
)
{
if
(
!
onLocalFilesChange
)
return
;
onAttachmentsChange
(
attachments
.
filter
((
attachment
)
=>
attachment
.
name
!==
name
));
const
itemIndex
=
localItems
.
findIndex
((
item
)
=>
item
.
id
===
itemId
);
const
targetIndex
=
itemIndex
+
direction
;
if
(
itemIndex
<
0
||
targetIndex
<
0
||
targetIndex
>=
localItems
.
length
)
{
return
;
}
}
const
reorderedItems
=
[...
localItems
];
[
reorderedItems
[
itemIndex
],
reorderedItems
[
targetIndex
]]
=
[
reorderedItems
[
targetIndex
],
reorderedItems
[
itemIndex
]];
const
localFileMap
=
new
Map
(
localFiles
.
map
((
localFile
)
=>
[
localFile
.
previewUrl
,
localFile
]));
onLocalFilesChange
(
reorderedItems
.
flatMap
((
item
)
=>
item
.
memberIds
.
map
((
memberId
)
=>
localFileMap
.
get
(
memberId
)).
filter
(
Boolean
)
as
LocalFile
[]),
);
};
};
const
handleRemoveItem
=
(
item
:
(
typeof
items
)[
0
]
)
=>
{
const
handleRemoveItem
=
(
item
:
AttachmentItem
)
=>
{
if
(
item
.
isLocal
)
{
if
(
item
.
isLocal
)
{
onRemoveLocalFile
?.(
item
.
id
);
const
nextLocalFiles
=
localFiles
.
filter
((
file
)
=>
!
item
.
memberIds
.
includes
(
file
.
previewUrl
));
}
else
{
onLocalFilesChange
?.(
nextLocalFiles
);
handleRemoveAttachment
(
item
.
id
);
if
(
!
onLocalFilesChange
)
{
item
.
memberIds
.
forEach
((
previewUrl
)
=>
onRemoveLocalFile
?.(
previewUrl
));
}
return
;
}
if
(
onAttachmentsChange
)
{
onAttachmentsChange
(
attachments
.
filter
((
attachment
)
=>
!
item
.
memberIds
.
includes
(
attachment
.
name
)));
}
}
};
};
return
(
return
(
<
MetadataSection
icon=
{
PaperclipIcon
}
title=
"Attachments"
count=
{
items
.
length
}
contentClassName=
"flex flex-col gap-0.5 p-1 sm:p-1.5"
>
<
MetadataSection
icon=
{
PaperclipIcon
}
title=
"Attachments"
count=
{
items
.
length
}
contentClassName=
"flex flex-col gap-0.5 p-1 sm:p-1.5"
>
{
items
.
map
((
item
)
=>
{
{
items
.
map
((
item
)
=>
{
const
i
sLocalFile
=
item
.
isLocal
;
const
i
temList
=
item
.
isLocal
?
localItems
:
attachmentItems
;
const
attachmentIndex
=
isLocalFile
?
-
1
:
attachments
.
findIndex
((
a
)
=>
a
.
name
===
item
.
id
);
const
itemIndex
=
itemList
.
findIndex
((
entry
)
=>
entry
.
id
===
item
.
id
);
return
(
return
(
<
AttachmentItemCard
<
AttachmentItemCard
key=
{
item
.
id
}
key=
{
item
.
id
}
item=
{
item
}
item=
{
item
}
onRemove=
{
()
=>
handleRemoveItem
(
item
)
}
onRemove=
{
()
=>
handleRemoveItem
(
item
)
}
onMoveUp=
{
!
isLocalFile
?
()
=>
handleMoveUp
(
attachmentIndex
)
:
undefined
}
onMoveUp=
{
item
.
isLocal
?
()
=>
handleMoveLocalFiles
(
item
.
id
,
-
1
)
:
()
=>
handleMoveAttachments
(
item
.
id
,
-
1
)
}
onMoveDown=
{
!
isLocalFile
?
()
=>
handleMoveDown
(
attachmentIndex
)
:
undefined
}
onMoveDown=
{
item
.
isLocal
?
()
=>
handleMoveLocalFiles
(
item
.
id
,
1
)
:
()
=>
handleMoveAttachments
(
item
.
id
,
1
)
}
canMoveUp=
{
!
isLocalFile
&&
attachment
Index
>
0
}
canMoveUp=
{
item
Index
>
0
}
canMoveDown=
{
!
isLocalFile
&&
attachmentIndex
<
attachments
.
length
-
1
}
canMoveDown=
{
itemIndex
>=
0
&&
itemIndex
<
itemList
.
length
-
1
}
/>
/>
);
);
})
}
})
}
...
...
web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx
View file @
4b4e7194
import
{
DownloadIcon
,
FileIcon
,
Maximize2Icon
,
PaperclipIcon
,
PlayIcon
}
from
"lucide-react"
;
import
{
DownloadIcon
,
FileIcon
,
PaperclipIcon
,
PlayIcon
}
from
"lucide-react"
;
import
{
useMemo
}
from
"react"
;
import
{
useMemo
}
from
"react"
;
import
MetadataSection
from
"@/components/MemoMetadata/MetadataSection"
;
import
MetadataSection
from
"@/components/MemoMetadata/MetadataSection"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
cn
}
from
"@/lib/utils"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
{
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
AttachmentCard
from
"./AttachmentCard"
;
import
type
{
PreviewMediaItem
}
from
"@/utils/media-item"
;
import
{
buildAttachmentVisualItems
}
from
"@/utils/media-item"
;
import
AudioAttachmentItem
from
"./AudioAttachmentItem"
;
import
AudioAttachmentItem
from
"./AudioAttachmentItem"
;
import
{
getAttachmentMetadata
,
is
ImageAttachment
,
isVide
oAttachment
,
separateAttachments
}
from
"./attachmentHelpers"
;
import
{
getAttachmentMetadata
,
is
Audi
oAttachment
,
separateAttachments
}
from
"./attachmentHelpers"
;
interface
AttachmentListViewProps
{
interface
AttachmentListViewProps
{
attachments
:
Attachment
[];
attachments
:
Attachment
[];
onImagePreview
?:
(
urls
:
string
[],
index
:
number
)
=>
void
;
onImagePreview
?:
(
items
:
PreviewMediaItem
[],
index
:
number
)
=>
void
;
}
}
const
AttachmentMeta
=
({
attachment
}:
{
attachment
:
Attachment
})
=>
{
const
AttachmentMeta
=
({
attachment
}:
{
attachment
:
Attachment
})
=>
{
...
@@ -48,21 +49,26 @@ const DocumentItem = ({ attachment }: { attachment: Attachment }) => {
...
@@ -48,21 +49,26 @@ const DocumentItem = ({ attachment }: { attachment: Attachment }) => {
);
);
};
};
interface
VisualItemProps
{
const
MotionBadge
=
()
=>
(
attachment
:
Attachment
;
<
span
className=
"pointer-events-none absolute left-2 top-2 rounded-full bg-black/70 px-2 py-0.5 text-[10px] font-semibold tracking-wide text-white backdrop-blur-sm"
>
featured
?:
boolean
;
LIVE
}
</
span
>
);
const
ImageItem
=
({
attachment
,
onImageClick
,
featured
=
false
}:
VisualItemProps
&
{
onImageClick
?:
(
url
:
string
)
=>
void
})
=>
{
const
handleClick
=
()
=>
{
onImageClick
?.(
getAttachmentUrl
(
attachment
));
};
const
MotionItem
=
({
item
,
featured
=
false
,
onPreview
,
}:
{
item
:
ReturnType
<
typeof
buildAttachmentVisualItems
>
[
number
];
featured
?:
boolean
;
onPreview
?:
()
=>
void
;
})
=>
{
return
(
return
(
<
button
<
button
type=
"button"
type=
"button"
className=
{
cn
(
"group block w-full text-left"
,
featured
?
"max-w-[18rem] sm:max-w-[20rem]"
:
""
)
}
className=
{
cn
(
"group block w-full text-left"
,
featured
?
"max-w-[18rem] sm:max-w-[20rem]"
:
""
)
}
onClick=
{
handleClick
}
onClick=
{
onPreview
}
>
>
<
div
<
div
className=
{
cn
(
className=
{
cn
(
...
@@ -70,77 +76,54 @@ const ImageItem = ({ attachment, onImageClick, featured = false }: VisualItemPro
...
@@ -70,77 +76,54 @@ const ImageItem = ({ attachment, onImageClick, featured = false }: VisualItemPro
featured
?
"aspect-[4/3]"
:
"aspect-square"
,
featured
?
"aspect-[4/3]"
:
"aspect-square"
,
)
}
)
}
>
>
<
AttachmentCard
{
item
.
kind
===
"video"
?
(
attachment=
{
attachment
}
<
video
className=
"h-full w-full rounded-none transition-transform duration-300 group-hover:scale-[1.02]"
src=
{
item
.
sourceUrl
}
className=
"h-full w-full rounded-none object-cover transition-transform duration-300 group-hover:scale-[1.02]"
preload=
"metadata"
/>
/>
)
:
(
<
div
className=
"pointer-events-none absolute inset-0 bg-gradient-to-t from-black/15 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100"
/>
<
img
<
span
className=
"pointer-events-none absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-background/80 text-foreground/70 backdrop-blur-sm"
>
src=
{
item
.
posterUrl
}
<
Maximize2Icon
className=
"h-3.5 w-3.5"
/>
alt=
{
item
.
filename
}
className=
"h-full w-full rounded-none object-cover transition-transform duration-300 group-hover:scale-[1.02]"
loading=
"lazy"
decoding=
"async"
/>
)
}
<
div
className=
"pointer-events-none absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100"
/>
{
item
.
kind
===
"motion"
&&
<
MotionBadge
/>
}
{
item
.
previewItem
.
kind
===
"video"
&&
(
<
span
className=
"pointer-events-none absolute bottom-2 right-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-background/80 text-foreground/70 backdrop-blur-sm"
>
<
PlayIcon
className=
"h-3.5 w-3.5 fill-current"
/>
</
span
>
</
span
>
)
}
</
div
>
</
div
>
</
button
>
</
button
>
);
);
};
};
const
ImageGallery
=
({
attachments
,
onImageClick
}:
{
attachments
:
Attachment
[];
onImageClick
?:
(
url
:
string
)
=>
void
})
=>
{
const
VisualGallery
=
({
if
(
attachments
.
length
===
1
)
{
items
,
onPreview
,
}:
{
items
:
ReturnType
<
typeof
buildAttachmentVisualItems
>
;
onPreview
?:
(
itemId
:
string
)
=>
void
;
})
=>
{
if
(
items
.
length
===
1
)
{
return
(
return
(
<
div
className=
"flex"
>
<
div
className=
"flex"
>
<
ImageItem
attachment=
{
attachments
[
0
]
}
featured
onImageClick=
{
onImageClick
}
/>
<
MotionItem
item=
{
items
[
0
]
}
featured
onPreview=
{
()
=>
onPreview
?.(
items
[
0
].
id
)
}
/>
</
div
>
</
div
>
);
);
}
}
return
(
return
(
<
div
className=
"grid max-w-[22rem] grid-cols-2 gap-1.5 sm:max-w-[24rem]"
>
<
div
className=
"grid max-w-[22rem] grid-cols-2 gap-1.5 sm:max-w-[24rem]"
>
{
attachments
.
map
((
attachment
)
=>
(
{
items
.
map
((
item
)
=>
(
<
ImageItem
key=
{
attachment
.
name
}
attachment=
{
attachment
}
onImageClick=
{
onImageClick
}
/>
<
MotionItem
key=
{
item
.
id
}
item=
{
item
}
onPreview=
{
()
=>
onPreview
?.(
item
.
id
)
}
/>
))
}
</
div
>
);
};
const
VideoItem
=
({
attachment
}:
VisualItemProps
)
=>
(
<
div
className=
"w-full max-w-[20rem] overflow-hidden rounded-xl border border-border/70 bg-background/80"
>
<
div
className=
"relative aspect-video bg-muted/40"
>
<
AttachmentCard
attachment=
{
attachment
}
className=
"h-full w-full rounded-none"
/>
<
span
className=
"pointer-events-none absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-background/80 text-foreground/70 backdrop-blur-sm"
>
<
PlayIcon
className=
"h-3.5 w-3.5 fill-current"
/>
</
span
>
</
div
>
<
div
className=
"border-t border-border/60 px-3 py-2.5"
>
<
div
className=
"truncate text-sm font-medium leading-tight text-foreground"
title=
{
attachment
.
filename
}
>
{
attachment
.
filename
}
</
div
>
<
AttachmentMeta
attachment=
{
attachment
}
/>
</
div
>
</
div
>
);
const
VideoList
=
({
attachments
}:
{
attachments
:
Attachment
[]
})
=>
(
<
div
className=
"flex flex-wrap gap-2"
>
{
attachments
.
map
((
attachment
)
=>
(
<
VideoItem
key=
{
attachment
.
name
}
attachment=
{
attachment
}
/>
))
}
))
}
</
div
>
</
div
>
);
const
VisualSection
=
({
attachments
,
onImageClick
}:
{
attachments
:
Attachment
[];
onImageClick
?:
(
url
:
string
)
=>
void
})
=>
{
const
images
=
attachments
.
filter
(
isImageAttachment
);
const
videos
=
attachments
.
filter
(
isVideoAttachment
);
return
(
<
div
className=
"flex flex-col gap-2"
>
{
images
.
length
>
0
&&
<
ImageGallery
attachments=
{
images
}
onImageClick=
{
onImageClick
}
/>
}
{
videos
.
length
>
0
&&
(
<
div
className=
"flex flex-col gap-2"
>
{
images
.
length
>
0
&&
<
Divider
/>
}
<
VideoList
attachments=
{
videos
}
/>
</
div
>
)
}
</
div
>
);
);
};
};
...
@@ -172,9 +155,9 @@ const Divider = () => <div className="border-t border-border/70 opacity-80" />;
...
@@ -172,9 +155,9 @@ const Divider = () => <div className="border-t border-border/70 opacity-80" />;
const
AttachmentListView
=
({
attachments
,
onImagePreview
}:
AttachmentListViewProps
)
=>
{
const
AttachmentListView
=
({
attachments
,
onImagePreview
}:
AttachmentListViewProps
)
=>
{
const
{
visual
,
audio
,
docs
}
=
useMemo
(()
=>
separateAttachments
(
attachments
),
[
attachments
]);
const
{
visual
,
audio
,
docs
}
=
useMemo
(()
=>
separateAttachments
(
attachments
),
[
attachments
]);
const
imageAttachments
=
useMemo
(()
=>
visual
.
filter
(
isImageAttachment
),
[
visual
]);
const
visualItems
=
useMemo
(()
=>
buildAttachmentVisualItems
(
visual
),
[
visual
]);
const
imageUrls
=
useMemo
(()
=>
imageAttachments
.
map
(
getAttachmentUrl
),
[
imageAttachment
s
]);
const
previewItems
=
useMemo
(()
=>
visualItems
.
map
((
item
)
=>
item
.
previewItem
),
[
visualItem
s
]);
const
hasVisual
=
visual
.
length
>
0
;
const
hasVisual
=
visual
Items
.
length
>
0
;
const
hasAudio
=
audio
.
length
>
0
;
const
hasAudio
=
audio
.
length
>
0
;
const
hasDocs
=
docs
.
length
>
0
;
const
hasDocs
=
docs
.
length
>
0
;
const
sectionCount
=
[
hasVisual
,
hasAudio
,
hasDocs
].
filter
(
Boolean
).
length
;
const
sectionCount
=
[
hasVisual
,
hasAudio
,
hasDocs
].
filter
(
Boolean
).
length
;
...
@@ -183,16 +166,21 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP
...
@@ -183,16 +166,21 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP
return
null
;
return
null
;
}
}
const
handle
ImageClick
=
(
imgUrl
:
string
)
=>
{
const
handle
Preview
=
(
itemId
:
string
)
=>
{
const
index
=
imageUrls
.
findIndex
((
url
)
=>
url
===
imgUrl
);
const
index
=
previewItems
.
findIndex
((
item
)
=>
item
.
id
===
itemId
);
onImagePreview
?.(
imageUrl
s
,
index
>=
0
?
index
:
0
);
onImagePreview
?.(
previewItem
s
,
index
>=
0
?
index
:
0
);
};
};
return
(
return
(
<
MetadataSection
icon=
{
PaperclipIcon
}
title=
"Attachments"
count=
{
attachments
.
length
}
contentClassName=
"flex flex-col gap-2 p-2"
>
<
MetadataSection
{
hasVisual
&&
<
VisualSection
attachments=
{
visual
}
onImageClick=
{
handleImageClick
}
/>
}
icon=
{
PaperclipIcon
}
title=
"Attachments"
count=
{
visualItems
.
length
+
audio
.
length
+
docs
.
length
}
contentClassName=
"flex flex-col gap-2 p-2"
>
{
hasVisual
&&
<
VisualGallery
items=
{
visualItems
}
onPreview=
{
handlePreview
}
/>
}
{
hasVisual
&&
sectionCount
>
1
&&
<
Divider
/>
}
{
hasVisual
&&
sectionCount
>
1
&&
<
Divider
/>
}
{
hasAudio
&&
<
AudioList
attachments=
{
audio
}
/>
}
{
hasAudio
&&
<
AudioList
attachments=
{
audio
.
filter
(
isAudioAttachment
)
}
/>
}
{
hasAudio
&&
hasDocs
&&
<
Divider
/>
}
{
hasAudio
&&
hasDocs
&&
<
Divider
/>
}
{
hasDocs
&&
<
DocsList
attachments=
{
docs
}
/>
}
{
hasDocs
&&
<
DocsList
attachments=
{
docs
}
/>
}
</
MetadataSection
>
</
MetadataSection
>
...
...
web/src/components/MemoPreview/MemoPreview.tsx
View file @
4b4e7194
...
@@ -5,7 +5,8 @@ import { cn } from "@/lib/utils";
...
@@ -5,7 +5,8 @@ import { cn } from "@/lib/utils";
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
MemoSchema
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
MemoSchema
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service_pb"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service_pb"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
{
getAttachmentType
,
isMotionAttachment
}
from
"@/utils/attachment"
;
import
{
buildAttachmentVisualItems
,
countLogicalAttachmentItems
}
from
"@/utils/media-item"
;
import
MemoContent
from
"../MemoContent"
;
import
MemoContent
from
"../MemoContent"
;
import
{
MemoViewContext
,
type
MemoViewContextValue
}
from
"../MemoView/MemoViewContext"
;
import
{
MemoViewContext
,
type
MemoViewContextValue
}
from
"../MemoView/MemoViewContext"
;
...
@@ -36,28 +37,35 @@ const STUB_CONTEXT: MemoViewContextValue = {
...
@@ -36,28 +37,35 @@ const STUB_CONTEXT: MemoViewContextValue = {
};
};
const
AttachmentThumbnails
=
({
attachments
}:
{
attachments
:
Attachment
[]
})
=>
{
const
AttachmentThumbnails
=
({
attachments
}:
{
attachments
:
Attachment
[]
})
=>
{
const
images
:
Attachment
[]
=
[];
const
visualAttachments
=
attachments
.
filter
(
const
others
:
Attachment
[]
=
[];
(
attachment
)
=>
for
(
const
a
of
attachments
)
{
getAttachmentType
(
attachment
)
===
"image/*"
||
getAttachmentType
(
attachment
)
===
"video/*"
||
isMotionAttachment
(
attachment
),
if
(
getAttachmentType
(
a
)
===
"image/*"
)
images
.
push
(
a
);
);
else
others
.
push
(
a
);
const
items
=
buildAttachmentVisualItems
(
visualAttachments
);
}
const
images
=
items
.
filter
((
item
)
=>
item
.
kind
===
"image"
||
item
.
kind
===
"motion"
);
const
others
=
items
.
filter
((
item
)
=>
item
.
kind
===
"video"
);
return
(
return
(
<
div
className=
"flex items-center gap-1.5 flex-wrap"
>
<
div
className=
"flex items-center gap-1.5 flex-wrap"
>
{
images
.
map
((
a
)
=>
(
{
images
.
map
((
item
)
=>
(
<
div
key=
{
item
.
id
}
className=
"relative"
>
<
img
<
img
key=
{
a
.
name
}
src=
{
item
.
posterUrl
}
src=
{
getAttachmentUrl
(
a
)
}
alt=
{
item
.
filename
}
alt=
{
a
.
filename
}
className=
"w-10 h-10 rounded border border-border object-cover bg-muted/40"
className=
"w-10 h-10 rounded border border-border object-cover bg-muted/40"
loading=
"lazy"
loading=
"lazy"
/>
/>
{
item
.
kind
===
"motion"
&&
(
<
span
className=
"absolute left-1 top-1 rounded bg-black/70 px-1 py-0.5 text-[8px] font-semibold leading-none text-white"
>
LIVE
</
span
>
)
}
</
div
>
))
}
))
}
{
others
.
map
((
a
)
=>
(
{
others
.
map
((
item
)
=>
(
<
div
key=
{
a
.
name
}
className=
"flex items-center gap-1 text-[10px] text-muted-foreground"
>
<
div
key=
{
item
.
id
}
className=
"flex items-center gap-1 text-[10px] text-muted-foreground"
>
<
FileIcon
className=
"w-3 h-3 shrink-0"
/>
<
FileIcon
className=
"w-3 h-3 shrink-0"
/>
<
span
className=
"truncate max-w-[80px]"
>
{
a
.
filename
}
</
span
>
<
span
className=
"truncate max-w-[80px]"
>
{
item
.
filename
}
</
span
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
...
@@ -138,7 +146,7 @@ const MemoPreview = ({
...
@@ -138,7 +146,7 @@ const MemoPreview = ({
(
truncate
?
(
(
truncate
?
(
<
div
className=
"shrink-0 text-muted-foreground/70 inline-flex justify-center items-center gap-0.5"
>
<
div
className=
"shrink-0 text-muted-foreground/70 inline-flex justify-center items-center gap-0.5"
>
<
FileIcon
className=
"w-3 h-3 inline-block"
/>
<
FileIcon
className=
"w-3 h-3 inline-block"
/>
<
span
className=
"text-xs"
>
{
attachments
.
length
}
</
span
>
<
span
className=
"text-xs"
>
{
countLogicalAttachmentItems
(
attachments
)
}
</
span
>
</
div
>
</
div
>
)
:
(
)
:
(
<
AttachmentThumbnails
attachments=
{
attachments
}
/>
<
AttachmentThumbnails
attachments=
{
attachments
}
/>
...
...
web/src/components/MemoView/MemoView.tsx
View file @
4b4e7194
...
@@ -97,7 +97,7 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
...
@@ -97,7 +97,7 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
<
PreviewImageDialog
<
PreviewImageDialog
open=
{
previewState
.
open
}
open=
{
previewState
.
open
}
onOpenChange=
{
setPreviewOpen
}
onOpenChange=
{
setPreviewOpen
}
i
mgUrls=
{
previewState
.
url
s
}
i
tems=
{
previewState
.
item
s
}
initialIndex=
{
previewState
.
index
}
initialIndex=
{
previewState
.
index
}
/>
/>
</
article
>
</
article
>
...
...
web/src/components/MemoView/MemoViewContext.tsx
View file @
4b4e7194
...
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
...
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
type
{
Memo
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
{
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service_pb"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service_pb"
;
import
type
{
User
}
from
"@/types/proto/api/v1/user_service_pb"
;
import
type
{
PreviewMediaItem
}
from
"@/utils/media-item"
;
import
{
RELATIVE_TIME_THRESHOLD_MS
}
from
"./constants"
;
import
{
RELATIVE_TIME_THRESHOLD_MS
}
from
"./constants"
;
export
interface
MemoViewContextValue
{
export
interface
MemoViewContextValue
{
...
@@ -17,7 +18,7 @@ export interface MemoViewContextValue {
...
@@ -17,7 +18,7 @@ export interface MemoViewContextValue {
blurred
:
boolean
;
blurred
:
boolean
;
openEditor
:
()
=>
void
;
openEditor
:
()
=>
void
;
toggleBlurVisibility
:
()
=>
void
;
toggleBlurVisibility
:
()
=>
void
;
openPreview
:
(
urls
:
string
|
string
[],
index
?:
number
)
=>
void
;
openPreview
:
(
items
:
string
|
string
[]
|
PreviewMediaItem
[],
index
?:
number
)
=>
void
;
}
}
export
const
MemoViewContext
=
createContext
<
MemoViewContextValue
|
null
>
(
null
);
export
const
MemoViewContext
=
createContext
<
MemoViewContextValue
|
null
>
(
null
);
...
...
web/src/components/MemoView/hooks/useImagePreview.ts
View file @
4b4e7194
import
{
useCallback
,
useState
}
from
"react"
;
import
{
useCallback
,
useState
}
from
"react"
;
import
type
{
PreviewMediaItem
}
from
"@/utils/media-item"
;
export
interface
ImagePreviewState
{
export
interface
ImagePreviewState
{
open
:
boolean
;
open
:
boolean
;
urls
:
string
[];
items
:
PreviewMediaItem
[];
index
:
number
;
index
:
number
;
}
}
export
interface
UseImagePreviewReturn
{
export
interface
UseImagePreviewReturn
{
previewState
:
ImagePreviewState
;
previewState
:
ImagePreviewState
;
openPreview
:
(
urls
:
string
|
string
[],
index
?:
number
)
=>
void
;
openPreview
:
(
items
:
string
|
string
[]
|
PreviewMediaItem
[],
index
?:
number
)
=>
void
;
setPreviewOpen
:
(
open
:
boolean
)
=>
void
;
setPreviewOpen
:
(
open
:
boolean
)
=>
void
;
}
}
export
const
useImagePreview
=
():
UseImagePreviewReturn
=>
{
export
const
useImagePreview
=
():
UseImagePreviewReturn
=>
{
const
[
previewState
,
setPreviewState
]
=
useState
<
ImagePreviewState
>
({
open
:
false
,
url
s
:
[],
index
:
0
});
const
[
previewState
,
setPreviewState
]
=
useState
<
ImagePreviewState
>
({
open
:
false
,
item
s
:
[],
index
:
0
});
const
openPreview
=
useCallback
((
urls
:
string
|
string
[],
index
=
0
)
=>
{
const
openPreview
=
useCallback
((
items
:
string
|
string
[]
|
PreviewMediaItem
[],
index
=
0
)
=>
{
setPreviewState
({
open
:
true
,
urls
:
Array
.
isArray
(
urls
)
?
urls
:
[
urls
],
index
});
const
normalizedItems
=
normalizePreviewItems
(
items
);
setPreviewState
({
open
:
true
,
items
:
normalizedItems
,
index
});
},
[]);
},
[]);
const
setPreviewOpen
=
useCallback
((
open
:
boolean
)
=>
{
const
setPreviewOpen
=
useCallback
((
open
:
boolean
)
=>
{
...
@@ -25,3 +27,31 @@ export const useImagePreview = (): UseImagePreviewReturn => {
...
@@ -25,3 +27,31 @@ export const useImagePreview = (): UseImagePreviewReturn => {
return
{
previewState
,
openPreview
,
setPreviewOpen
};
return
{
previewState
,
openPreview
,
setPreviewOpen
};
};
};
function
normalizePreviewItems
(
items
:
string
|
string
[]
|
PreviewMediaItem
[]):
PreviewMediaItem
[]
{
if
(
typeof
items
===
"string"
)
{
return
[
{
id
:
items
,
kind
:
"image"
,
sourceUrl
:
items
,
posterUrl
:
items
,
filename
:
"Image"
,
isMotion
:
false
,
},
];
}
if
(
Array
.
isArray
(
items
)
&&
(
items
.
length
===
0
||
typeof
items
[
0
]
===
"string"
))
{
return
(
items
as
string
[]).
map
((
url
)
=>
({
id
:
url
,
kind
:
"image"
,
sourceUrl
:
url
,
posterUrl
:
url
,
filename
:
"Image"
,
isMotion
:
false
,
}));
}
return
items
as
PreviewMediaItem
[];
}
web/src/components/MemoView/hooks/useMemoHandlers.ts
View file @
4b4e7194
import
{
useCallback
}
from
"react"
;
import
{
useCallback
}
from
"react"
;
import
{
useInstance
}
from
"@/contexts/InstanceContext"
;
import
{
useInstance
}
from
"@/contexts/InstanceContext"
;
import
type
{
PreviewMediaItem
}
from
"@/utils/media-item"
;
interface
UseMemoHandlersOptions
{
interface
UseMemoHandlersOptions
{
readonly
:
boolean
;
readonly
:
boolean
;
openEditor
:
()
=>
void
;
openEditor
:
()
=>
void
;
openPreview
:
(
urls
:
string
|
string
[],
index
?:
number
)
=>
void
;
openPreview
:
(
items
:
string
|
string
[]
|
PreviewMediaItem
[],
index
?:
number
)
=>
void
;
}
}
export
const
useMemoHandlers
=
(
options
:
UseMemoHandlersOptions
)
=>
{
export
const
useMemoHandlers
=
(
options
:
UseMemoHandlersOptions
)
=>
{
...
...
web/src/components/PreviewImageDialog.tsx
View file @
4b4e7194
...
@@ -2,16 +2,21 @@ import { X } from "lucide-react";
...
@@ -2,16 +2,21 @@ import { X } from "lucide-react";
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
}
from
"@/components/ui/dialog"
;
import
{
Dialog
,
DialogContent
}
from
"@/components/ui/dialog"
;
import
type
{
PreviewMediaItem
}
from
"@/utils/media-item"
;
interface
Props
{
interface
Props
{
open
:
boolean
;
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
imgUrls
:
string
[];
imgUrls
?:
string
[];
items
?:
PreviewMediaItem
[];
initialIndex
?:
number
;
initialIndex
?:
number
;
}
}
function
PreviewImageDialog
({
open
,
onOpenChange
,
imgUrls
,
initialIndex
=
0
}:
Props
)
{
function
PreviewImageDialog
({
open
,
onOpenChange
,
imgUrls
=
[],
items
,
initialIndex
=
0
}:
Props
)
{
const
[
currentIndex
,
setCurrentIndex
]
=
useState
(
initialIndex
);
const
[
currentIndex
,
setCurrentIndex
]
=
useState
(
initialIndex
);
const
previewItems
=
items
??
imgUrls
.
map
((
url
)
=>
({
id
:
url
,
kind
:
"image"
as
const
,
sourceUrl
:
url
,
posterUrl
:
url
,
filename
:
"Image"
,
isMotion
:
false
}));
// Update current index when initialIndex prop changes
// Update current index when initialIndex prop changes
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -28,7 +33,7 @@ function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: P
...
@@ -28,7 +33,7 @@ function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: P
onOpenChange
(
false
);
onOpenChange
(
false
);
break
;
break
;
case
"ArrowRight"
:
case
"ArrowRight"
:
setCurrentIndex
((
prev
)
=>
Math
.
min
(
prev
+
1
,
imgUrl
s
.
length
-
1
));
setCurrentIndex
((
prev
)
=>
Math
.
min
(
prev
+
1
,
previewItem
s
.
length
-
1
));
break
;
break
;
case
"ArrowLeft"
:
case
"ArrowLeft"
:
setCurrentIndex
((
prev
)
=>
Math
.
max
(
prev
-
1
,
0
));
setCurrentIndex
((
prev
)
=>
Math
.
max
(
prev
-
1
,
0
));
...
@@ -53,10 +58,11 @@ function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: P
...
@@ -53,10 +58,11 @@ function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: P
};
};
// Return early if no images provided
// Return early if no images provided
if
(
!
imgUrl
s
.
length
)
return
null
;
if
(
!
previewItem
s
.
length
)
return
null
;
// Ensure currentIndex is within bounds
// Ensure currentIndex is within bounds
const
safeIndex
=
Math
.
max
(
0
,
Math
.
min
(
currentIndex
,
imgUrls
.
length
-
1
));
const
safeIndex
=
Math
.
max
(
0
,
Math
.
min
(
currentIndex
,
previewItems
.
length
-
1
));
const
currentItem
=
previewItems
[
safeIndex
];
return
(
return
(
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
...
@@ -79,14 +85,30 @@ function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: P
...
@@ -79,14 +85,30 @@ function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: P
{
/* Image container */
}
{
/* Image container */
}
<
div
className=
"w-full h-full flex items-center justify-center p-4 sm:p-8 overflow-auto"
onClick=
{
handleBackdropClick
}
>
<
div
className=
"w-full h-full flex items-center justify-center p-4 sm:p-8 overflow-auto"
onClick=
{
handleBackdropClick
}
>
{
currentItem
.
kind
===
"video"
?
(
<
video
key=
{
currentItem
.
id
}
src=
{
currentItem
.
sourceUrl
}
poster=
{
currentItem
.
posterUrl
}
className=
"max-w-full max-h-full object-contain"
controls
autoPlay
onLoadedMetadata=
{
(
event
)
=>
{
if
(
currentItem
.
presentationTimestampUs
&&
currentItem
.
presentationTimestampUs
>
0
n
)
{
event
.
currentTarget
.
currentTime
=
Number
(
currentItem
.
presentationTimestampUs
)
/
1
_000_000
;
}
}
}
/>
)
:
(
<
img
<
img
src=
{
imgUrls
[
safeIndex
]
}
src=
{
currentItem
.
sourceUrl
}
alt=
{
`Preview image ${safeIndex + 1} of ${imgUrl
s.length}`
}
alt=
{
`Preview image ${safeIndex + 1} of ${previewItem
s.length}`
}
className=
"max-w-full max-h-full object-contain select-none"
className=
"max-w-full max-h-full object-contain select-none"
draggable=
{
false
}
draggable=
{
false
}
loading=
"eager"
loading=
"eager"
decoding=
"async"
decoding=
"async"
/>
/>
)
}
</
div
>
</
div
>
{
/* Screen reader description */
}
{
/* Screen reader description */
}
...
...
web/src/types/proto/api/v1/attachment_service_pb.ts
View file @
4b4e7194
...
@@ -2,8 +2,8 @@
...
@@ -2,8 +2,8 @@
// @generated from file api/v1/attachment_service.proto (package memos.api.v1, syntax proto3)
// @generated from file api/v1/attachment_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
/* eslint-disable */
import
type
{
GenFile
,
GenMessage
,
GenService
}
from
"@bufbuild/protobuf/codegenv2"
;
import
type
{
Gen
Enum
,
Gen
File
,
GenMessage
,
GenService
}
from
"@bufbuild/protobuf/codegenv2"
;
import
{
fileDesc
,
messageDesc
,
serviceDesc
}
from
"@bufbuild/protobuf/codegenv2"
;
import
{
enumDesc
,
fileDesc
,
messageDesc
,
serviceDesc
}
from
"@bufbuild/protobuf/codegenv2"
;
import
{
file_google_api_annotations
}
from
"../../google/api/annotations_pb"
;
import
{
file_google_api_annotations
}
from
"../../google/api/annotations_pb"
;
import
{
file_google_api_client
}
from
"../../google/api/client_pb"
;
import
{
file_google_api_client
}
from
"../../google/api/client_pb"
;
import
{
file_google_api_field_behavior
}
from
"../../google/api/field_behavior_pb"
;
import
{
file_google_api_field_behavior
}
from
"../../google/api/field_behavior_pb"
;
...
@@ -16,7 +16,44 @@ import type { Message } from "@bufbuild/protobuf";
...
@@ -16,7 +16,44 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file api/v1/attachment_service.proto.
* Describes the file api/v1/attachment_service.proto.
*/
*/
export
const
file_api_v1_attachment_service
:
GenFile
=
/*@__PURE__*/
export
const
file_api_v1_attachment_service
:
GenFile
=
/*@__PURE__*/
fileDesc
(
"Ch9hcGkvdjEvYXR0YWNobWVudF9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEitgIKCkF0dGFjaG1lbnQSEQoEbmFtZRgBIAEoCUID4EEIEjQKC2NyZWF0ZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEhUKCGZpbGVuYW1lGAMgASgJQgPgQQISFAoHY29udGVudBgEIAEoDEID4EEEEhoKDWV4dGVybmFsX2xpbmsYBSABKAlCA+BBARIRCgR0eXBlGAYgASgJQgPgQQISEQoEc2l6ZRgHIAEoA0ID4EEDEhYKBG1lbW8YCCABKAlCA+BBAUgAiAEBOk/qQUwKF21lbW9zLmFwaS52MS9BdHRhY2htZW50EhhhdHRhY2htZW50cy97YXR0YWNobWVudH0qC2F0dGFjaG1lbnRzMgphdHRhY2htZW50QgcKBV9tZW1vImgKF0NyZWF0ZUF0dGFjaG1lbnRSZXF1ZXN0EjEKCmF0dGFjaG1lbnQYASABKAsyGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudEID4EECEhoKDWF0dGFjaG1lbnRfaWQYAiABKAlCA+BBASJ1ChZMaXN0QXR0YWNobWVudHNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIVCghvcmRlcl9ieRgEIAEoCUID4EEBInUKF0xpc3RBdHRhY2htZW50c1Jlc3BvbnNlEi0KC2F0dGFjaG1lbnRzGAEgAygLMhgubWVtb3MuYXBpLnYxLkF0dGFjaG1lbnQSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUiRQoUR2V0QXR0YWNobWVudFJlcXVlc3QSLQoEbmFtZRgBIAEoCUIf4EEC+kEZChdtZW1vcy5hcGkudjEvQXR0YWNobWVudCKCAQoXVXBkYXRlQXR0YWNobWVudFJlcXVlc3QSMQoKYXR0YWNobWVudBgBIAEoCzIYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50QgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIiSAoXRGVsZXRlQXR0YWNobWVudFJlcXVlc3QSLQoEbmFtZRgBIAEoCUIf4EEC+kEZChdtZW1vcy5hcGkudjEvQXR0YWNobWVudDLEBQoRQXR0YWNobWVudFNlcnZpY2USiQEKEENyZWF0ZUF0dGFjaG1lbnQSJS5tZW1vcy5hcGkudjEuQ3JlYXRlQXR0YWNobWVudFJlcXVlc3QaGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudCI02kEKYXR0YWNobWVudILT5JMCIToKYXR0YWNobWVudCITL2FwaS92MS9hdHRhY2htZW50cxJ7Cg9MaXN0QXR0YWNobWVudHMSJC5tZW1vcy5hcGkudjEuTGlzdEF0dGFjaG1lbnRzUmVxdWVzdBolLm1lbW9zLmFwaS52MS5MaXN0QXR0YWNobWVudHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL2F0dGFjaG1lbnRzEnoKDUdldEF0dGFjaG1lbnQSIi5tZW1vcy5hcGkudjEuR2V0QXR0YWNobWVudFJlcXVlc3QaGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudCIr2kEEbmFtZYLT5JMCHhIcL2FwaS92MS97bmFtZT1hdHRhY2htZW50cy8qfRKpAQoQVXBkYXRlQXR0YWNobWVudBIlLm1lbW9zLmFwaS52MS5VcGRhdGVBdHRhY2htZW50UmVxdWVzdBoYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50IlTaQRZhdHRhY2htZW50LHVwZGF0ZV9tYXNrgtPkkwI1OgphdHRhY2htZW50MicvYXBpL3YxL3thdHRhY2htZW50Lm5hbWU9YXR0YWNobWVudHMvKn0SfgoQRGVsZXRlQXR0YWNobWVudBIlLm1lbW9zLmFwaS52MS5EZWxldGVBdHRhY2htZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIr2kEEbmFtZYLT5JMCHiocL2FwaS92MS97bmFtZT1hdHRhY2htZW50cy8qfUKuAQoQY29tLm1lbW9zLmFwaS52MUIWQXR0YWNobWVudFNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z"
,
[
file_google_api_annotations
,
file_google_api_client
,
file_google_api_field_behavior
,
file_google_api_resource
,
file_google_protobuf_empty
,
file_google_protobuf_field_mask
,
file_google_protobuf_timestamp
]);
fileDesc
(
"Ch9hcGkvdjEvYXR0YWNobWVudF9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEivAEKC01vdGlvbk1lZGlhEi8KBmZhbWlseRgBIAEoDjIfLm1lbW9zLmFwaS52MS5Nb3Rpb25NZWRpYUZhbWlseRIrCgRyb2xlGAIgASgOMh0ubWVtb3MuYXBpLnYxLk1vdGlvbk1lZGlhUm9sZRIQCghncm91cF9pZBgDIAEoCRIhChlwcmVzZW50YXRpb25fdGltZXN0YW1wX3VzGAQgASgDEhoKEmhhc19lbWJlZGRlZF92aWRlbxgFIAEoCCLsAgoKQXR0YWNobWVudBIRCgRuYW1lGAEgASgJQgPgQQgSNAoLY3JlYXRlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSFQoIZmlsZW5hbWUYAyABKAlCA+BBAhIUCgdjb250ZW50GAQgASgMQgPgQQQSGgoNZXh0ZXJuYWxfbGluaxgFIAEoCUID4EEBEhEKBHR5cGUYBiABKAlCA+BBAhIRCgRzaXplGAcgASgDQgPgQQMSFgoEbWVtbxgIIAEoCUID4EEBSACIAQESNAoMbW90aW9uX21lZGlhGAkgASgLMhkubWVtb3MuYXBpLnYxLk1vdGlvbk1lZGlhQgPgQQE6T+pBTAoXbWVtb3MuYXBpLnYxL0F0dGFjaG1lbnQSGGF0dGFjaG1lbnRzL3thdHRhY2htZW50fSoLYXR0YWNobWVudHMyCmF0dGFjaG1lbnRCBwoFX21lbW8iaAoXQ3JlYXRlQXR0YWNobWVudFJlcXVlc3QSMQoKYXR0YWNobWVudBgBIAEoCzIYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50QgPgQQISGgoNYXR0YWNobWVudF9pZBgCIAEoCUID4EEBInUKFkxpc3RBdHRhY2htZW50c1JlcXVlc3QSFgoJcGFnZV9zaXplGAEgASgFQgPgQQESFwoKcGFnZV90b2tlbhgCIAEoCUID4EEBEhMKBmZpbHRlchgDIAEoCUID4EEBEhUKCG9yZGVyX2J5GAQgASgJQgPgQQEidQoXTGlzdEF0dGFjaG1lbnRzUmVzcG9uc2USLQoLYXR0YWNobWVudHMYASADKAsyGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEgoKdG90YWxfc2l6ZRgDIAEoBSJFChRHZXRBdHRhY2htZW50UmVxdWVzdBItCgRuYW1lGAEgASgJQh/gQQL6QRkKF21lbW9zLmFwaS52MS9BdHRhY2htZW50IoIBChdVcGRhdGVBdHRhY2htZW50UmVxdWVzdBIxCgphdHRhY2htZW50GAEgASgLMhgubWVtb3MuYXBpLnYxLkF0dGFjaG1lbnRCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAiJIChdEZWxldGVBdHRhY2htZW50UmVxdWVzdBItCgRuYW1lGAEgASgJQh/gQQL6QRkKF21lbW9zLmFwaS52MS9BdHRhY2htZW50IjMKHUJhdGNoRGVsZXRlQXR0YWNobWVudHNSZXF1ZXN0EhIKBW5hbWVzGAEgAygJQgPgQQIqaAoRTW90aW9uTWVkaWFGYW1pbHkSIwofTU9USU9OX01FRElBX0ZBTUlMWV9VTlNQRUNJRklFRBAAEhQKEEFQUExFX0xJVkVfUEhPVE8QARIYChRBTkRST0lEX01PVElPTl9QSE9UTxACKlkKD01vdGlvbk1lZGlhUm9sZRIhCh1NT1RJT05fTUVESUFfUk9MRV9VTlNQRUNJRklFRBAAEgkKBVNUSUxMEAESCQoFVklERU8QAhINCglDT05UQUlORVIQAzLQBgoRQXR0YWNobWVudFNlcnZpY2USiQEKEENyZWF0ZUF0dGFjaG1lbnQSJS5tZW1vcy5hcGkudjEuQ3JlYXRlQXR0YWNobWVudFJlcXVlc3QaGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudCI02kEKYXR0YWNobWVudILT5JMCIToKYXR0YWNobWVudCITL2FwaS92MS9hdHRhY2htZW50cxJ7Cg9MaXN0QXR0YWNobWVudHMSJC5tZW1vcy5hcGkudjEuTGlzdEF0dGFjaG1lbnRzUmVxdWVzdBolLm1lbW9zLmFwaS52MS5MaXN0QXR0YWNobWVudHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL2F0dGFjaG1lbnRzEnoKDUdldEF0dGFjaG1lbnQSIi5tZW1vcy5hcGkudjEuR2V0QXR0YWNobWVudFJlcXVlc3QaGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudCIr2kEEbmFtZYLT5JMCHhIcL2FwaS92MS97bmFtZT1hdHRhY2htZW50cy8qfRKpAQoQVXBkYXRlQXR0YWNobWVudBIlLm1lbW9zLmFwaS52MS5VcGRhdGVBdHRhY2htZW50UmVxdWVzdBoYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50IlTaQRZhdHRhY2htZW50LHVwZGF0ZV9tYXNrgtPkkwI1OgphdHRhY2htZW50MicvYXBpL3YxL3thdHRhY2htZW50Lm5hbWU9YXR0YWNobWVudHMvKn0SfgoQRGVsZXRlQXR0YWNobWVudBIlLm1lbW9zLmFwaS52MS5EZWxldGVBdHRhY2htZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIr2kEEbmFtZYLT5JMCHiocL2FwaS92MS97bmFtZT1hdHRhY2htZW50cy8qfRKJAQoWQmF0Y2hEZWxldGVBdHRhY2htZW50cxIrLm1lbW9zLmFwaS52MS5CYXRjaERlbGV0ZUF0dGFjaG1lbnRzUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIqgtPkkwIkOgEqIh8vYXBpL3YxL2F0dGFjaG1lbnRzOmJhdGNoRGVsZXRlQq4BChBjb20ubWVtb3MuYXBpLnYxQhZBdHRhY2htZW50U2VydmljZVByb3RvUAFaMGdpdGh1Yi5jb20vdXNlbWVtb3MvbWVtb3MvcHJvdG8vZ2VuL2FwaS92MTthcGl2MaICA01BWKoCDE1lbW9zLkFwaS5WMcoCDE1lbW9zXEFwaVxWMeICGE1lbW9zXEFwaVxWMVxHUEJNZXRhZGF0YeoCDk1lbW9zOjpBcGk6OlYxYgZwcm90bzM"
,
[
file_google_api_annotations
,
file_google_api_client
,
file_google_api_field_behavior
,
file_google_api_resource
,
file_google_protobuf_empty
,
file_google_protobuf_field_mask
,
file_google_protobuf_timestamp
]);
/**
* @generated from message memos.api.v1.MotionMedia
*/
export
type
MotionMedia
=
Message
<
"memos.api.v1.MotionMedia"
>
&
{
/**
* @generated from field: memos.api.v1.MotionMediaFamily family = 1;
*/
family
:
MotionMediaFamily
;
/**
* @generated from field: memos.api.v1.MotionMediaRole role = 2;
*/
role
:
MotionMediaRole
;
/**
* @generated from field: string group_id = 3;
*/
groupId
:
string
;
/**
* @generated from field: int64 presentation_timestamp_us = 4;
*/
presentationTimestampUs
:
bigint
;
/**
* @generated from field: bool has_embedded_video = 5;
*/
hasEmbeddedVideo
:
boolean
;
};
/**
* Describes the message memos.api.v1.MotionMedia.
* Use `create(MotionMediaSchema)` to create a new message.
*/
export
const
MotionMediaSchema
:
GenMessage
<
MotionMedia
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
0
);
/**
/**
* @generated from message memos.api.v1.Attachment
* @generated from message memos.api.v1.Attachment
...
@@ -79,6 +116,13 @@ export type Attachment = Message<"memos.api.v1.Attachment"> & {
...
@@ -79,6 +116,13 @@ export type Attachment = Message<"memos.api.v1.Attachment"> & {
* @generated from field: optional string memo = 8;
* @generated from field: optional string memo = 8;
*/
*/
memo
?:
string
;
memo
?:
string
;
/**
* Optional. Motion media metadata.
*
* @generated from field: memos.api.v1.MotionMedia motion_media = 9;
*/
motionMedia
?:
MotionMedia
;
};
};
/**
/**
...
@@ -86,7 +130,7 @@ export type Attachment = Message<"memos.api.v1.Attachment"> & {
...
@@ -86,7 +130,7 @@ export type Attachment = Message<"memos.api.v1.Attachment"> & {
* Use `create(AttachmentSchema)` to create a new message.
* Use `create(AttachmentSchema)` to create a new message.
*/
*/
export
const
AttachmentSchema
:
GenMessage
<
Attachment
>
=
/*@__PURE__*/
export
const
AttachmentSchema
:
GenMessage
<
Attachment
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
0
);
messageDesc
(
file_api_v1_attachment_service
,
1
);
/**
/**
* @generated from message memos.api.v1.CreateAttachmentRequest
* @generated from message memos.api.v1.CreateAttachmentRequest
...
@@ -113,7 +157,7 @@ export type CreateAttachmentRequest = Message<"memos.api.v1.CreateAttachmentRequ
...
@@ -113,7 +157,7 @@ export type CreateAttachmentRequest = Message<"memos.api.v1.CreateAttachmentRequ
* Use `create(CreateAttachmentRequestSchema)` to create a new message.
* Use `create(CreateAttachmentRequestSchema)` to create a new message.
*/
*/
export
const
CreateAttachmentRequestSchema
:
GenMessage
<
CreateAttachmentRequest
>
=
/*@__PURE__*/
export
const
CreateAttachmentRequestSchema
:
GenMessage
<
CreateAttachmentRequest
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
1
);
messageDesc
(
file_api_v1_attachment_service
,
2
);
/**
/**
* @generated from message memos.api.v1.ListAttachmentsRequest
* @generated from message memos.api.v1.ListAttachmentsRequest
...
@@ -161,7 +205,7 @@ export type ListAttachmentsRequest = Message<"memos.api.v1.ListAttachmentsReques
...
@@ -161,7 +205,7 @@ export type ListAttachmentsRequest = Message<"memos.api.v1.ListAttachmentsReques
* Use `create(ListAttachmentsRequestSchema)` to create a new message.
* Use `create(ListAttachmentsRequestSchema)` to create a new message.
*/
*/
export
const
ListAttachmentsRequestSchema
:
GenMessage
<
ListAttachmentsRequest
>
=
/*@__PURE__*/
export
const
ListAttachmentsRequestSchema
:
GenMessage
<
ListAttachmentsRequest
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
2
);
messageDesc
(
file_api_v1_attachment_service
,
3
);
/**
/**
* @generated from message memos.api.v1.ListAttachmentsResponse
* @generated from message memos.api.v1.ListAttachmentsResponse
...
@@ -195,7 +239,7 @@ export type ListAttachmentsResponse = Message<"memos.api.v1.ListAttachmentsRespo
...
@@ -195,7 +239,7 @@ export type ListAttachmentsResponse = Message<"memos.api.v1.ListAttachmentsRespo
* Use `create(ListAttachmentsResponseSchema)` to create a new message.
* Use `create(ListAttachmentsResponseSchema)` to create a new message.
*/
*/
export
const
ListAttachmentsResponseSchema
:
GenMessage
<
ListAttachmentsResponse
>
=
/*@__PURE__*/
export
const
ListAttachmentsResponseSchema
:
GenMessage
<
ListAttachmentsResponse
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
3
);
messageDesc
(
file_api_v1_attachment_service
,
4
);
/**
/**
* @generated from message memos.api.v1.GetAttachmentRequest
* @generated from message memos.api.v1.GetAttachmentRequest
...
@@ -215,7 +259,7 @@ export type GetAttachmentRequest = Message<"memos.api.v1.GetAttachmentRequest">
...
@@ -215,7 +259,7 @@ export type GetAttachmentRequest = Message<"memos.api.v1.GetAttachmentRequest">
* Use `create(GetAttachmentRequestSchema)` to create a new message.
* Use `create(GetAttachmentRequestSchema)` to create a new message.
*/
*/
export
const
GetAttachmentRequestSchema
:
GenMessage
<
GetAttachmentRequest
>
=
/*@__PURE__*/
export
const
GetAttachmentRequestSchema
:
GenMessage
<
GetAttachmentRequest
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
4
);
messageDesc
(
file_api_v1_attachment_service
,
5
);
/**
/**
* @generated from message memos.api.v1.UpdateAttachmentRequest
* @generated from message memos.api.v1.UpdateAttachmentRequest
...
@@ -241,7 +285,7 @@ export type UpdateAttachmentRequest = Message<"memos.api.v1.UpdateAttachmentRequ
...
@@ -241,7 +285,7 @@ export type UpdateAttachmentRequest = Message<"memos.api.v1.UpdateAttachmentRequ
* Use `create(UpdateAttachmentRequestSchema)` to create a new message.
* Use `create(UpdateAttachmentRequestSchema)` to create a new message.
*/
*/
export
const
UpdateAttachmentRequestSchema
:
GenMessage
<
UpdateAttachmentRequest
>
=
/*@__PURE__*/
export
const
UpdateAttachmentRequestSchema
:
GenMessage
<
UpdateAttachmentRequest
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
5
);
messageDesc
(
file_api_v1_attachment_service
,
6
);
/**
/**
* @generated from message memos.api.v1.DeleteAttachmentRequest
* @generated from message memos.api.v1.DeleteAttachmentRequest
...
@@ -261,7 +305,81 @@ export type DeleteAttachmentRequest = Message<"memos.api.v1.DeleteAttachmentRequ
...
@@ -261,7 +305,81 @@ export type DeleteAttachmentRequest = Message<"memos.api.v1.DeleteAttachmentRequ
* Use `create(DeleteAttachmentRequestSchema)` to create a new message.
* Use `create(DeleteAttachmentRequestSchema)` to create a new message.
*/
*/
export
const
DeleteAttachmentRequestSchema
:
GenMessage
<
DeleteAttachmentRequest
>
=
/*@__PURE__*/
export
const
DeleteAttachmentRequestSchema
:
GenMessage
<
DeleteAttachmentRequest
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
6
);
messageDesc
(
file_api_v1_attachment_service
,
7
);
/**
* @generated from message memos.api.v1.BatchDeleteAttachmentsRequest
*/
export
type
BatchDeleteAttachmentsRequest
=
Message
<
"memos.api.v1.BatchDeleteAttachmentsRequest"
>
&
{
/**
* @generated from field: repeated string names = 1;
*/
names
:
string
[];
};
/**
* Describes the message memos.api.v1.BatchDeleteAttachmentsRequest.
* Use `create(BatchDeleteAttachmentsRequestSchema)` to create a new message.
*/
export
const
BatchDeleteAttachmentsRequestSchema
:
GenMessage
<
BatchDeleteAttachmentsRequest
>
=
/*@__PURE__*/
messageDesc
(
file_api_v1_attachment_service
,
8
);
/**
* @generated from enum memos.api.v1.MotionMediaFamily
*/
export
enum
MotionMediaFamily
{
/**
* @generated from enum value: MOTION_MEDIA_FAMILY_UNSPECIFIED = 0;
*/
MOTION_MEDIA_FAMILY_UNSPECIFIED
=
0
,
/**
* @generated from enum value: APPLE_LIVE_PHOTO = 1;
*/
APPLE_LIVE_PHOTO
=
1
,
/**
* @generated from enum value: ANDROID_MOTION_PHOTO = 2;
*/
ANDROID_MOTION_PHOTO
=
2
,
}
/**
* Describes the enum memos.api.v1.MotionMediaFamily.
*/
export
const
MotionMediaFamilySchema
:
GenEnum
<
MotionMediaFamily
>
=
/*@__PURE__*/
enumDesc
(
file_api_v1_attachment_service
,
0
);
/**
* @generated from enum memos.api.v1.MotionMediaRole
*/
export
enum
MotionMediaRole
{
/**
* @generated from enum value: MOTION_MEDIA_ROLE_UNSPECIFIED = 0;
*/
MOTION_MEDIA_ROLE_UNSPECIFIED
=
0
,
/**
* @generated from enum value: STILL = 1;
*/
STILL
=
1
,
/**
* @generated from enum value: VIDEO = 2;
*/
VIDEO
=
2
,
/**
* @generated from enum value: CONTAINER = 3;
*/
CONTAINER
=
3
,
}
/**
* Describes the enum memos.api.v1.MotionMediaRole.
*/
export
const
MotionMediaRoleSchema
:
GenEnum
<
MotionMediaRole
>
=
/*@__PURE__*/
enumDesc
(
file_api_v1_attachment_service
,
1
);
/**
/**
* @generated from service memos.api.v1.AttachmentService
* @generated from service memos.api.v1.AttachmentService
...
@@ -317,6 +435,16 @@ export const AttachmentService: GenService<{
...
@@ -317,6 +435,16 @@ export const AttachmentService: GenService<{
input
:
typeof
DeleteAttachmentRequestSchema
;
input
:
typeof
DeleteAttachmentRequestSchema
;
output
:
typeof
EmptySchema
;
output
:
typeof
EmptySchema
;
},
},
/**
* BatchDeleteAttachments deletes multiple attachments in one request.
*
* @generated from rpc memos.api.v1.AttachmentService.BatchDeleteAttachments
*/
batchDeleteAttachments
:
{
methodKind
:
"unary"
;
input
:
typeof
BatchDeleteAttachmentsRequestSchema
;
output
:
typeof
EmptySchema
;
},
}
>
=
/*@__PURE__*/
}
>
=
/*@__PURE__*/
serviceDesc
(
file_api_v1_attachment_service
,
0
);
serviceDesc
(
file_api_v1_attachment_service
,
0
);
web/src/utils/attachment.ts
View file @
4b4e7194
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
Attachment
,
MotionMediaFamily
,
MotionMediaRole
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
export
const
getAttachmentUrl
=
(
attachment
:
Attachment
)
=>
{
export
const
getAttachmentUrl
=
(
attachment
:
Attachment
)
=>
{
if
(
attachment
.
externalLink
)
{
if
(
attachment
.
externalLink
)
{
...
@@ -12,6 +12,10 @@ export const getAttachmentThumbnailUrl = (attachment: Attachment) => {
...
@@ -12,6 +12,10 @@ export const getAttachmentThumbnailUrl = (attachment: Attachment) => {
return
`
${
window
.
location
.
origin
}
/file/
${
attachment
.
name
}
/
${
attachment
.
filename
}
?thumbnail=true`
;
return
`
${
window
.
location
.
origin
}
/file/
${
attachment
.
name
}
/
${
attachment
.
filename
}
?thumbnail=true`
;
};
};
export
const
getAttachmentMotionClipUrl
=
(
attachment
:
Attachment
)
=>
{
return
`
${
window
.
location
.
origin
}
/file/
${
attachment
.
name
}
/
${
attachment
.
filename
}
?motion=true`
;
};
export
const
getAttachmentType
=
(
attachment
:
Attachment
)
=>
{
export
const
getAttachmentType
=
(
attachment
:
Attachment
)
=>
{
if
(
isImage
(
attachment
.
type
))
{
if
(
isImage
(
attachment
.
type
))
{
return
"image/*"
;
return
"image/*"
;
...
@@ -52,3 +56,21 @@ export const isMidiFile = (mimeType: string): boolean => {
...
@@ -52,3 +56,21 @@ export const isMidiFile = (mimeType: string): boolean => {
const
isPSD
=
(
t
:
string
)
=>
{
const
isPSD
=
(
t
:
string
)
=>
{
return
t
===
"image/vnd.adobe.photoshop"
||
t
===
"image/x-photoshop"
||
t
===
"image/photoshop"
;
return
t
===
"image/vnd.adobe.photoshop"
||
t
===
"image/x-photoshop"
||
t
===
"image/photoshop"
;
};
};
export
const
getAttachmentMotionGroupId
=
(
attachment
:
Attachment
):
string
|
undefined
=>
{
return
attachment
.
motionMedia
?.
groupId
||
undefined
;
};
export
const
isAppleLivePhotoStill
=
(
attachment
:
Attachment
):
boolean
=>
attachment
.
motionMedia
?.
family
===
MotionMediaFamily
.
APPLE_LIVE_PHOTO
&&
attachment
.
motionMedia
.
role
===
MotionMediaRole
.
STILL
;
export
const
isAppleLivePhotoVideo
=
(
attachment
:
Attachment
):
boolean
=>
attachment
.
motionMedia
?.
family
===
MotionMediaFamily
.
APPLE_LIVE_PHOTO
&&
attachment
.
motionMedia
.
role
===
MotionMediaRole
.
VIDEO
;
export
const
isAndroidMotionContainer
=
(
attachment
:
Attachment
):
boolean
=>
attachment
.
motionMedia
?.
family
===
MotionMediaFamily
.
ANDROID_MOTION_PHOTO
&&
attachment
.
motionMedia
.
role
===
MotionMediaRole
.
CONTAINER
&&
attachment
.
motionMedia
.
hasEmbeddedVideo
;
export
const
isMotionAttachment
=
(
attachment
:
Attachment
):
boolean
=>
isAppleLivePhotoStill
(
attachment
)
||
isAppleLivePhotoVideo
(
attachment
)
||
isAndroidMotionContainer
(
attachment
);
web/src/utils/media-item.ts
0 → 100644
View file @
4b4e7194
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
getAttachmentMotionClipUrl
,
getAttachmentMotionGroupId
,
getAttachmentThumbnailUrl
,
getAttachmentType
,
getAttachmentUrl
,
isAndroidMotionContainer
,
isAppleLivePhotoStill
,
isAppleLivePhotoVideo
,
isMotionAttachment
,
}
from
"./attachment"
;
export
interface
PreviewMediaItem
{
id
:
string
;
kind
:
"image"
|
"video"
;
sourceUrl
:
string
;
posterUrl
?:
string
;
filename
:
string
;
isMotion
:
boolean
;
presentationTimestampUs
?:
bigint
;
}
export
interface
AttachmentVisualItem
{
id
:
string
;
kind
:
"image"
|
"video"
|
"motion"
;
filename
:
string
;
posterUrl
:
string
;
sourceUrl
:
string
;
attachmentNames
:
string
[];
attachments
:
Attachment
[];
previewItem
:
PreviewMediaItem
;
mimeType
:
string
;
}
export
function
buildAttachmentVisualItems
(
attachments
:
Attachment
[]):
AttachmentVisualItem
[]
{
const
attachmentsByGroup
=
new
Map
<
string
,
Attachment
[]
>
();
for
(
const
attachment
of
attachments
)
{
const
groupId
=
getAttachmentMotionGroupId
(
attachment
);
if
(
!
groupId
)
{
continue
;
}
const
group
=
attachmentsByGroup
.
get
(
groupId
)
??
[];
group
.
push
(
attachment
);
attachmentsByGroup
.
set
(
groupId
,
group
);
}
const
consumedGroups
=
new
Set
<
string
>
();
const
items
:
AttachmentVisualItem
[]
=
[];
for
(
const
attachment
of
attachments
)
{
if
(
isAndroidMotionContainer
(
attachment
))
{
items
.
push
(
buildAndroidMotionItem
(
attachment
));
continue
;
}
const
groupId
=
getAttachmentMotionGroupId
(
attachment
);
if
(
!
groupId
||
consumedGroups
.
has
(
groupId
))
{
if
(
!
groupId
)
{
items
.
push
(
buildSingleAttachmentItem
(
attachment
));
}
continue
;
}
const
group
=
attachmentsByGroup
.
get
(
groupId
)
??
[];
const
still
=
group
.
find
(
isAppleLivePhotoStill
);
const
video
=
group
.
find
(
isAppleLivePhotoVideo
);
if
(
still
&&
video
&&
group
.
length
===
2
)
{
items
.
push
(
buildAppleMotionItem
(
still
,
video
));
consumedGroups
.
add
(
groupId
);
continue
;
}
items
.
push
(
buildSingleAttachmentItem
(
attachment
));
consumedGroups
.
add
(
groupId
);
for
(
const
member
of
group
)
{
if
(
member
.
name
===
attachment
.
name
)
{
continue
;
}
items
.
push
(
buildSingleAttachmentItem
(
member
));
}
}
return
dedupeVisualItems
(
items
);
}
export
function
countLogicalAttachmentItems
(
attachments
:
Attachment
[]):
number
{
const
visualAttachments
=
attachments
.
filter
(
(
attachment
)
=>
getAttachmentType
(
attachment
)
===
"image/*"
||
getAttachmentType
(
attachment
)
===
"video/*"
||
isMotionAttachment
(
attachment
),
);
const
visualNames
=
new
Set
(
visualAttachments
.
map
((
attachment
)
=>
attachment
.
name
));
const
visualCount
=
buildAttachmentVisualItems
(
visualAttachments
).
length
;
const
nonVisualCount
=
attachments
.
filter
((
attachment
)
=>
!
visualNames
.
has
(
attachment
.
name
)).
length
;
return
visualCount
+
nonVisualCount
;
}
function
buildSingleAttachmentItem
(
attachment
:
Attachment
):
AttachmentVisualItem
{
const
attachmentType
=
getAttachmentType
(
attachment
);
const
sourceUrl
=
getAttachmentUrl
(
attachment
);
const
posterUrl
=
attachmentType
===
"image/*"
?
getAttachmentThumbnailUrl
(
attachment
)
:
sourceUrl
;
const
previewKind
=
attachmentType
===
"video/*"
?
"video"
:
"image"
;
return
{
id
:
attachment
.
name
,
kind
:
attachmentType
===
"video/*"
?
"video"
:
"image"
,
filename
:
attachment
.
filename
,
posterUrl
,
sourceUrl
,
attachmentNames
:
[
attachment
.
name
],
attachments
:
[
attachment
],
previewItem
:
{
id
:
attachment
.
name
,
kind
:
previewKind
,
sourceUrl
,
posterUrl
,
filename
:
attachment
.
filename
,
isMotion
:
false
,
},
mimeType
:
attachment
.
type
,
};
}
function
buildAppleMotionItem
(
still
:
Attachment
,
video
:
Attachment
):
AttachmentVisualItem
{
const
sourceUrl
=
getAttachmentUrl
(
video
);
const
posterUrl
=
getAttachmentThumbnailUrl
(
still
);
return
{
id
:
getAttachmentMotionGroupId
(
still
)
??
still
.
name
,
kind
:
"motion"
,
filename
:
still
.
filename
,
posterUrl
,
sourceUrl
,
attachmentNames
:
[
still
.
name
,
video
.
name
],
attachments
:
[
still
,
video
],
previewItem
:
{
id
:
getAttachmentMotionGroupId
(
still
)
??
still
.
name
,
kind
:
"video"
,
sourceUrl
,
posterUrl
,
filename
:
still
.
filename
,
isMotion
:
true
,
},
mimeType
:
still
.
type
,
};
}
function
buildAndroidMotionItem
(
attachment
:
Attachment
):
AttachmentVisualItem
{
return
{
id
:
attachment
.
name
,
kind
:
"motion"
,
filename
:
attachment
.
filename
,
posterUrl
:
getAttachmentThumbnailUrl
(
attachment
),
sourceUrl
:
getAttachmentMotionClipUrl
(
attachment
),
attachmentNames
:
[
attachment
.
name
],
attachments
:
[
attachment
],
previewItem
:
{
id
:
attachment
.
name
,
kind
:
"video"
,
sourceUrl
:
getAttachmentMotionClipUrl
(
attachment
),
posterUrl
:
getAttachmentThumbnailUrl
(
attachment
),
filename
:
attachment
.
filename
,
isMotion
:
true
,
presentationTimestampUs
:
attachment
.
motionMedia
?.
presentationTimestampUs
,
},
mimeType
:
attachment
.
type
,
};
}
function
dedupeVisualItems
(
items
:
AttachmentVisualItem
[]):
AttachmentVisualItem
[]
{
const
seen
=
new
Set
<
string
>
();
return
items
.
filter
((
item
)
=>
{
if
(
seen
.
has
(
item
.
id
))
{
return
false
;
}
seen
.
add
(
item
.
id
);
return
true
;
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment