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
2d682ae1
Commit
2d682ae1
authored
Apr 07, 2026
by
boojack
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(attachments): compact audio item layout and playback logic
Made-with: Cursor
parent
5b78023f
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
60 additions
and
61 deletions
+60
-61
AudioAttachmentItem.tsx
...omponents/MemoMetadata/Attachment/AudioAttachmentItem.tsx
+60
-61
No files found.
web/src/components/MemoMetadata/Attachment/AudioAttachmentItem.tsx
View file @
2d682ae1
import
{
FileAudioIcon
,
PauseIcon
,
PlayIcon
}
from
"lucide-react"
;
import
{
PauseIcon
,
PlayIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
formatFileSize
,
getFileTypeLabel
}
from
"@/utils/format"
;
import
{
formatAudioTime
}
from
"./attachmentHelpers"
;
const
AUDIO_PLAYBACK_RATES
=
[
1
,
1.5
,
2
]
as
const
;
const
UNKNOWN_DURATION_LABEL
=
"--:--"
;
const
getDurationLabel
=
(
duration
:
number
):
string
=>
(
duration
>
0
?
formatAudioTime
(
duration
)
:
UNKNOWN_DURATION_LABEL
);
const
getNextPlaybackRate
=
(
currentRate
:
(
typeof
AUDIO_PLAYBACK_RATES
)[
number
]):
(
typeof
AUDIO_PLAYBACK_RATES
)[
number
]
=>
{
const
currentRateIndex
=
AUDIO_PLAYBACK_RATES
.
findIndex
((
rate
)
=>
rate
===
currentRate
);
return
AUDIO_PLAYBACK_RATES
[(
currentRateIndex
+
1
)
%
AUDIO_PLAYBACK_RATES
.
length
];
};
interface
AudioProgressBarProps
{
filename
:
string
;
currentTime
:
number
;
duration
:
number
;
progressPercent
:
number
;
onSeek
:
(
value
:
string
)
=>
void
;
onSeek
:
(
value
:
number
)
=>
void
;
className
?:
string
;
}
const
AudioProgressBar
=
({
filename
,
currentTime
,
duration
,
progressPercent
,
onSeek
}:
AudioProgressBarProps
)
=>
(
<
div
className=
"mt-2 flex items-center gap-2.5"
>
<
div
className=
"relative flex h-
4
min-w-0 flex-1 items-center"
>
const
AudioProgressBar
=
({
filename
,
currentTime
,
duration
,
progressPercent
,
onSeek
,
className
}:
AudioProgressBarProps
)
=>
(
<
div
className=
{
`flex items-center gap-2 ${className ?? ""}`
}
>
<
div
className=
"relative flex h-
3.5
min-w-0 flex-1 items-center"
>
<
div
className=
"absolute inset-x-0 h-1 rounded-full bg-muted/75"
/>
<
div
className=
"absolute left-0 h-1 rounded-full bg-foreground/20"
style=
{
{
width
:
`${Math.min(progressPercent, 100)}%`
}
}
/>
<
input
...
...
@@ -24,12 +33,12 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on
max=
{
duration
||
1
}
step=
{
0.1
}
value=
{
Math
.
min
(
currentTime
,
duration
||
0
)
}
onChange=
{
(
e
)
=>
onSeek
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
onSeek
(
Number
(
e
.
target
.
value
)
)
}
aria
-
label=
{
`Seek ${filename}`
}
className=
"relative z-10 h-
4
w-full cursor-pointer appearance-none bg-transparent outline-none disabled:cursor-default
className=
"relative z-10 h-
3.5
w-full cursor-pointer appearance-none bg-transparent outline-none disabled:cursor-default
[&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full
[&::-webkit-slider-runnable-track]:bg-transparent
[&::-webkit-slider-thumb]:mt-[-
3
px] [&::-webkit-slider-thumb]:size-2 [&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:mt-[-
2.5
px] [&::-webkit-slider-thumb]:size-2 [&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-border/50
[&::-webkit-slider-thumb]:bg-background/95
[&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent
...
...
@@ -38,9 +47,6 @@ const AudioProgressBar = ({ filename, currentTime, duration, progressPercent, on
disabled=
{
duration
===
0
}
/>
</
div
>
<
div
className=
"shrink-0 text-[11px] tabular-nums text-muted-foreground"
>
{
formatAudioTime
(
currentTime
)
}
/
{
duration
>
0
?
formatAudioTime
(
duration
)
:
"--:--"
}
</
div
>
</
div
>
);
...
...
@@ -62,6 +68,9 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
const
fileTypeLabel
=
getFileTypeLabel
(
mimeType
);
const
fileSizeLabel
=
size
?
formatFileSize
(
size
)
:
undefined
;
const
progressPercent
=
duration
>
0
?
(
currentTime
/
duration
)
*
100
:
0
;
const
currentTimeLabel
=
formatAudioTime
(
currentTime
);
const
durationLabel
=
getDurationLabel
(
duration
);
const
timeLabel
=
`
${
currentTimeLabel
}
/
${
durationLabel
}
`
;
useEffect
(()
=>
{
if
(
!
audioRef
.
current
)
{
...
...
@@ -90,9 +99,8 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
audio
.
pause
();
};
const
handleSeek
=
(
value
:
string
)
=>
{
const
handleSeek
=
(
nextTime
:
number
)
=>
{
const
audio
=
audioRef
.
current
;
const
nextTime
=
Number
(
value
);
if
(
!
audio
||
Number
.
isNaN
(
nextTime
))
{
return
;
...
...
@@ -103,9 +111,7 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
};
const
handlePlaybackRateChange
=
()
=>
{
const
currentRateIndex
=
AUDIO_PLAYBACK_RATES
.
findIndex
((
rate
)
=>
rate
===
playbackRate
);
const
nextRate
=
AUDIO_PLAYBACK_RATES
[(
currentRateIndex
+
1
)
%
AUDIO_PLAYBACK_RATES
.
length
];
setPlaybackRate
(
nextRate
);
setPlaybackRate
((
currentRate
)
=>
getNextPlaybackRate
(
currentRate
));
};
const
handleDuration
=
(
value
:
number
)
=>
{
...
...
@@ -113,56 +119,49 @@ const AudioAttachmentItem = ({ filename, sourceUrl, mimeType, size, title }: Aud
};
return
(
<
div
className=
"rounded-xl border border-border/35 bg-background/70 px-2.5 py-2.5"
>
<
div
className=
"flex items-start gap-2.5"
>
<
div
className=
"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted/55 text-muted-foreground"
>
<
FileAudioIcon
className=
"size-3.5"
/>
</
div
>
<
div
className=
"flex min-w-0 flex-1 items-start justify-between gap-3"
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"truncate text-sm font-medium leading-5 text-foreground"
title=
{
filename
}
>
{
displayTitle
}
</
div
>
<
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
>
{
fileSizeLabel
&&
(
<>
<
span
className=
"text-muted-foreground/50"
>
•
</
span
>
<
span
>
{
fileSizeLabel
}
</
span
>
</>
)
}
</
div
>
<
div
className=
"rounded-xl border border-border/40 bg-background/75 px-2 py-1.5"
>
<
div
className=
"flex items-center justify-between gap-1.5"
>
<
div
className=
"min-w-0 flex flex-1 items-baseline gap-1"
>
<
div
className=
"truncate text-sm font-medium leading-5 text-foreground"
title=
{
filename
}
>
{
displayTitle
}
</
div
>
<
div
className=
"mt-0.5 flex shrink-0 items-center gap-1"
>
<
button
type=
"button"
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"
aria
-
label=
{
`Playback speed ${playbackRate}x for ${displayTitle}`
}
>
{
playbackRate
}
x
</
button
>
<
button
type=
"button"
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"
aria
-
label=
{
isPlaying
?
`Pause ${displayTitle}`
:
`Play ${displayTitle}`
}
>
{
isPlaying
?
<
PauseIcon
className=
"size-3"
/>
:
<
PlayIcon
className=
"size-3 translate-x-[0.5px]"
/>
}
</
button
>
<
div
className=
"truncate text-[11px] leading-4 text-muted-foreground"
>
{
fileTypeLabel
}
{
fileSizeLabel
?
` · ${fileSizeLabel}`
:
""
}
</
div
>
</
div
>
<
button
type=
"button"
onClick=
{
togglePlayback
}
className=
"inline-flex size-5.5 shrink-0 items-center justify-center rounded-md border border-border/45 bg-background/90 text-foreground transition-colors hover:bg-muted/45"
aria
-
label=
{
isPlaying
?
`Pause ${displayTitle}`
:
`Play ${displayTitle}`
}
>
{
isPlaying
?
<
PauseIcon
className=
"size-2.5"
/>
:
<
PlayIcon
className=
"size-2.5 translate-x-[0.5px]"
/>
}
</
button
>
</
div
>
<
AudioProgressBar
filename=
{
filename
}
currentTime=
{
currentTime
}
duration=
{
duration
}
progressPercent=
{
progressPercent
}
onSeek=
{
handleSeek
}
/>
<
div
className=
"mt-1 flex items-center gap-1"
>
<
AudioProgressBar
filename=
{
filename
}
currentTime=
{
currentTime
}
duration=
{
duration
}
progressPercent=
{
progressPercent
}
onSeek=
{
handleSeek
}
className=
"min-w-0 flex-1"
/>
<
div
className=
"shrink-0 text-[10px] tabular-nums text-muted-foreground"
>
{
timeLabel
}
</
div
>
<
button
type=
"button"
onClick=
{
handlePlaybackRateChange
}
className=
"inline-flex h-5 shrink-0 items-center justify-center rounded-md border border-transparent px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:border-border/40 hover:text-foreground"
aria
-
label=
{
`Playback speed ${playbackRate}x for ${displayTitle}`
}
>
{
playbackRate
}
x
</
button
>
</
div
>
<
audio
ref=
{
audioRef
}
...
...
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