Flatten Bolt-Compress to repo root

This commit is contained in:
majestic
2026-10-24 02:29:12 -06:00
parent 3326caa070
commit 93af8f8415
18 changed files with 0 additions and 6 deletions
+496
View File
@@ -0,0 +1,496 @@
from __future__ import annotations
from typing import Optional, Set
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
QAbstractSpinBox,
QCheckBox,
QComboBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QSizePolicy,
QSlider,
QSpinBox,
QTabWidget,
QVBoxLayout,
QWidget,
)
from .models import ImageOptions, VideoOptions, default_image_options, default_video_options
_CODEC_INFO = {
"h264": (
"H.264 · MP4",
"Plays nearly everywhere and encodes quickly. A safe default.",
),
"h265": (
"H.265 · MP4",
"Often 30–50% smaller than H.264, but slower and less widely supported.",
),
"vp9": (
"VP9 · WebM",
"Efficient open codec. A solid choice for web video.",
),
"av1": (
"AV1 · WebM",
"Excellent compression, but encoding can be very slow.",
),
}
_IMAGE_FORMAT_HINTS = {
"original": "Each image keeps its format. The quality setting applies to JPEG and WebP.",
"jpeg": "Universally compatible. Transparency is flattened onto white.",
"webp": "Excellent size-to-quality ratio. Supports transparency.",
"png": "Lossless and great for graphics. PNG files are often larger.",
}
def _hint_label(text: str = "") -> QLabel:
label = QLabel(text)
label.setObjectName("hintLabel")
label.setWordWrap(True)
return label
def _field_label(text: str) -> QLabel:
label = QLabel(text)
label.setObjectName("fieldLabel")
return label
def _divider() -> QFrame:
line = QFrame()
line.setObjectName("divider")
line.setFrameShape(QFrame.Shape.HLine)
return line
def _make_spin(minimum: int, maximum: int, value: int, suffix: str, step: int = 1) -> QSpinBox:
spin = QSpinBox()
spin.setRange(minimum, maximum)
spin.setValue(value)
spin.setSuffix(suffix)
spin.setSingleStep(step)
spin.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.NoButtons)
spin.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
return spin
def _quality_word(value: int) -> str:
if value < 55:
return "Compact"
if value < 72:
return "Balanced"
if value < 88:
return "High"
return "Maximum"
class SettingsPanel(QFrame):
settingsChanged = pyqtSignal()
def __init__(self, video_codecs: Optional[Set[str]] = None, parent=None):
super().__init__(parent)
self.setObjectName("settingsCard")
self._loading = False
self._available_codecs = (
set(video_codecs) if video_codecs is not None else {"h264"}
)
layout = QVBoxLayout(self)
layout.setContentsMargins(18, 18, 18, 16)
layout.setSpacing(10)
header = QHBoxLayout()
header.setSpacing(8)
title = QLabel("Settings")
title.setObjectName("cardTitle")
header.addWidget(title)
header.addStretch(1)
self.reset_button = QPushButton("Reset")
self.reset_button.setObjectName("ghostButton")
self.reset_button.setToolTip("Restore the default compression options")
self.reset_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.reset_button.clicked.connect(self.reset_all)
header.addWidget(self.reset_button)
layout.addLayout(header)
subtitle = _hint_label("Applied to each media type independently.")
layout.addWidget(subtitle)
self.tabs = QTabWidget()
self.tabs.setObjectName("settingsTabs")
self.tabs.setDocumentMode(True)
self.tabs.addTab(self._build_image_tab(), "Images")
self.tabs.addTab(self._build_video_tab(), "Videos")
self.tabs.tabBar().setDrawBase(False)
layout.addWidget(self.tabs, 1)
self.tabs.currentChanged.connect(self._notify)
self.max_width_spin.valueChanged.connect(self._notify)
self.max_height_spin.valueChanged.connect(self._notify)
self.scale_percent_spin.valueChanged.connect(self._notify)
self.image_metadata_check.stateChanged.connect(self._notify)
self.resolution_combo.currentIndexChanged.connect(self._notify)
self.audio_combo.currentIndexChanged.connect(self._notify)
self.video_metadata_check.stateChanged.connect(self._notify)
self._update_image_format_ui()
self._update_video_codec_ui()
self._update_resize_ui()
self._update_speed_ui()
self._update_quality_labels()
# ------------------------------------------------------------------ UI
def _scroll_page(self) -> tuple[QScrollArea, QVBoxLayout]:
scroll = QScrollArea()
scroll.setObjectName("tabScroll")
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
content = QWidget()
content.setObjectName("tabContent")
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(0, 12, 4, 4)
content_layout.setSpacing(11)
scroll.setWidget(content)
return scroll, content_layout
def _build_image_tab(self) -> QWidget:
page, layout = self._scroll_page()
layout.addWidget(_field_label("Format"))
self.image_format_combo = QComboBox()
self.image_format_combo.addItem("Keep original", "original")
self.image_format_combo.addItem("JPEG · .jpg", "jpeg")
self.image_format_combo.addItem("WebP · .webp", "webp")
self.image_format_combo.addItem("PNG · .png", "png")
self.image_format_combo.currentIndexChanged.connect(self._on_image_format_changed)
layout.addWidget(self.image_format_combo)
self.image_format_hint = _hint_label()
layout.addWidget(self.image_format_hint)
layout.addSpacing(3)
quality_header = QHBoxLayout()
quality_header.setContentsMargins(0, 0, 0, 0)
quality_header.addWidget(_field_label("Quality"))
quality_header.addStretch(1)
self.image_quality_value = QLabel()
self.image_quality_value.setObjectName("qualityValue")
quality_header.addWidget(self.image_quality_value)
layout.addLayout(quality_header)
self.image_quality_slider = QSlider(Qt.Orientation.Horizontal)
self.image_quality_slider.setRange(30, 100)
self.image_quality_slider.setValue(82)
self.image_quality_slider.setPageStep(5)
self.image_quality_slider.valueChanged.connect(self._on_quality_changed)
layout.addWidget(self.image_quality_slider)
self.image_quality_hint = _hint_label()
layout.addWidget(self.image_quality_hint)
layout.addWidget(_divider())
layout.addWidget(_field_label("Resize"))
self.resize_combo = QComboBox()
self.resize_combo.addItem("Original size", "original")
self.resize_combo.addItem("Fit within a box", "fit")
self.resize_combo.addItem("Scale by percentage", "percent")
self.resize_combo.currentIndexChanged.connect(self._update_resize_ui)
layout.addWidget(self.resize_combo)
self.fit_controls = QWidget()
fit_layout = QHBoxLayout(self.fit_controls)
fit_layout.setContentsMargins(0, 2, 0, 0)
fit_layout.setSpacing(9)
width_col = QVBoxLayout()
width_col.setSpacing(5)
width_col.addWidget(_hint_label("Max width"))
self.max_width_spin = _make_spin(16, 20000, 1920, " px", 16)
width_col.addWidget(self.max_width_spin)
fit_layout.addLayout(width_col, 1)
height_col = QVBoxLayout()
height_col.setSpacing(5)
height_col.addWidget(_hint_label("Max height"))
self.max_height_spin = _make_spin(16, 20000, 1080, " px", 16)
height_col.addWidget(self.max_height_spin)
fit_layout.addLayout(height_col, 1)
layout.addWidget(self.fit_controls)
self.percent_controls = QWidget()
percent_layout = QHBoxLayout(self.percent_controls)
percent_layout.setContentsMargins(0, 2, 0, 0)
percent_layout.setSpacing(9)
self.scale_percent_spin = _make_spin(5, 400, 80, " %", 5)
percent_layout.addWidget(self.scale_percent_spin)
percent_layout.addWidget(_hint_label("of the original size"))
percent_layout.addStretch(1)
layout.addWidget(self.percent_controls)
self.image_resize_hint = _hint_label()
layout.addWidget(self.image_resize_hint)
layout.addSpacing(4)
self.image_metadata_check = QCheckBox("Keep metadata")
self.image_metadata_check.setToolTip("Preserve EXIF and color profile information.")
layout.addWidget(self.image_metadata_check)
layout.addStretch(1)
return page
def _build_video_tab(self) -> QWidget:
page, layout = self._scroll_page()
layout.addWidget(_field_label("Codec"))
self.codec_combo = QComboBox()
for codec in ("h264", "h265", "vp9", "av1"):
if codec not in self._available_codecs:
continue
label, hint = _CODEC_INFO[codec]
self.codec_combo.addItem(label, codec)
self.codec_combo.setItemData(self.codec_combo.count() - 1, hint, Qt.ItemDataRole.ToolTipRole)
if self.codec_combo.count() == 0:
self.codec_combo.addItem("No supported encoder found", None)
self.codec_combo.setEnabled(False)
self.codec_combo.currentIndexChanged.connect(self._on_codec_changed)
layout.addWidget(self.codec_combo)
self.codec_hint = _hint_label()
layout.addWidget(self.codec_hint)
layout.addSpacing(3)
quality_header = QHBoxLayout()
quality_header.setContentsMargins(0, 0, 0, 0)
quality_header.addWidget(_field_label("Quality"))
quality_header.addStretch(1)
self.video_quality_value = QLabel()
self.video_quality_value.setObjectName("qualityValue")
quality_header.addWidget(self.video_quality_value)
layout.addLayout(quality_header)
self.video_quality_slider = QSlider(Qt.Orientation.Horizontal)
self.video_quality_slider.setRange(20, 95)
self.video_quality_slider.setValue(68)
self.video_quality_slider.setPageStep(5)
self.video_quality_slider.valueChanged.connect(self._on_quality_changed)
layout.addWidget(self.video_quality_slider)
layout.addWidget(
_hint_label("Higher quality keeps more detail; lower saves space.")
)
layout.addWidget(_divider())
layout.addWidget(_field_label("Encoding speed"))
self.speed_combo = QComboBox()
self.speed_combo.addItem("Fast", "fast")
self.speed_combo.addItem("Balanced", "balanced")
self.speed_combo.addItem("Best compression", "best")
self.speed_combo.setCurrentIndex(1)
self.speed_combo.currentIndexChanged.connect(self._update_speed_ui)
layout.addWidget(self.speed_combo)
self.speed_hint = _hint_label()
layout.addWidget(self.speed_hint)
layout.addWidget(_field_label("Resolution"))
self.resolution_combo = QComboBox()
self.resolution_combo.addItem("Original resolution", 0)
self.resolution_combo.addItem("2160p · 4K", 2160)
self.resolution_combo.addItem("1440p · QHD", 1440)
self.resolution_combo.addItem("1080p · Full HD", 1080)
self.resolution_combo.addItem("720p · HD", 720)
self.resolution_combo.addItem("480p · SD", 480)
self.resolution_combo.addItem("360p", 360)
layout.addWidget(self.resolution_combo)
layout.addWidget(
_hint_label("Caps the shorter edge. Videos are never upscaled.")
)
layout.addWidget(_field_label("Audio"))
self.audio_combo = QComboBox()
self.audio_combo.addItem("Balanced · 128 kbps", 128)
self.audio_combo.addItem("High · 192 kbps", 192)
self.audio_combo.addItem("Data saver · 96 kbps", 96)
self.audio_combo.addItem("Remove audio", 0)
layout.addWidget(self.audio_combo)
layout.addWidget(_hint_label("Audio is re-encoded to match the output container."))
layout.addSpacing(4)
self.video_metadata_check = QCheckBox("Keep metadata")
self.video_metadata_check.setToolTip("Preserve global metadata and chapters.")
layout.addWidget(self.video_metadata_check)
layout.addStretch(1)
return page
# ------------------------------------------------------------- behavior
def _notify(self, *_args) -> None:
if self._loading:
return
self.settingsChanged.emit()
def _on_quality_changed(self, _value: int) -> None:
self._update_quality_labels()
self._notify()
def _on_image_format_changed(self, _index: int) -> None:
self._update_image_format_ui()
self._notify()
def _on_codec_changed(self, _index: int) -> None:
self._update_video_codec_ui()
self._notify()
def _update_quality_labels(self) -> None:
image_quality = self.image_quality_slider.value()
video_quality = self.video_quality_slider.value()
self.image_quality_value.setText(f"{image_quality} · {_quality_word(image_quality)}")
self.video_quality_value.setText(f"{video_quality} · {_quality_word(video_quality)}")
def _update_image_format_ui(self) -> None:
format_choice = self.image_format_combo.currentData() or "original"
self.image_format_hint.setText(_IMAGE_FORMAT_HINTS.get(format_choice, ""))
quality_applies = format_choice != "png"
self.image_quality_slider.setEnabled(quality_applies)
if not quality_applies:
self.image_quality_hint.setText("PNG is lossless, so the quality slider does not apply.")
elif format_choice == "original":
self.image_quality_hint.setText("Used for JPEG and WebP images; ignored for PNG.")
else:
self.image_quality_hint.setText("Higher values preserve more detail and create larger files.")
def _update_resize_ui(self, *_args) -> None:
mode = self.resize_combo.currentData() or "original"
self.fit_controls.setVisible(mode == "fit")
self.percent_controls.setVisible(mode == "percent")
if mode == "fit":
self.image_resize_hint.setText("Images are scaled down to fit inside the box, keeping their aspect ratio.")
elif mode == "percent":
self.image_resize_hint.setText("Scales width and height by the chosen percentage.")
else:
self.image_resize_hint.setText("No resizing. Only the format and quality are changed.")
self._notify()
def _update_video_codec_ui(self) -> None:
codec = self.codec_combo.currentData()
if codec in _CODEC_INFO:
self.codec_hint.setText(_CODEC_INFO[codec][1])
else:
self.codec_hint.setText("No video encoder is available in this FFmpeg installation.")
def _update_speed_ui(self, *_args) -> None:
speed = self.speed_combo.currentData() or "balanced"
descriptions = {
"fast": "Quickest encode. Output is slightly larger.",
"balanced": "A good balance of encoding time and file size.",
"best": "Slower, but seeks out extra savings.",
}
self.speed_hint.setText(descriptions.get(speed, ""))
self._notify()
# --------------------------------------------------------------- values
def image_options(self) -> ImageOptions:
return ImageOptions(
output_format=self.image_format_combo.currentData() or "original",
quality=self.image_quality_slider.value(),
resize_mode=self.resize_combo.currentData() or "original",
max_width=self.max_width_spin.value(),
max_height=self.max_height_spin.value(),
scale_percent=self.scale_percent_spin.value(),
keep_metadata=self.image_metadata_check.isChecked(),
)
def video_options(self) -> VideoOptions:
return VideoOptions(
codec=self.codec_combo.currentData() or "h264",
quality=self.video_quality_slider.value(),
speed=self.speed_combo.currentData() or "balanced",
resolution=int(self.resolution_combo.currentData() or 0),
audio_bitrate=int(self.audio_combo.currentData() or 0),
keep_metadata=self.video_metadata_check.isChecked(),
)
def set_image_options(self, options: ImageOptions) -> None:
self._loading = True
try:
self._set_combo_data(self.image_format_combo, options.output_format)
self.image_quality_slider.setValue(self._clamp(options.quality, 30, 100))
self._set_combo_data(self.resize_combo, options.resize_mode)
self.max_width_spin.setValue(self._clamp(options.max_width, 16, 20000))
self.max_height_spin.setValue(self._clamp(options.max_height, 16, 20000))
self.scale_percent_spin.setValue(self._clamp(options.scale_percent, 5, 400))
self.image_metadata_check.setChecked(bool(options.keep_metadata))
self._update_image_format_ui()
self._update_resize_ui()
self._update_quality_labels()
finally:
self._loading = False
def set_video_options(self, options: VideoOptions) -> None:
self._loading = True
try:
self._set_combo_data(self.codec_combo, options.codec)
self.video_quality_slider.setValue(self._clamp(options.quality, 20, 95))
self._set_combo_data(self.speed_combo, options.speed)
self._set_combo_data(self.resolution_combo, options.resolution)
self._set_combo_data(self.audio_combo, options.audio_bitrate)
self.video_metadata_check.setChecked(bool(options.keep_metadata))
self._update_video_codec_ui()
self._update_speed_ui()
self._update_quality_labels()
finally:
self._loading = False
def reset_all(self) -> None:
self._loading = True
try:
image_defaults = default_image_options()
video_defaults = default_video_options()
self._set_combo_data(self.image_format_combo, image_defaults.output_format)
self.image_quality_slider.setValue(image_defaults.quality)
self._set_combo_data(self.resize_combo, image_defaults.resize_mode)
self.max_width_spin.setValue(image_defaults.max_width)
self.max_height_spin.setValue(image_defaults.max_height)
self.scale_percent_spin.setValue(image_defaults.scale_percent)
self.image_metadata_check.setChecked(image_defaults.keep_metadata)
self._set_combo_data(self.codec_combo, video_defaults.codec)
self.video_quality_slider.setValue(video_defaults.quality)
self._set_combo_data(self.speed_combo, video_defaults.speed)
self._set_combo_data(self.resolution_combo, video_defaults.resolution)
self._set_combo_data(self.audio_combo, video_defaults.audio_bitrate)
self.video_metadata_check.setChecked(video_defaults.keep_metadata)
self._update_image_format_ui()
self._update_resize_ui()
self._update_video_codec_ui()
self._update_speed_ui()
self._update_quality_labels()
finally:
self._loading = False
self.settingsChanged.emit()
def current_tab_index(self) -> int:
return self.tabs.currentIndex()
def set_current_tab_index(self, index: int) -> None:
self._loading = True
try:
self.tabs.setCurrentIndex(0 if index not in (0, 1) else index)
finally:
self._loading = False
@staticmethod
def _clamp(value: int, minimum: int, maximum: int) -> int:
try:
return max(minimum, min(maximum, int(value)))
except (TypeError, ValueError):
return minimum
@staticmethod
def _set_combo_data(combo: QComboBox, value) -> None:
index = combo.findData(value)
if index >= 0:
combo.setCurrentIndex(index)