Skip to content

Commit ecd37f0

Browse files
committed
* Adding Start/End Time tab to right-side options panel between Size and Crop tabs with compact 3-column layout
* Fixing video track selector showing unnecessarily when source video has only one video track * Fixing visual border between filename area and video track selector * Fixing test suite hanging due to missing QApplication in PySide6 widget tests
1 parent 428eb08 commit ecd37f0

21 files changed

Lines changed: 1097 additions & 358 deletions

CHANGES

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Version 5.13.0
44

55
* Adding #712 audio profile title options: No Title, Generate Title, and Custom Title
6+
* Adding Start/End Time tab to right-side options panel between Size and Crop tabs with compact 3-column layout
7+
* Fixing video track selector showing unnecessarily when source video has only one video track
8+
* Fixing visual border between filename area and video track selector
9+
* Fixing test suite hanging due to missing QApplication in PySide6 widget tests
610
* Adding async queue saving to prevent GUI blocking during queue operations
711
* Adding atomic file writes for queue to prevent corruption from interrupted saves
812
* Adding file-based locking for queue operations to prevent race conditions between instances
@@ -20,6 +24,7 @@
2024
* Fixing IndexError when applying profile audio filters to videos with non-sequential track indices
2125
* Fixing TypeError crash with Rigaya encoders when audio quality not explicitly set
2226
* Fixing AttributeError crash when audio track metadata is incomplete
27+
* Fixing queue file generation mismatch errors due to redundant save calls on startup and when adding to queue
2328

2429
## Version 5.12.4
2530

fastflix/data/languages.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11377,3 +11377,27 @@ Custom Title:
1137711377
ukr: Користувацька назва
1137811378
kor: 사용자 지정 제목
1137911379
ron: Titlu personalizat
11380+
Flip:
11381+
eng: Flip
11382+
V Flip:
11383+
eng: V Flip
11384+
H Flip:
11385+
eng: H Flip
11386+
V+H Flip:
11387+
eng: V+H Flip
11388+
Size:
11389+
eng: Size
11390+
Reset:
11391+
eng: Reset
11392+
Reset start and end times:
11393+
eng: Reset start and end times
11394+
Fast:
11395+
eng: Fast
11396+
Exact:
11397+
eng: Exact
11398+
Start/End Time:
11399+
eng: Start/End Time
11400+
Reset crop:
11401+
eng: Reset crop
11402+
Options:
11403+
eng: Options

fastflix/encoders/common/setting_panel.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from fastflix.exceptions import FastFlixInternalException
1010
from fastflix.language import t
1111
from fastflix.models.fastflix_app import FastFlixApp
12+
from fastflix.ui_scale import scaler
1213
from fastflix.widgets.background_tasks import ExtractHDR10
1314
from fastflix.resources import group_box_style, get_icon
1415

