Merge branch 'develop'
This commit is contained in:
commit
4a36390c17
@ -26,6 +26,8 @@ IMAGE_EXTENSIONS = {
|
||||
".webp",
|
||||
}
|
||||
VIDEO_EXTENSIONS = {
|
||||
".3gp",
|
||||
".avi",
|
||||
".m4v",
|
||||
".mkv",
|
||||
".mov",
|
||||
@ -97,19 +99,17 @@ def _render_video_thumbnail(attachment: TaskAttachment) -> bytes:
|
||||
output_path = output_file.name
|
||||
|
||||
filter_value = "scale=640:280:force_original_aspect_ratio=increase,crop=640:280"
|
||||
command = [
|
||||
ffmpeg,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
"0.5",
|
||||
"-i",
|
||||
input_path,
|
||||
command_prefix = [ffmpeg, "-hide_banner", "-loglevel", "error"]
|
||||
command_suffix = [
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
filter_value,
|
||||
"-an",
|
||||
"-sn",
|
||||
"-dn",
|
||||
"-c:v",
|
||||
"libwebp",
|
||||
"-quality",
|
||||
@ -117,22 +117,33 @@ def _render_video_thumbnail(attachment: TaskAttachment) -> bytes:
|
||||
"-y",
|
||||
output_path,
|
||||
]
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
command[5] = "0"
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.decode("utf-8", errors="replace"))
|
||||
commands = [
|
||||
# Fast input seek works for ordinary MP4/WebM files.
|
||||
[*command_prefix, "-ss", "0.5", "-i", input_path, *command_suffix],
|
||||
# Accurate output seek handles fragmented MP4/MOV screen recordings
|
||||
# whose first keyframe cannot be reached by input seeking.
|
||||
[*command_prefix, "-i", input_path, "-ss", "0.5", *command_suffix],
|
||||
# Some recordings start on a non-zero timeline. In that case use the
|
||||
# first frame FFmpeg can decode instead of requiring a timestamp.
|
||||
[*command_prefix, "-i", input_path, *command_suffix],
|
||||
]
|
||||
last_error = "FFmpeg did not produce a video thumbnail."
|
||||
for command in commands:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
last_error = str(error)
|
||||
continue
|
||||
if result.returncode == 0 and Path(output_path).stat().st_size > 0:
|
||||
break
|
||||
last_error = result.stderr.decode("utf-8", errors="replace")
|
||||
else:
|
||||
raise RuntimeError(last_error)
|
||||
return Path(output_path).read_bytes()
|
||||
finally:
|
||||
if temporary_input and input_path:
|
||||
|
||||
@ -97,13 +97,6 @@ function boardGroupPosition(
|
||||
return board.group_positions?.[String(groupId)];
|
||||
}
|
||||
|
||||
function boardGroupSortKey(
|
||||
board: TaskBoardSummary,
|
||||
groupId: number,
|
||||
): string | undefined {
|
||||
return board.group_sort_keys?.[String(groupId)];
|
||||
}
|
||||
|
||||
function compareBoardNames(first: TaskBoardSummary, second: TaskBoardSummary): number {
|
||||
return first.name.localeCompare(second.name, "ru") || first.id - second.id;
|
||||
}
|
||||
@ -519,14 +512,18 @@ export default function TaskBoardNavigator({
|
||||
(effectiveBoardSort === "priority" || effectiveBoardSort === "priority-desc")
|
||||
&& typeof effectiveGroupFilter === "number"
|
||||
) {
|
||||
const firstRank = boardGroupSortKey(first, effectiveGroupFilter);
|
||||
const secondRank = boardGroupSortKey(second, effectiveGroupFilter);
|
||||
if (firstRank && secondRank && firstRank !== secondRank) {
|
||||
const firstPosition = boardGroupPosition(first, effectiveGroupFilter);
|
||||
const secondPosition = boardGroupPosition(second, effectiveGroupFilter);
|
||||
if (
|
||||
firstPosition !== undefined
|
||||
&& secondPosition !== undefined
|
||||
&& firstPosition !== secondPosition
|
||||
) {
|
||||
const direction = effectiveBoardSort === "priority-desc" ? -1 : 1;
|
||||
return (firstRank < secondRank ? -1 : 1) * direction;
|
||||
return (firstPosition - secondPosition) * direction;
|
||||
}
|
||||
if (firstRank && !secondRank) return -1;
|
||||
if (!firstRank && secondRank) return 1;
|
||||
if (firstPosition !== undefined && secondPosition === undefined) return -1;
|
||||
if (firstPosition === undefined && secondPosition !== undefined) return 1;
|
||||
}
|
||||
|
||||
if (effectiveBoardSort === "name-desc") {
|
||||
|
||||
@ -98,7 +98,7 @@ export default function TaskCommentAttachment({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void openPreview()}
|
||||
disabled={!thumbnailSource || thumbnailFailed || previewLoading}
|
||||
disabled={previewLoading}
|
||||
className="group relative block aspect-video min-h-32 w-full overflow-hidden bg-[var(--surface-secondary)] outline-none focus-visible:ring-2 focus-visible:ring-sky-400/70 disabled:cursor-default"
|
||||
title={`Просмотреть ${attachment.file_name}`}
|
||||
>
|
||||
@ -113,7 +113,15 @@ export default function TaskCommentAttachment({
|
||||
/>
|
||||
) : (
|
||||
<span className="app-text-muted absolute inset-0 flex items-center justify-center">
|
||||
{thumbnailFailed ? <FileText size={20} /> : <Loader2 size={18} className="animate-spin" />}
|
||||
{previewLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : thumbnailFailed ? (
|
||||
<span className="app-selected inline-flex h-11 w-11 items-center justify-center rounded-full">
|
||||
{video ? <Play size={20} fill="currentColor" /> : <Eye size={20} />}
|
||||
</span>
|
||||
) : (
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{thumbnailSource ? (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user