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
0e4d2d25
Commit
0e4d2d25
authored
Apr 02, 2026
by
memoclaw
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: simplify audio attachment playback component
parent
9676e725
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
25 additions
and
58 deletions
+25
-58
VoiceRecorderPanel.tsx
...c/components/MemoEditor/components/VoiceRecorderPanel.tsx
+1
-1
AttachmentListEditor.tsx
...mponents/MemoMetadata/Attachment/AttachmentListEditor.tsx
+1
-29
AttachmentListView.tsx
...components/MemoMetadata/Attachment/AttachmentListView.tsx
+7
-1
AudioAttachmentItem.tsx
...omponents/MemoMetadata/Attachment/AudioAttachmentItem.tsx
+16
-27
No files found.
web/src/components/MemoEditor/components/VoiceRecorderPanel.tsx
View file @
0e4d2d25
...
@@ -87,10 +87,10 @@ export const VoiceRecorderPanel: FC<VoiceRecorderPanelProps> = ({
...
@@ -87,10 +87,10 @@ export const VoiceRecorderPanel: FC<VoiceRecorderPanelProps> = ({
<
div
className=
"mt-3"
>
<
div
className=
"mt-3"
>
<
AudioAttachmentItem
<
AudioAttachmentItem
filename=
{
recording
.
localFile
.
file
.
name
}
filename=
{
recording
.
localFile
.
file
.
name
}
displayName=
"Voice note"
sourceUrl=
{
recording
.
localFile
.
previewUrl
}
sourceUrl=
{
recording
.
localFile
.
previewUrl
}
mimeType=
{
recording
.
mimeType
}
mimeType=
{
recording
.
mimeType
}
size=
{
recording
.
localFile
.
file
.
size
}
size=
{
recording
.
localFile
.
file
.
size
}
title=
"Voice note"
/>
/>
</
div
>
</
div
>
)
}
)
}
...
...
web/src/components/MemoMetadata/Attachment/AttachmentListEditor.tsx
View file @
0e4d2d25
...
@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils";
...
@@ -6,7 +6,6 @@ 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
{
formatFileSize
,
getFileTypeLabel
}
from
"@/utils/format"
;
import
{
formatFileSize
,
getFileTypeLabel
}
from
"@/utils/format"
;
import
SectionHeader
from
"../SectionHeader"
;
import
SectionHeader
from
"../SectionHeader"
;
import
AudioAttachmentItem
from
"./AudioAttachmentItem"
;
interface
AttachmentListEditorProps
{
interface
AttachmentListEditorProps
{
attachments
:
Attachment
[];
attachments
:
Attachment
[];
...
@@ -23,38 +22,11 @@ const AttachmentItemCard: FC<{
...
@@ -23,38 +22,11 @@ const AttachmentItemCard: FC<{
canMoveUp
?:
boolean
;
canMoveUp
?:
boolean
;
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
,
sourceUrl
}
=
item
;
const
{
category
,
filename
,
thumbnailUrl
,
mimeType
,
size
}
=
item
;
const
fileTypeLabel
=
getFileTypeLabel
(
mimeType
);
const
fileTypeLabel
=
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
;
if
(
category
===
"audio"
)
{
return
(
<
div
className=
"rounded border border-transparent transition-all hover:border-border hover:bg-accent/20"
>
<
AudioAttachmentItem
filename=
{
filename
}
displayName=
{
displayName
}
sourceUrl=
{
sourceUrl
}
mimeType=
{
mimeType
}
size=
{
size
}
actionSlot=
{
onRemove
?
(
<
button
type=
"button"
onClick=
{
onRemove
}
className=
"inline-flex size-6.5 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
title=
"Remove"
aria
-
label=
"Remove attachment"
>
<
XIcon
className=
"h-3 w-3"
/>
</
button
>
)
:
undefined
}
/>
</
div
>
);
}
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"
>
...
...
web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx
View file @
0e4d2d25
...
@@ -147,7 +147,13 @@ const VisualSection = ({ attachments, onImageClick }: { attachments: Attachment[
...
@@ -147,7 +147,13 @@ const VisualSection = ({ attachments, onImageClick }: { attachments: Attachment[
const
AudioList
=
({
attachments
}:
{
attachments
:
Attachment
[]
})
=>
(
const
AudioList
=
({
attachments
}:
{
attachments
:
Attachment
[]
})
=>
(
<
div
className=
"flex flex-col gap-2"
>
<
div
className=
"flex flex-col gap-2"
>
{
attachments
.
map
((
attachment
)
=>
(
{
attachments
.
map
((
attachment
)
=>
(
<
AudioAttachmentItem
key=
{
attachment
.
name
}
attachment=
{
attachment
}
/>
<
AudioAttachmentItem
key=
{
attachment
.
name
}
filename=
{
attachment
.
filename
}
sourceUrl=
{
getAttachmentUrl
(
attachment
)
}
mimeType=
{
attachment
.
type
}
size=
{
Number
(
attachment
.
size
)
}
/>
))
}
))
}
</
div
>
</
div
>
);
);
...
...
web/src/components/MemoMetadata/Attachment/AudioAttachmentItem.tsx
View file @
0e4d2d25
import
{
FileAudioIcon
,
PauseIcon
,
PlayIcon
}
from
"lucide-react"
;
import
{
FileAudioIcon
,
PauseIcon
,
PlayIcon
}
from
"lucide-react"
;
import
{
type
ReactNode
,
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service_pb"
;
import
{
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
{
formatFileSize
,
getFileTypeLabel
}
from
"@/utils/format"
;
import
{
formatFileSize
,
getFileTypeLabel
}
from
"@/utils/format"
;
import
{
formatAudioTime
,
getAttachmentMetadata
}
from
"./attachmentViewHelpers"
;
import
{
formatAudioTime
}
from
"./attachmentViewHelpers"
;
const
AUDIO_PLAYBACK_RATES
=
[
1
,
1.5
,
2
]
as
const
;
const
AUDIO_PLAYBACK_RATES
=
[
1
,
1.5
,
2
]
as
const
;
...
@@ -47,30 +45,22 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on
...
@@ -47,30 +45,22 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on
);
);
interface
AudioAttachmentItemProps
{
interface
AudioAttachmentItemProps
{
attachment
?:
Attachment
;
filename
:
string
;
filename
?:
string
;
sourceUrl
:
string
;
displayName
?:
string
;
mimeType
:
string
;
sourceUrl
?:
string
;
mimeType
?:
string
;
size
?:
number
;
size
?:
number
;
actionSlot
?:
ReactNode
;
title
?:
string
;
}
}
const
AudioAttachmentItem
=
({
attachment
,
filename
,
displayName
,
sourceUrl
,
mimeType
,
size
,
actionSlot
}:
AudioAttachmentItemProps
)
=>
{
const
AudioAttachmentItem
=
({
filename
,
sourceUrl
,
mimeType
,
size
,
title
}:
AudioAttachmentItemProps
)
=>
{
const
resolvedFilename
=
attachment
?.
filename
??
filename
??
"audio"
;
const
resolvedDisplayName
=
displayName
??
resolvedFilename
;
const
resolvedSourceUrl
=
attachment
?
getAttachmentUrl
(
attachment
)
:
(
sourceUrl
??
""
);
const
audioRef
=
useRef
<
HTMLAudioElement
>
(
null
);
const
audioRef
=
useRef
<
HTMLAudioElement
>
(
null
);
const
[
isPlaying
,
setIsPlaying
]
=
useState
(
false
);
const
[
isPlaying
,
setIsPlaying
]
=
useState
(
false
);
const
[
currentTime
,
setCurrentTime
]
=
useState
(
0
);
const
[
currentTime
,
setCurrentTime
]
=
useState
(
0
);
const
[
duration
,
setDuration
]
=
useState
(
0
);
const
[
duration
,
setDuration
]
=
useState
(
0
);
const
[
playbackRate
,
setPlaybackRate
]
=
useState
<
(
typeof
AUDIO_PLAYBACK_RATES
)[
number
]
>
(
1
);
const
[
playbackRate
,
setPlaybackRate
]
=
useState
<
(
typeof
AUDIO_PLAYBACK_RATES
)[
number
]
>
(
1
);
const
{
fileTypeLabel
,
fileSizeLabel
}
=
attachment
const
displayTitle
=
title
??
filename
;
?
getAttachmentMetadata
(
attachment
)
const
fileTypeLabel
=
getFileTypeLabel
(
mimeType
);
:
{
const
fileSizeLabel
=
size
?
formatFileSize
(
size
)
:
undefined
;
fileTypeLabel
:
getFileTypeLabel
(
mimeType
??
""
),
fileSizeLabel
:
size
?
formatFileSize
(
size
)
:
undefined
,
};
const
progressPercent
=
duration
>
0
?
(
currentTime
/
duration
)
*
100
:
0
;
const
progressPercent
=
duration
>
0
?
(
currentTime
/
duration
)
*
100
:
0
;
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -131,8 +121,8 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
...
@@ -131,8 +121,8 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
<
div
className=
"flex min-w-0 flex-1 items-start justify-between gap-3"
>
<
div
className=
"flex min-w-0 flex-1 items-start justify-between gap-3"
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"truncate text-sm font-medium leading-5 text-foreground"
title=
{
resolvedF
ilename
}
>
<
div
className=
"truncate text-sm font-medium leading-5 text-foreground"
title=
{
f
ilename
}
>
{
resolvedDisplayNam
e
}
{
displayTitl
e
}
</
div
>
</
div
>
<
div
className=
"flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs leading-4 text-muted-foreground"
>
<
div
className=
"flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs leading-4 text-muted-foreground"
>
<
span
>
{
fileTypeLabel
}
</
span
>
<
span
>
{
fileTypeLabel
}
</
span
>
...
@@ -146,12 +136,11 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
...
@@ -146,12 +136,11 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
</
div
>
</
div
>
<
div
className=
"mt-0.5 flex shrink-0 items-center gap-1"
>
<
div
className=
"mt-0.5 flex shrink-0 items-center gap-1"
>
{
actionSlot
}
<
button
<
button
type=
"button"
type=
"button"
onClick=
{
handlePlaybackRateChange
}
onClick=
{
handlePlaybackRateChange
}
className=
"inline-flex h-6 items-center justify-center px-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
className=
"inline-flex h-6 items-center justify-center px-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
aria
-
label=
{
`Playback speed ${playbackRate}x for ${
resolvedDisplayNam
e}`
}
aria
-
label=
{
`Playback speed ${playbackRate}x for ${
displayTitl
e}`
}
>
>
{
playbackRate
}
x
{
playbackRate
}
x
</
button
>
</
button
>
...
@@ -159,7 +148,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
...
@@ -159,7 +148,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
type=
"button"
type=
"button"
onClick=
{
togglePlayback
}
onClick=
{
togglePlayback
}
className=
"inline-flex size-6.5 items-center justify-center rounded-md border border-border/45 bg-background/85 text-foreground transition-colors hover:bg-muted/45"
className=
"inline-flex size-6.5 items-center justify-center rounded-md border border-border/45 bg-background/85 text-foreground transition-colors hover:bg-muted/45"
aria
-
label=
{
isPlaying
?
`Pause ${
resolvedDisplayName}`
:
`Play ${resolvedDisplayNam
e}`
}
aria
-
label=
{
isPlaying
?
`Pause ${
displayTitle}`
:
`Play ${displayTitl
e}`
}
>
>
{
isPlaying
?
<
PauseIcon
className=
"size-3"
/>
:
<
PlayIcon
className=
"size-3 translate-x-[0.5px]"
/>
}
{
isPlaying
?
<
PauseIcon
className=
"size-3"
/>
:
<
PlayIcon
className=
"size-3 translate-x-[0.5px]"
/>
}
</
button
>
</
button
>
...
@@ -168,7 +157,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
...
@@ -168,7 +157,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
</
div
>
</
div
>
<
AudioProgressBar
<
AudioProgressBar
filename=
{
resolvedF
ilename
}
filename=
{
f
ilename
}
currentTime=
{
currentTime
}
currentTime=
{
currentTime
}
duration=
{
duration
}
duration=
{
duration
}
progressPercent=
{
progressPercent
}
progressPercent=
{
progressPercent
}
...
@@ -177,7 +166,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
...
@@ -177,7 +166,7 @@ const AudioAttachmentItem = ({ attachment, filename, displayName, sourceUrl, mim
<
audio
<
audio
ref=
{
audioRef
}
ref=
{
audioRef
}
src=
{
resolvedS
ourceUrl
}
src=
{
s
ourceUrl
}
preload=
"metadata"
preload=
"metadata"
className=
"hidden"
className=
"hidden"
onLoadedMetadata=
{
(
e
)
=>
handleDuration
(
e
.
currentTarget
.
duration
)
}
onLoadedMetadata=
{
(
e
)
=>
handleDuration
(
e
.
currentTarget
.
duration
)
}
...
...
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