Merge branch 'develop'

This commit is contained in:
lizerk 2026-08-24 14:05:08 +03:00
commit 4a36390c17
3 changed files with 56 additions and 40 deletions

View File

@ -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:

View File

@ -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") {

View File

@ -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 ? (