@@ -143,9 +144,9 @@ def _add_combo_box(
143144
self.widgets[widget_name] = QtWidgets.QComboBox()
144145
self.widgets[widget_name].addItems(options)
145146
if min_width:
146-
self.widgets[widget_name].setMinimumWidth(min_width)
147+
self.widgets[widget_name].setMinimumWidth(scaler.scale(min_width))
147148
if width:
148-
self.widgets[widget_name].setFixedWidth(width)
149+
self.widgets[widget_name].setFixedWidth(scaler.scale(width))
149150

150151
if opt:
151152
default = self.determine_default(
@@ -227,7 +228,7 @@ def _add_text_box(
227228
self.widgets[widget_name].setValidator(self.only_int)
228229

229230
if width:
230-
self.widgets[widget_name].setFixedWidth(width)
231+
self.widgets[widget_name].setFixedWidth(scaler.scale(width))
231232

232233
layout.addWidget(self.labels[widget_name])
233234
layout.addWidget(self.widgets[widget_name])
@@ -369,7 +370,7 @@ def _add_modes(
369370
config_opt = None
370371
if not disable_bitrate:
371372
self.bitrate_radio = QtWidgets.QRadioButton("Bitrate")
372-
self.bitrate_radio.setFixedWidth(80)
373+
self.bitrate_radio.setFixedWidth(scaler.scale(67))
373374
self.widgets.mode.addButton(self.bitrate_radio)
374375
self.widgets.bitrate = QtWidgets.QComboBox()
375376
self.widgets.bitrate.addItems(recommended_bitrates)
@@ -389,7 +390,7 @@ def _add_modes(
389390
self.widgets.bitrate.setCurrentIndex(default_bitrate_index)
390391
self.widgets.custom_bitrate = QtWidgets.QLineEdit("3000" if not custom_bitrate else config_opt)
391392
self.widgets.custom_bitrate.setValidator(QtGui.QDoubleValidator())
392-
self.widgets.custom_bitrate.setFixedWidth(100)
393+
self.widgets.custom_bitrate.setMinimumWidth(scaler.scale(83))
393394
self.widgets.custom_bitrate.setEnabled(custom_bitrate)
394395
self.widgets.custom_bitrate.textChanged.connect(lambda: self.main.build_commands())
395396
self.widgets.custom_bitrate.setValidator(self.only_int)
@@ -409,7 +410,7 @@ def _add_modes(
409410

410411
self.qp_radio = QtWidgets.QRadioButton(qp_display_name)
411412
self.qp_radio.setChecked(True)
412-
self.qp_radio.setFixedWidth(80)
413+
self.qp_radio.setFixedWidth(scaler.scale(67))
413414
self.qp_radio.setToolTip(qp_help)
414415
self.widgets.mode.addButton(self.qp_radio)
415416

@@ -430,7 +431,7 @@ def _add_modes(
430431

431432
if not disable_custom_qp:
432433
self.widgets[f"custom_{qp_name}"] = QtWidgets.QLineEdit("30" if not custom_qp else str(qp_value))
433-
self.widgets[f"custom_{qp_name}"].setFixedWidth(100)
434+
self.widgets[f"custom_{qp_name}"].setMinimumWidth(scaler.scale(83))
434435
self.widgets[f"custom_{qp_name}"].setValidator(QtGui.QDoubleValidator())
435436
self.widgets[f"custom_{qp_name}"].setEnabled(custom_qp)
436437
self.widgets[f"custom_{qp_name}"].textChanged.connect(lambda: self.main.build_commands())

fastflix/ui_constants.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
UI Constants for FastFlix.
4+
5+
Defines base dimensions for UI elements at the reference resolution of 1200x680.
6+
These values are used with the UIScaler to compute actual pixel sizes at runtime.
7+
"""
8+
9+
from dataclasses import dataclass
10+
11+
12+
@dataclass(frozen=True, slots=True)
13+
class BaseWidths:
14+
"""Base width values (~25% smaller than original for better default scaling)."""
15+
16+
MENUBAR: int = 270
17+
PROFILE_BOX: int = 190
18+
ENCODER_MIN: int = 165
19+
CROP_BOX_MIN: int = 280
20+
SOURCE_LABEL: int = 65
21+
RESOLUTION_CUSTOM: int = 115
22+
FLIP_DROPDOWN: int = 120
23+
ROTATE_DROPDOWN: int = 130
24+
PREVIEW_MIN: int = 330
25+
OUTPUT_TYPE: int = 60
26+
VIDEO_TRACK_LABEL: int = 75
27+
ENCODER_LABEL: int = 50
28+
RESOLUTION_LABEL: int = 70
29+
FAST_TIME: int = 50
30+
AUTO_CROP: int = 40
31+
RESET_BUTTON: int = 12
32+
SMALL_BUTTON: int = 15
33+
AUDIO_TITLE: int = 115
34+
AUDIO_INFO: int = 265
35+
SPACER_SMALL: int = 3
36+
CUSTOM_INPUT: int = 75
37+
38+
39+
@dataclass(frozen=True, slots=True)
40+
class BaseHeights:
41+
"""Base height values (~25% smaller than original for better default scaling)."""
42+
43+
TOP_BAR_BUTTON: int = 38
44+
PATH_WIDGET: int = 20
45+
COMBO_BOX: int = 22
46+
PANEL_ITEM: int = 45
47+
SCROLL_MIN: int = 150
48+
PREVIEW_MIN: int = 195
49+
OUTPUT_DIR: int = 18
50+
HEADER: int = 23
51+
SPACER_TINY: int = 2
52+
SPACER_SMALL: int = 4
53+
BUTTON_SIZE: int = 22
54+
55+
56+
@dataclass(frozen=True, slots=True)
57+
class BaseIconSizes:
58+
"""Base icon sizes (square) - ~25% smaller than original."""
59+
60+
TINY: int = 8
61+
SMALL: int = 12
62+
MEDIUM: int = 17
63+
LARGE: int = 20
64+
XLARGE: int = 26
65+
66+
67+
@dataclass(frozen=True, slots=True)
68+
class BaseFontSizes:
69+
"""Base font sizes."""
70+
71+
SMALL: int = 9
72+
NORMAL: int = 10
73+
MEDIUM: int = 11
74+
LARGE: int = 12
75+
XLARGE: int = 14
76+
77+
78+
WIDTHS = BaseWidths()
79+
HEIGHTS = BaseHeights()
80+
ICONS = BaseIconSizes()
81+
FONTS = BaseFontSizes()

fastflix/ui_scale.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
UI Scaling module for FastFlix.
4+
5+
Provides a singleton UIScaler that manages scale factors for the entire application.
6+
Scale factors are computed based on the current window size relative to the base
7+
reference size of 1200x680.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import copy
13+
from dataclasses import dataclass
14+
from typing import Callable
15+
16+
from PySide6 import QtCore
17+
18+
BASE_WIDTH = 1200
19+
BASE_HEIGHT = 680
20+
21+
22+
@dataclass
23+
class ScaleFactors:
24+
"""Scale factors for UI elements - immutable, use copy.replace() to modify."""
25+
26+
width: float = 1.0
27+
height: float = 1.0
28+
uniform: float = 1.0
29+
font: float = 1.0
30+
icon: float = 1.0
31+
32+
33+
class UIScaler:
34+
"""Singleton for managing UI scaling throughout the application."""
35+
36+
_instance: UIScaler | None = None
37+
38+
def __new__(cls) -> UIScaler:
39+
if cls._instance is None:
40+
cls._instance = super().__new__(cls)
41+
cls._instance._initialized = False
42+
return cls._instance
43+
44+
def __init__(self) -> None:
45+
if self._initialized:
46+
return
47+
self._initialized = True
48+
self.factors = ScaleFactors()
49+
self._listeners: list[Callable[[ScaleFactors], None]] = []
50+
51+
def calculate_factors(self, width: int, height: int) -> None:
52+
"""Calculate and update scale factors based on current window dimensions."""
53+
width_factor = width / BASE_WIDTH
54+
height_factor = height / BASE_HEIGHT
55+
uniform = min(width_factor, height_factor)
56+
57+
# Use Python 3.13 copy.replace() for immutable update
58+
self.factors = copy.replace(
59+
self.factors,
60+
width=width_factor,
61+
height=height_factor,
62+
uniform=uniform,
63+
font=uniform,
64+
icon=uniform,
65+
)
66+
self._notify_listeners()
67+
68+
def scale(self, base_value: int) -> int:
69+
"""Scale a base value by the uniform scale factor."""
70+
return max(1, int(base_value * self.factors.uniform))
71+
72+
def scale_font(self, base_size: int) -> int:
73+
"""Scale a font size, with minimum of 8px for readability."""
74+
return max(8, int(base_size * self.factors.font))
75+
76+
def scale_icon(self, base_size: int) -> int:
77+
"""Scale an icon size, with minimum of 10px for visibility."""
78+
return max(10, int(base_size * self.factors.icon))
79+
80+
def scale_size(self, width: int, height: int) -> QtCore.QSize:
81+
"""Scale a width/height pair and return as QSize."""
82+
return QtCore.QSize(self.scale(width), self.scale(height))
83+
84+
def add_listener(self, callback: Callable[[ScaleFactors], None]) -> None:
85+
"""Register a callback to be notified when scale factors change."""
86+
self._listeners.append(callback)
87+
88+
def remove_listener(self, callback: Callable[[ScaleFactors], None]) -> None:
89+
"""Unregister a previously registered callback."""
90+
if callback in self._listeners:
91+
self._listeners.remove(callback)
92+
93+
def _notify_listeners(self) -> None:
94+
"""Notify all registered listeners of scale factor changes."""
95+
for callback in self._listeners:
96+
callback(self.factors)
97+
98+
99+
# Global singleton instance
100+
scaler = UIScaler()

fastflix/ui_styles.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
UI Styles module for FastFlix.
4+
5+
Provides scaled stylesheets that adapt to the current UI scale factors.
6+
"""
7+
8+
from fastflix.ui_scale import scaler
9+
from fastflix.ui_constants import FONTS
10+
11+
12+
def get_scaled_stylesheet(theme: str) -> str:
13+
"""Generate a scaled stylesheet based on the current theme and scale factors."""
14+
font_size = scaler.scale_font(FONTS.LARGE)
15+
border_radius = scaler.scale(10)
16+
17+
base = f"QWidget {{ font-size: {font_size}px; }}"
18+
19+
if theme == "onyx":
20+
base += f"""
21+
QAbstractItemView {{ background-color: #4b5054; }}
22+
QComboBox QAbstractItemView {{ background-color: #1d2023; border: 2px solid #76797c; }}
23+
QPushButton {{ border-radius: {border_radius}px; }}
24+
QLineEdit {{
25+
background-color: #707070;
26+
color: black;
27+
border-radius: {border_radius}px;
28+
}}
29+
QTextEdit {{ background-color: #707070; color: black; }}
30+
QTabBar::tab {{ background-color: #4b5054; }}
31+
QComboBox {{ border-radius: {border_radius}px; }}
32+
QScrollArea {{ border: 1px solid #919191; }}
33+
"""
34+
35+
return base
36+
37+
38+
def get_video_options_stylesheet(theme: str) -> str:
39+
"""Generate scaled stylesheet for the video options tab widget."""
40+
tab_font_size = scaler.scale_font(FONTS.MEDIUM)
41+
combo_min_height = scaler.scale(22)
42+
43+
if theme == "onyx":
44+
return f"""
45+
* {{ background-color: #4b5054; color: white; }}
46+
QTabWidget {{ margin-top: {scaler.scale(34)}px; background-color: #4b5054; }}
47+
QTabBar {{ font-size: {tab_font_size}px; background-color: #4f5962; }}
48+
QComboBox {{ min-height: {combo_min_height}px; }}
49+
"""
50+
return ""
51+
52+
53+
def get_menubar_stylesheet() -> str:
54+
"""Generate scaled stylesheet for the menu bar."""
55+
font_size = scaler.scale_font(FONTS.LARGE)
56+
return f"font-size: {font_size}px"

0 commit comments

Comments
 (0)