diff --git a/.gitignore b/.gitignore
index 7b44eb7..18d1761 100644
--- a/.gitignore
+++ b/.gitignore
@@ -169,3 +169,5 @@ cython_debug/
# Custom
download/
+/.idea/
+/Pipfile
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index feedfc0..2ed1832 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,3 @@
-default_language_version:
- python: python3.12
-
default_stages: [pre-commit, pre-push]
repos:
@@ -12,6 +9,7 @@ repos:
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
+ exclude: LICENSE
- id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit
@@ -24,13 +22,6 @@ repos:
hooks:
- id: prettier
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
- hooks:
- - id: check-yaml
- - id: end-of-file-fixer
- exclude: LICENSE
-
- repo: local
hooks:
- id: pyupgrade
diff --git a/docs/delimiters_category.md b/docs/delimiters_category.md
new file mode 100644
index 0000000..0ef148b
--- /dev/null
+++ b/docs/delimiters_category.md
@@ -0,0 +1,43 @@
+# "Delimiters" Category
+
+## Goal
+
+Allow users to customize separators used for artists and album artists, both in metadata and filenames.
+
+## Parameters
+
+- `metadata_delimiter_artist`: artists separator (metadata)
+- `metadata_delimiter_album_artist`: album artists separator (metadata)
+- `filename_delimiter_artist`: artists separator (filename)
+- `filename_delimiter_album_artist`: album artists separator (filename)
+
+## UI
+
+- Page `page_delimiters` inside `sw_categories`
+- GroupBox `gb_delimiters`
+- 4 `QLineEdit` entries (`le_*`) with max width 100px
+
+## Integration
+
+- Add the 4 fields to `DialogPreferences.parameters_line_edit`
+- Load values via `populate_line_edit()`
+- Save via `to_settings()`
+
+## Tests
+
+- `tests/test_delimiters_category.py`: 13 tests
+ - Page existence
+ - Widgets accessibility
+ - Navigation to the page
+ - Width constraints
+
+## Examples
+
+- Semicolon: `"; "` → `Artist1; Artist2`
+- Ampersand: `" & "` → `Artist1 & Artist2`
+- Slash: `" / "` → `Artist1 / Artist2`
+
+## Notes
+
+- Default values are `", "` (comma + space)
+- Fields are kept short (max 100px) to avoid breaking the layout
diff --git a/docs/refactor_settings_ui.md b/docs/refactor_settings_ui.md
new file mode 100644
index 0000000..651427c
--- /dev/null
+++ b/docs/refactor_settings_ui.md
@@ -0,0 +1,128 @@
+# Settings Window Refactor
+
+Date: 2025-11-30
+Author: Automation (Copilot)
+
+## Goal
+
+Refactor the settings window to improve usability and maintainability:
+
+- Category list on the left
+- Settings content on the right via QStackedWidget
+- Clear separation of pages (Flags, Quality, Numbers, Paths & Formats, Delimiters)
+- Consistent dark styling and tuned spacing
+- Add unit and integration tests (61 total)
+
+## Key Changes
+
+### 1. UI Architecture
+
+- Navigation column: `QListWidget (lw_categories)`
+- Pages container: `QStackedWidget (sw_categories)`
+- Pages created:
+ - `page_flags` (GroupBox: gb_flags)
+ - `page_quality` (GroupBox: gb_choices)
+ - `page_numbers` (GroupBox: gb_numbers)
+ - `page_paths` (GroupBox: gb_path)
+ - `page_delimiters` (GroupBox: gb_delimiters) — NEW
+
+Impacted file: `tidal_dl_ng/ui/dialog_settings.ui` (auto-generated `dialog_settings.py`).
+
+### 2. New Category: Delimiters
+
+- "Delimiters" page with 4 `QLineEdit` fields to edit separators:
+ - `metadata_delimiter_artist`
+ - `metadata_delimiter_album_artist`
+ - `filename_delimiter_artist`
+ - `filename_delimiter_album_artist`
+- Automatic integration with `DialogPreferences` through `parameters_line_edit`, `populate_line_edit()` and `to_settings()`.
+
+Files touched:
+
+- `tidal_dl_ng/ui/dialog_settings.ui` (new page)
+- `tidal_dl_ng/ui/dialog_settings.py` (regenerated)
+- `tidal_dl_ng/dialog.py` (added 4 parameters and new category in `_init_categories`).
+
+### 3. Styles & Spacing
+
+- Category list: dark style, blue selection, gray hover, light text.
+- Vertical spacing: list item margin `2px 0px` (and optional view spacing).
+- Settings area: dark background (QStackedWidget), styled GroupBoxes.
+
+Files touched:
+
+- `tidal_dl_ng/ui/dialog_settings.ui` (stylesheets on lw_categories and sw_categories)
+- `tidal_dl_ng/ui/dialog_settings.py` (regenerated).
+
+### 4. Tests
+
+- New files:
+ - `tests/test_settings_ui.py`: UI and basic integration (14)
+ - `tests/test_settings_dialog_structure.py`: detailed structure (20)
+ - `tests/test_dialog_preferences_integration.py`: DialogPreferences integration (14)
+ - `tests/test_delimiters_category.py`: Delimiters page (13)
+- Total: **61 tests**; ruff/black/pyupgrade applied via pre-commit.
+
+### 5. Pre-commit & Quality
+
+- Ruff fixes:
+ - F841 (unused variable) in `test_dialog_preferences_integration.py`
+ - S108 (using /tmp) replaced by `Path.home()` and safe values
+- end-of-file-fixer and black reformatted automatically.
+
+## Usage
+
+### Launch the UI
+
+- Via the app: `poetry run tidal-dl-ng-gui` (or `python -m tidal_dl_ng.gui`)
+
+### Navigate
+
+- Select a category in `lw_categories`
+- The corresponding page shows in `sw_categories`.
+
+### Edit separators
+
+- Open the "Delimiters" category
+- Edit fields `le_metadata_delimiter_*` and `le_filename_delimiter_*`
+- Confirm via OK
+
+## Known issue: poetry+dulwich KeyError b'HEAD
+
+- Cause: dulwich cannot resolve HEAD in the Git repo when Poetry queries VCS info.
+- Fix: ensure `.git/HEAD` points to an existing branch (e.g. `ref: refs/heads/main`) and the branch exists; then re-run `poetry install`. See commands below.
+
+## Useful Commands (PowerShell)
+
+```powershell
+cd C:\Users\mathe\PycharmProjects\tidal-dl-ng
+poetry check --lock
+poetry run pre-commit run -a
+pytest -q
+```
+
+To fix HEAD if needed:
+
+```powershell
+cd C:\Users\mathe\PycharmProjects\tidal-dl-ng
+Get-Content .git\HEAD
+# If empty/incorrect:
+git init
+git add .
+git commit -m "Initialize repo for Poetry/Dulwich"
+git branch -M main
+Get-Content .git\HEAD
+poetry install -vvv --no-interaction --all-extras --with dev,docs
+```
+
+## Impact
+
+- Better UX: category navigation, improved readability
+- Extensible: easy to add new pages/settings
+- Quality: comprehensive tests ensure robustness
+
+## Next Steps
+
+- Add contextual help (tooltips) for new delimiter fields
+- Optionally adjust list spacing (8–10) based on feedback
+- Add a preview of resulting formatting (e.g., sample metadata)
diff --git a/docs/testing_summary.md b/docs/testing_summary.md
new file mode 100644
index 0000000..5229548
--- /dev/null
+++ b/docs/testing_summary.md
@@ -0,0 +1,32 @@
+# Created Tests and Results
+
+## Test files
+
+- `tests/test_settings_ui.py` (14 tests)
+- `tests/test_settings_dialog_structure.py` (20 tests)
+- `tests/test_dialog_preferences_integration.py` (14 tests)
+- `tests/test_delimiters_category.py` (13 tests)
+
+## Total
+
+- **61 tests** (unit + integration)
+
+## Coverage
+
+- UI creation and structure
+- Category navigation
+- Widgets presence and accessibility
+- New "Delimiters" page
+- Quality: ruff/black/pyupgrade enforced by pre-commit
+
+## Commands
+
+```powershell
+cd C:\Users\mathe\PycharmProjects\tidal-dl-ng
+poetry run pre-commit run -a
+pytest -q
+```
+
+## Note
+
+If `poetry install` fails with KeyError b'HEAD, see "Settings Window Refactor" doc to fix the Git repo’s HEAD.
diff --git a/docs/ui_styles.md b/docs/ui_styles.md
new file mode 100644
index 0000000..6e2a81e
--- /dev/null
+++ b/docs/ui_styles.md
@@ -0,0 +1,23 @@
+# UI Styles (Dark)
+
+## Category List (QListWidget)
+
+- Background: `#2b2b2b`
+- Selection: blue `#3d5a80` + white text
+- Hover: gray `#3a3a3a`
+- Text: light gray `#e0e0e0`
+- Padding: 4px
+- Item margin: 2px vertical
+- View spacing: 0 (can be set to 6–10 for more breathing room)
+
+## Settings Area (QStackedWidget)
+
+- Background: `#333333`
+- Border: `#3d3d3d`
+- GroupBox: background `#3a3a3a`, border `#4a4a4a`, radius 6
+
+## Tuning Tips
+
+- Increase view spacing to 8–10 if preferred visually
+- Harmonize corner radius with global UI style
+- Use tooltips to explain parameters
diff --git a/tests/test_delimiters_category.py b/tests/test_delimiters_category.py
new file mode 100644
index 0000000..df5e950
--- /dev/null
+++ b/tests/test_delimiters_category.py
@@ -0,0 +1,257 @@
+"""Tests for the Delimiters category in settings dialog."""
+
+from unittest.mock import Mock, patch
+
+import pytest
+from PySide6 import QtWidgets
+
+from tidal_dl_ng.dialog import DialogPreferences
+from tidal_dl_ng.ui.dialog_settings import Ui_DialogSettings
+
+
+@pytest.fixture
+def qapp():
+ """Create QApplication instance for tests."""
+ app = QtWidgets.QApplication.instance()
+ if app is None:
+ app = QtWidgets.QApplication([])
+ yield app
+
+
+class TestDelimitersPage:
+ """Test the Delimiters page specifically."""
+
+ def test_delimiters_page_exists(self, qapp):
+ """Test that the Delimiters page exists."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert hasattr(ui, "page_delimiters")
+ assert ui.page_delimiters is not None
+
+ def test_delimiters_page_is_fifth(self, qapp):
+ """Test that the Delimiters page is at index 4."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.sw_categories.widget(4) == ui.page_delimiters
+
+ def test_delimiters_groupbox_exists(self, qapp):
+ """Test that the Delimiters GroupBox exists."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert hasattr(ui, "gb_delimiters")
+ assert ui.gb_delimiters is not None
+ assert isinstance(ui.gb_delimiters, QtWidgets.QGroupBox)
+
+ def test_delimiters_page_has_four_line_edits(self, qapp):
+ """Test that the Delimiters page has 4 line edits."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ line_edits = ui.page_delimiters.findChildren(QtWidgets.QLineEdit)
+ assert len(line_edits) == 4, "Delimiters page should have 4 line edits"
+
+ def test_all_delimiter_line_edits_accessible(self, qapp):
+ """Test that all delimiter line edits are accessible."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ line_edit_names = [
+ "le_metadata_delimiter_artist",
+ "le_metadata_delimiter_album_artist",
+ "le_filename_delimiter_artist",
+ "le_filename_delimiter_album_artist",
+ ]
+
+ for name in line_edit_names:
+ line_edit = getattr(ui, name, None)
+ assert line_edit is not None, f"LineEdit {name} not found"
+ assert isinstance(line_edit, QtWidgets.QLineEdit)
+
+ def test_delimiter_line_edits_have_max_width(self, qapp):
+ """Test that delimiter line edits have a maximum width set."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ line_edit_names = [
+ "le_metadata_delimiter_artist",
+ "le_metadata_delimiter_album_artist",
+ "le_filename_delimiter_artist",
+ "le_filename_delimiter_album_artist",
+ ]
+
+ for name in line_edit_names:
+ line_edit = getattr(ui, name)
+ # Should have max width of 100px
+ assert line_edit.maximumWidth() == 100, f"{name} should have max width of 100px"
+
+ def test_delimiter_labels_exist(self, qapp):
+ """Test that labels and icon labels exist for each delimiter."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ label_names = [
+ ("l_metadata_delimiter_artist", "l_icon_metadata_delimiter_artist"),
+ ("l_metadata_delimiter_album_artist", "l_icon_metadata_delimiter_album_artist"),
+ ("l_filename_delimiter_artist", "l_icon_filename_delimiter_artist"),
+ ("l_filename_delimiter_album_artist", "l_icon_filename_delimiter_album_artist"),
+ ]
+
+ for label_name, icon_label_name in label_names:
+ label = getattr(ui, label_name, None)
+ icon_label = getattr(ui, icon_label_name, None)
+
+ assert label is not None, f"Label {label_name} not found"
+ assert icon_label is not None, f"Icon label {icon_label_name} not found"
+ assert isinstance(label, QtWidgets.QLabel)
+ assert isinstance(icon_label, QtWidgets.QLabel)
+
+
+class TestDelimitersIntegration:
+ """Test Delimiters integration with DialogPreferences."""
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Create a mock Settings object with delimiter attributes."""
+ settings = Mock()
+ settings.data = Mock()
+
+ # Boolean flags
+ for attr in [
+ "lyrics_embed",
+ "lyrics_file",
+ "use_primary_album_artist",
+ "video_download",
+ "download_dolby_atmos",
+ "download_delay",
+ "video_convert_mp4",
+ "extract_flac",
+ "metadata_cover_embed",
+ "mark_explicit",
+ "cover_album_file",
+ "skip_existing",
+ "symlink_to_track",
+ "playlist_create",
+ ]:
+ setattr(settings.data, attr, False)
+
+ # Paths
+ for attr in [
+ "download_base_path",
+ "format_album",
+ "format_playlist",
+ "format_mix",
+ "format_track",
+ "format_video",
+ "path_binary_ffmpeg",
+ ]:
+ setattr(settings.data, attr, f"/test/{attr}")
+
+ # Delimiters
+ settings.data.metadata_delimiter_artist = ", "
+ settings.data.metadata_delimiter_album_artist = ", "
+ settings.data.filename_delimiter_artist = ", "
+ settings.data.filename_delimiter_album_artist = ", "
+
+ # Numbers
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ # Enums
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+ @patch.object(DialogPreferences, "exec")
+ def test_delimiters_category_added_to_list(self, mock_exec, mock_settings, qapp):
+ """Test that Delimiters category is added to the list."""
+ from PySide6.QtCore import Signal as QtSignal
+
+ settings_save = QtSignal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Should have 5 categories now
+ assert dialog.ui.lw_categories.count() == 5
+ assert dialog.ui.lw_categories.item(4).text() == "Delimiters"
+
+ @patch.object(DialogPreferences, "exec")
+ def test_delimiter_line_edits_in_parameters(self, mock_exec, mock_settings, qapp):
+ """Test that delimiter line edits are in parameters_line_edit."""
+ from PySide6.QtCore import Signal as QtSignal
+
+ settings_save = QtSignal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check that all 4 delimiters are in the list
+ assert "metadata_delimiter_artist" in dialog.parameters_line_edit
+ assert "metadata_delimiter_album_artist" in dialog.parameters_line_edit
+ assert "filename_delimiter_artist" in dialog.parameters_line_edit
+ assert "filename_delimiter_album_artist" in dialog.parameters_line_edit
+
+ @patch.object(DialogPreferences, "exec")
+ def test_navigate_to_delimiters_page(self, mock_exec, mock_settings, qapp):
+ """Test navigation to Delimiters page."""
+ from PySide6.QtCore import Signal as QtSignal
+
+ settings_save = QtSignal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Navigate to Delimiters (index 4)
+ dialog.ui.lw_categories.setCurrentRow(4)
+ assert dialog.ui.sw_categories.currentIndex() == 4
+ assert dialog.ui.sw_categories.currentWidget() == dialog.ui.page_delimiters
+
+
+class TestDelimitersStructure:
+ """Test the structural integrity of the Delimiters page."""
+
+ def test_stacked_widget_has_five_pages(self, qapp):
+ """Test that the stacked widget now contains 5 pages."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.sw_categories.count() == 5, "Should have 5 pages including Delimiters"
+
+ def test_delimiters_page_has_layout(self, qapp):
+ """Test that Delimiters page has a proper layout."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.page_delimiters.layout() is not None
+ assert isinstance(ui.page_delimiters.layout(), QtWidgets.QVBoxLayout)
+
+ def test_delimiters_page_has_spacer(self, qapp):
+ """Test that Delimiters page has a vertical spacer."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Check that there's a spacer in the layout
+ layout = ui.page_delimiters.layout()
+ spacer_found = False
+ for i in range(layout.count()):
+ item = layout.itemAt(i)
+ if item.spacerItem():
+ spacer_found = True
+ break
+
+ assert spacer_found, "Delimiters page should have a vertical spacer"
diff --git a/tests/test_dialog_preferences_integration.py b/tests/test_dialog_preferences_integration.py
new file mode 100644
index 0000000..b7c21c2
--- /dev/null
+++ b/tests/test_dialog_preferences_integration.py
@@ -0,0 +1,309 @@
+"""Integration tests for DialogPreferences with the refactored UI."""
+
+from unittest.mock import Mock, patch
+
+import pytest
+from PySide6 import QtCore, QtWidgets
+
+from tidal_dl_ng.config import Settings
+from tidal_dl_ng.dialog import DialogPreferences
+
+
+@pytest.fixture
+def qapp():
+ """Create QApplication instance for tests."""
+ app = QtWidgets.QApplication.instance()
+ if app is None:
+ app = QtWidgets.QApplication([])
+ yield app
+
+
+@pytest.fixture
+def mock_settings():
+ """Create a mock Settings object with all required attributes."""
+ settings = Mock(spec=Settings)
+ settings.data = Mock()
+
+ # Boolean flags
+ bool_attrs = [
+ "lyrics_embed",
+ "lyrics_file",
+ "use_primary_album_artist",
+ "video_download",
+ "download_dolby_atmos",
+ "download_delay",
+ "video_convert_mp4",
+ "extract_flac",
+ "metadata_cover_embed",
+ "mark_explicit",
+ "cover_album_file",
+ "skip_existing",
+ "symlink_to_track",
+ "playlist_create",
+ ]
+ for attr in bool_attrs:
+ setattr(settings.data, attr, False)
+
+ # String paths
+ path_attrs = [
+ "download_base_path",
+ "format_album",
+ "format_playlist",
+ "format_mix",
+ "format_track",
+ "format_video",
+ "path_binary_ffmpeg",
+ ]
+ for attr in path_attrs:
+ setattr(settings.data, attr, f"/test/{attr}")
+
+ # Numeric values
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ # Enum values
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+
+class TestDialogPreferencesInitialization:
+ """Test DialogPreferences initialization with refactored UI."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_dialog_initializes_with_ui(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that DialogPreferences initializes with the new UI."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ assert hasattr(dialog, "ui")
+ assert dialog.ui is not None
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_categories_initialized_correctly(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that categories are initialized correctly."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check categories count
+ assert dialog.ui.lw_categories.count() == 4
+
+ # Check categories text
+ categories = [dialog.ui.lw_categories.item(i).text() for i in range(4)]
+ assert categories == ["Flags", "Quality", "Numbers", "Paths & Formats"]
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_first_category_selected_by_default(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that the first category is selected on initialization."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ assert dialog.ui.lw_categories.currentRow() == 0
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+
+class TestDialogPreferencesCategoryNavigation:
+ """Test category navigation functionality."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_category_change_updates_page(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that changing category updates the displayed page."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Test each category
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_on_category_changed_signal_connected(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that the category change signal is properly connected."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Verify the connection by changing category programmatically
+ initial_page = dialog.ui.sw_categories.currentIndex()
+ dialog.ui.lw_categories.setCurrentRow(2)
+ new_page = dialog.ui.sw_categories.currentIndex()
+
+ assert initial_page != new_page
+ assert new_page == 2
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_navigate_through_all_categories_sequentially(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test sequential navigation through all categories."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Navigate forward
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+ assert dialog.ui.lw_categories.currentRow() == i
+
+ # Navigate backward
+ for i in range(3, -1, -1):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+ assert dialog.ui.lw_categories.currentRow() == i
+
+
+class TestDialogPreferencesWidgetAccess:
+ """Test access to widgets through DialogPreferences."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_flags_checkboxes(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that flag checkboxes are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ checkbox_names = ["cb_video_download", "cb_lyrics_embed", "cb_skip_existing"]
+
+ for name in checkbox_names:
+ checkbox = getattr(dialog.ui, name, None)
+ assert checkbox is not None
+ assert isinstance(checkbox, QtWidgets.QCheckBox)
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_quality_combos(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that quality combo boxes are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ combo_names = ["c_quality_audio", "c_quality_video", "c_metadata_cover_dimension"]
+
+ for name in combo_names:
+ combo = getattr(dialog.ui, name, None)
+ assert combo is not None
+ assert isinstance(combo, QtWidgets.QComboBox)
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_numbers_spinboxes(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that number spinboxes are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ spinbox_names = ["sb_album_track_num_pad_min", "sb_downloads_concurrent_max"]
+
+ for name in spinbox_names:
+ spinbox = getattr(dialog.ui, name, None)
+ assert spinbox is not None
+ assert isinstance(spinbox, QtWidgets.QSpinBox)
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_path_line_edits(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that path line edits are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ line_edit_names = ["le_download_base_path", "le_format_track", "le_path_binary_ffmpeg"]
+
+ for name in line_edit_names:
+ line_edit = getattr(dialog.ui, name, None)
+ assert line_edit is not None
+ assert isinstance(line_edit, QtWidgets.QLineEdit)
+
+
+class TestDialogPreferencesPageVisibility:
+ """Test that pages are correctly shown/hidden based on category selection."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_only_current_page_visible(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that only the current page is visible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ current_widget = dialog.ui.sw_categories.currentWidget()
+
+ # Check that the current widget is the expected page
+ if i == 0:
+ assert current_widget == dialog.ui.page_flags
+ elif i == 1:
+ assert current_widget == dialog.ui.page_quality
+ elif i == 2:
+ assert current_widget == dialog.ui.page_numbers
+ elif i == 3:
+ assert current_widget == dialog.ui.page_paths
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_page_content_accessible_when_selected(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that page content is accessible when the page is selected."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Select Flags page
+ dialog.ui.lw_categories.setCurrentRow(0)
+ assert dialog.ui.gb_flags.isVisibleTo(dialog.ui.page_flags)
+
+ # Select Quality page
+ dialog.ui.lw_categories.setCurrentRow(1)
+ assert dialog.ui.gb_choices.isVisibleTo(dialog.ui.page_quality)
+
+ # Select Numbers page
+ dialog.ui.lw_categories.setCurrentRow(2)
+ assert dialog.ui.gb_numbers.isVisibleTo(dialog.ui.page_numbers)
+
+ # Select Paths page
+ dialog.ui.lw_categories.setCurrentRow(3)
+ assert dialog.ui.gb_path.isVisibleTo(dialog.ui.page_paths)
+
+
+class TestDialogPreferencesMethodsStillWork:
+ """Test that existing DialogPreferences methods still work with new UI."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_init_methods_called(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that initialization methods are called."""
+ settings_save = QtCore.Signal()
+
+ with (
+ patch.object(DialogPreferences, "_init_checkboxes") as mock_init_cb,
+ patch.object(DialogPreferences, "_init_comboboxes") as mock_init_combo,
+ patch.object(DialogPreferences, "_init_line_edit") as mock_init_le,
+ patch.object(DialogPreferences, "_init_spin_box") as mock_init_sb,
+ patch.object(DialogPreferences, "_init_categories") as mock_init_cat,
+ ):
+
+ # Instantiate to trigger init methods; no need to keep a variable
+ DialogPreferences(mock_settings, settings_save)
+
+ mock_init_cb.assert_called_once()
+ mock_init_combo.assert_called_once()
+ mock_init_le.assert_called_once()
+ mock_init_sb.assert_called_once()
+ mock_init_cat.assert_called_once()
+
+ @patch.object(DialogPreferences, "exec")
+ def test_on_category_changed_method_exists(self, mock_exec, mock_settings, qapp):
+ """Test that _on_category_changed method exists and is callable."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ assert hasattr(dialog, "_on_category_changed")
+ assert callable(dialog._on_category_changed)
+
+ # Test calling it doesn't raise an error
+ dialog._on_category_changed(0)
+ dialog._on_category_changed(1)
+ dialog._on_category_changed(2)
+ dialog._on_category_changed(3)
diff --git a/tests/test_settings_dialog_structure.py b/tests/test_settings_dialog_structure.py
new file mode 100644
index 0000000..726776b
--- /dev/null
+++ b/tests/test_settings_dialog_structure.py
@@ -0,0 +1,277 @@
+"""Tests for the settings dialog category structure and organization."""
+
+import pytest
+from PySide6 import QtCore, QtWidgets
+
+from tidal_dl_ng.ui.dialog_settings import Ui_DialogSettings
+
+
+@pytest.fixture
+def qapp():
+ """Create QApplication instance for tests."""
+ app = QtWidgets.QApplication.instance()
+ if app is None:
+ app = QtWidgets.QApplication([])
+ yield app
+
+
+class TestSettingsDialogStructure:
+ """Test the structural integrity of the settings dialog."""
+
+ def test_dialog_has_proper_layout_hierarchy(self, qapp):
+ """Test that the dialog has the correct layout hierarchy."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Check main layout
+ main_layout = dialog.layout()
+ assert main_layout is not None
+ assert isinstance(main_layout, QtWidgets.QVBoxLayout)
+
+ def test_categories_and_pages_match(self, qapp):
+ """Test that the number of pages matches the expected categories."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # We expect 4 categories/pages
+ expected_categories = ["Flags", "Quality", "Numbers", "Paths & Formats"]
+ assert ui.sw_categories.count() == len(expected_categories)
+
+ def test_all_pages_have_layouts(self, qapp):
+ """Test that all pages in the stacked widget have proper layouts."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ for i in range(ui.sw_categories.count()):
+ page = ui.sw_categories.widget(i)
+ assert page is not None
+ assert page.layout() is not None
+ assert isinstance(page.layout(), QtWidgets.QVBoxLayout)
+
+ def test_all_pages_have_group_boxes(self, qapp):
+ """Test that all pages contain at least one group box."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ for i in range(ui.sw_categories.count()):
+ page = ui.sw_categories.widget(i)
+ group_boxes = page.findChildren(QtWidgets.QGroupBox)
+ assert len(group_boxes) > 0, f"Page {i} should have at least one group box"
+
+
+class TestSettingsFlagsPage:
+ """Test the Flags page specifically."""
+
+ def test_flags_page_is_first(self, qapp):
+ """Test that the Flags page is at index 0."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.page_flags is not None
+ assert ui.sw_categories.widget(0) == ui.page_flags
+
+ def test_flags_page_has_correct_number_of_checkboxes(self, qapp):
+ """Test that the Flags page has the expected number of checkboxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Count checkboxes in the flags page
+ checkboxes = ui.page_flags.findChildren(QtWidgets.QCheckBox)
+ assert len(checkboxes) == 14, "Flags page should have 14 checkboxes"
+
+ def test_all_flags_checkboxes_accessible_via_ui(self, qapp):
+ """Test that all flag checkboxes are accessible through the ui object."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ checkbox_names = [
+ "cb_video_download",
+ "cb_video_convert_mp4",
+ "cb_lyrics_embed",
+ "cb_lyrics_file",
+ "cb_download_delay",
+ "cb_extract_flac",
+ "cb_metadata_cover_embed",
+ "cb_cover_album_file",
+ "cb_skip_existing",
+ "cb_symlink_to_track",
+ "cb_playlist_create",
+ "cb_mark_explicit",
+ "cb_use_primary_album_artist",
+ "cb_download_dolby_atmos",
+ ]
+
+ for name in checkbox_names:
+ checkbox = getattr(ui, name, None)
+ assert checkbox is not None, f"Checkbox {name} not found"
+ assert isinstance(checkbox, QtWidgets.QCheckBox)
+
+
+class TestSettingsQualityPage:
+ """Test the Quality page specifically."""
+
+ def test_quality_page_is_second(self, qapp):
+ """Test that the Quality page is at index 1."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.page_quality is not None
+ assert ui.sw_categories.widget(1) == ui.page_quality
+
+ def test_quality_page_has_combo_boxes(self, qapp):
+ """Test that the Quality page has combo boxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ combos = ui.page_quality.findChildren(QtWidgets.QComboBox)
+ assert len(combos) == 3, "Quality page should have 3 combo boxes"
+
+ def test_quality_combo_boxes_have_labels(self, qapp):
+ """Test that each combo box has associated labels."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ combo_label_pairs = [
+ ("c_quality_audio", "l_quality_audio", "l_icon_quality_audio"),
+ ("c_quality_video", "l_quality_video", "l_icon_quality_video"),
+ ("c_metadata_cover_dimension", "l_metadata_cover_dimension", "l_icon_metadata_cover_dimension"),
+ ]
+
+ for combo_name, label_name, icon_label_name in combo_label_pairs:
+ combo = getattr(ui, combo_name, None)
+ label = getattr(ui, label_name, None)
+ icon_label = getattr(ui, icon_label_name, None)
+
+ assert combo is not None, f"ComboBox {combo_name} not found"
+ assert label is not None, f"Label {label_name} not found"
+ assert icon_label is not None, f"Icon label {icon_label_name} not found"
+
+
+class TestSettingsNumbersPage:
+ """Test the Numbers page specifically."""
+
+ def test_numbers_page_is_third(self, qapp):
+ """Test that the Numbers page is at index 2."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.page_numbers is not None
+ assert ui.sw_categories.widget(2) == ui.page_numbers
+
+ def test_numbers_page_has_spin_boxes(self, qapp):
+ """Test that the Numbers page has spin boxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ spinboxes = ui.page_numbers.findChildren(QtWidgets.QSpinBox)
+ assert len(spinboxes) == 2, "Numbers page should have 2 spin boxes"
+
+ def test_spin_boxes_have_valid_ranges(self, qapp):
+ """Test that spin boxes have appropriate min/max values."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test album track num padding
+ assert ui.sb_album_track_num_pad_min.maximum() == 4
+ assert ui.sb_album_track_num_pad_min.minimum() >= 0
+
+ # Test concurrent downloads
+ assert ui.sb_downloads_concurrent_max.minimum() == 1
+ assert ui.sb_downloads_concurrent_max.maximum() == 5
+
+
+class TestSettingsPathsPage:
+ """Test the Paths & Formats page specifically."""
+
+ def test_paths_page_is_fourth(self, qapp):
+ """Test that the Paths page is at index 3."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.page_paths is not None
+ assert ui.sw_categories.widget(3) == ui.page_paths
+
+ def test_paths_page_has_line_edits(self, qapp):
+ """Test that the Paths page has line edits."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ line_edits = ui.page_paths.findChildren(QtWidgets.QLineEdit)
+ assert len(line_edits) == 7, "Paths page should have 7 line edits"
+
+ def test_paths_page_has_browse_buttons(self, qapp):
+ """Test that the Paths page has browse buttons."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ buttons = ui.page_paths.findChildren(QtWidgets.QPushButton)
+ # Should have at least 2 browse buttons (... buttons)
+ assert len(buttons) >= 2, "Paths page should have at least 2 browse buttons"
+
+ def test_path_line_edits_accessible(self, qapp):
+ """Test that all path-related line edits are accessible."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ line_edit_names = [
+ "le_download_base_path",
+ "le_format_track",
+ "le_format_video",
+ "le_format_album",
+ "le_format_playlist",
+ "le_format_mix",
+ "le_path_binary_ffmpeg",
+ ]
+
+ for name in line_edit_names:
+ line_edit = getattr(ui, name, None)
+ assert line_edit is not None, f"LineEdit {name} not found"
+ assert isinstance(line_edit, QtWidgets.QLineEdit)
+
+
+class TestSettingsDialogButtons:
+ """Test the dialog button box."""
+
+ def test_dialog_has_button_box(self, qapp):
+ """Test that the dialog has a button box."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.bb_dialog is not None
+ assert isinstance(ui.bb_dialog, QtWidgets.QDialogButtonBox)
+
+ def test_button_box_has_ok_and_cancel(self, qapp):
+ """Test that the button box has OK and Cancel buttons."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ buttons = ui.bb_dialog.standardButtons()
+ assert QtWidgets.QDialogButtonBox.StandardButton.Ok in buttons
+ assert QtWidgets.QDialogButtonBox.StandardButton.Cancel in buttons
+
+ def test_button_box_orientation(self, qapp):
+ """Test that the button box has horizontal orientation."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.bb_dialog.orientation() == QtCore.Qt.Orientation.Horizontal
diff --git a/tests/test_settings_ui.py b/tests/test_settings_ui.py
new file mode 100644
index 0000000..8890500
--- /dev/null
+++ b/tests/test_settings_ui.py
@@ -0,0 +1,387 @@
+"""Tests for the refactored settings dialog UI."""
+
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+import pytest
+from PySide6 import QtCore, QtWidgets
+
+from tidal_dl_ng.dialog import DialogPreferences
+from tidal_dl_ng.ui.dialog_settings import Ui_DialogSettings
+
+
+@pytest.fixture
+def qapp():
+ """Create QApplication instance for tests."""
+ app = QtWidgets.QApplication.instance()
+ if app is None:
+ app = QtWidgets.QApplication([])
+ yield app
+ # Cleanup is handled by pytest-qt if available, otherwise by QApplication
+
+
+class TestUiDialogSettings:
+ """Test the generated UI class."""
+
+ def test_ui_setupui_creates_all_widgets(self, qapp):
+ """Test that setupUi creates all expected widgets."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Check main structural widgets
+ assert hasattr(ui, "lw_categories")
+ assert hasattr(ui, "sw_categories")
+ assert hasattr(ui, "bb_dialog")
+
+ # Check pages exist
+ assert hasattr(ui, "page_flags")
+ assert hasattr(ui, "page_quality")
+ assert hasattr(ui, "page_numbers")
+ assert hasattr(ui, "page_paths")
+
+ def test_ui_list_widget_properties(self, qapp):
+ """Test that the categories list widget has correct properties."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.lw_categories is not None
+ assert isinstance(ui.lw_categories, QtWidgets.QListWidget)
+ assert ui.lw_categories.minimumWidth() == 150
+ assert ui.lw_categories.maximumWidth() == 200
+
+ def test_ui_stacked_widget_has_all_pages(self, qapp):
+ """Test that the stacked widget contains all 4 pages."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.sw_categories is not None
+ assert isinstance(ui.sw_categories, QtWidgets.QStackedWidget)
+ assert ui.sw_categories.count() == 4
+
+ def test_ui_flags_page_widgets(self, qapp):
+ """Test that the Flags page contains all expected checkboxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test flags group box exists
+ assert hasattr(ui, "gb_flags")
+ assert ui.gb_flags is not None
+
+ # Test all checkboxes exist
+ checkboxes = [
+ "cb_video_download",
+ "cb_video_convert_mp4",
+ "cb_lyrics_embed",
+ "cb_lyrics_file",
+ "cb_download_delay",
+ "cb_extract_flac",
+ "cb_metadata_cover_embed",
+ "cb_cover_album_file",
+ "cb_skip_existing",
+ "cb_symlink_to_track",
+ "cb_playlist_create",
+ "cb_mark_explicit",
+ "cb_use_primary_album_artist",
+ "cb_download_dolby_atmos",
+ ]
+
+ for cb_name in checkboxes:
+ assert hasattr(ui, cb_name), f"Missing checkbox: {cb_name}"
+ checkbox = getattr(ui, cb_name)
+ assert isinstance(checkbox, QtWidgets.QCheckBox)
+
+ def test_ui_quality_page_widgets(self, qapp):
+ """Test that the Quality page contains all expected combo boxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test choices group box exists
+ assert hasattr(ui, "gb_choices")
+ assert ui.gb_choices is not None
+
+ # Test all combo boxes exist
+ combos = [
+ "c_quality_audio",
+ "c_quality_video",
+ "c_metadata_cover_dimension",
+ ]
+
+ for combo_name in combos:
+ assert hasattr(ui, combo_name), f"Missing combo box: {combo_name}"
+ combo = getattr(ui, combo_name)
+ assert isinstance(combo, QtWidgets.QComboBox)
+
+ def test_ui_numbers_page_widgets(self, qapp):
+ """Test that the Numbers page contains all expected spin boxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test numbers group box exists
+ assert hasattr(ui, "gb_numbers")
+ assert ui.gb_numbers is not None
+
+ # Test all spin boxes exist
+ spinboxes = [
+ "sb_album_track_num_pad_min",
+ "sb_downloads_concurrent_max",
+ ]
+
+ for sb_name in spinboxes:
+ assert hasattr(ui, sb_name), f"Missing spin box: {sb_name}"
+ spinbox = getattr(ui, sb_name)
+ assert isinstance(spinbox, QtWidgets.QSpinBox)
+
+ def test_ui_paths_page_widgets(self, qapp):
+ """Test that the Paths page contains all expected line edits and buttons."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test path group box exists
+ assert hasattr(ui, "gb_path")
+ assert ui.gb_path is not None
+
+ # Test all line edits exist
+ line_edits = [
+ "le_download_base_path",
+ "le_format_track",
+ "le_format_video",
+ "le_format_album",
+ "le_format_playlist",
+ "le_format_mix",
+ "le_path_binary_ffmpeg",
+ ]
+
+ for le_name in line_edits:
+ assert hasattr(ui, le_name), f"Missing line edit: {le_name}"
+ line_edit = getattr(ui, le_name)
+ assert isinstance(line_edit, QtWidgets.QLineEdit)
+
+ # Test buttons exist
+ assert hasattr(ui, "pb_download_base_path")
+ assert hasattr(ui, "pb_path_binary_ffmpeg")
+
+ def test_ui_dialog_button_box(self, qapp):
+ """Test that the dialog has OK and Cancel buttons."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.bb_dialog is not None
+ assert isinstance(ui.bb_dialog, QtWidgets.QDialogButtonBox)
+ # Check that standard buttons include OK and Cancel
+ buttons = ui.bb_dialog.standardButtons()
+ assert QtWidgets.QDialogButtonBox.StandardButton.Ok in buttons
+ assert QtWidgets.QDialogButtonBox.StandardButton.Cancel in buttons
+
+
+class TestDialogPreferencesCategories:
+ """Test the DialogPreferences category initialization and navigation."""
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Create a mock Settings object."""
+ settings = Mock()
+ settings.data = Mock()
+
+ # Set default values for all parameters
+ settings.data.lyrics_embed = False
+ settings.data.lyrics_file = False
+ settings.data.use_primary_album_artist = False
+ settings.data.video_download = False
+ settings.data.download_dolby_atmos = False
+ settings.data.download_delay = False
+ settings.data.video_convert_mp4 = False
+ settings.data.extract_flac = False
+ settings.data.metadata_cover_embed = False
+ settings.data.mark_explicit = False
+ settings.data.cover_album_file = False
+ settings.data.skip_existing = False
+ settings.data.symlink_to_track = False
+ settings.data.playlist_create = False
+
+ settings.data.download_base_path = str(Path.home())
+ settings.data.format_album = "{artist}/{album}"
+ settings.data.format_playlist = "Playlists/{name}"
+ settings.data.format_mix = "Mixes/{name}"
+ settings.data.format_track = "{title}"
+ settings.data.format_video = "{title}"
+ settings.data.path_binary_ffmpeg = "ffmpeg"
+
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+ @patch.object(DialogPreferences, "exec")
+ def test_init_categories_adds_all_items(self, mock_exec, mock_settings, qapp):
+ """Test that _init_categories adds all 4 categories to the list."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check that all categories were added
+ assert dialog.ui.lw_categories.count() == 4
+ assert dialog.ui.lw_categories.item(0).text() == "Flags"
+ assert dialog.ui.lw_categories.item(1).text() == "Quality"
+ assert dialog.ui.lw_categories.item(2).text() == "Numbers"
+ assert dialog.ui.lw_categories.item(3).text() == "Paths & Formats"
+
+ @patch.object(DialogPreferences, "exec")
+ def test_init_categories_sets_first_selected(self, mock_exec, mock_settings, qapp):
+ """Test that the first category is selected by default."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check that first item is selected
+ assert dialog.ui.lw_categories.currentRow() == 0
+
+ @patch.object(DialogPreferences, "exec")
+ def test_category_change_switches_page(self, mock_exec, mock_settings, qapp):
+ """Test that changing category switches the stacked widget page."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Initially on page 0
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+ # Change to category 1 (Quality)
+ dialog.ui.lw_categories.setCurrentRow(1)
+ assert dialog.ui.sw_categories.currentIndex() == 1
+
+ # Change to category 2 (Numbers)
+ dialog.ui.lw_categories.setCurrentRow(2)
+ assert dialog.ui.sw_categories.currentIndex() == 2
+
+ # Change to category 3 (Paths & Formats)
+ dialog.ui.lw_categories.setCurrentRow(3)
+ assert dialog.ui.sw_categories.currentIndex() == 3
+
+ # Back to category 0 (Flags)
+ dialog.ui.lw_categories.setCurrentRow(0)
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+ @patch.object(DialogPreferences, "exec")
+ def test_on_category_changed_method(self, mock_exec, mock_settings, qapp):
+ """Test that _on_category_changed method works correctly."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Test method directly
+ dialog._on_category_changed(0)
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+ dialog._on_category_changed(1)
+ assert dialog.ui.sw_categories.currentIndex() == 1
+
+ dialog._on_category_changed(2)
+ assert dialog.ui.sw_categories.currentIndex() == 2
+
+ dialog._on_category_changed(3)
+ assert dialog.ui.sw_categories.currentIndex() == 3
+
+
+class TestDialogPreferencesIntegration:
+ """Integration tests for the complete dialog functionality."""
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Create a mock Settings object with all required attributes."""
+ settings = Mock()
+ settings.data = Mock()
+
+ # Set all required attributes
+ for attr in [
+ "lyrics_embed",
+ "lyrics_file",
+ "use_primary_album_artist",
+ "video_download",
+ "download_dolby_atmos",
+ "download_delay",
+ "video_convert_mp4",
+ "extract_flac",
+ "metadata_cover_embed",
+ "mark_explicit",
+ "cover_album_file",
+ "skip_existing",
+ "symlink_to_track",
+ "playlist_create",
+ ]:
+ setattr(settings.data, attr, False)
+
+ for attr in [
+ "download_base_path",
+ "format_album",
+ "format_playlist",
+ "format_mix",
+ "format_track",
+ "format_video",
+ "path_binary_ffmpeg",
+ ]:
+ setattr(settings.data, attr, f"/test/{attr}")
+
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+ @patch.object(DialogPreferences, "exec")
+ def test_dialog_structure_is_correct(self, mock_exec, mock_settings, qapp):
+ """Test that the complete dialog structure is correctly set up."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check dialog has UI
+ assert hasattr(dialog, "ui")
+ assert isinstance(dialog.ui, Ui_DialogSettings)
+
+ # Check categories list is populated
+ assert dialog.ui.lw_categories.count() == 4
+
+ # Check stacked widget has 4 pages
+ assert dialog.ui.sw_categories.count() == 4
+
+ # Check all pages are accessible
+ for i in range(4):
+ page = dialog.ui.sw_categories.widget(i)
+ assert page is not None
+
+ @patch.object(DialogPreferences, "exec")
+ def test_navigation_between_all_categories(self, mock_exec, mock_settings, qapp):
+ """Test that navigation works correctly between all categories."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Navigate through all categories
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+
+ # Check that the correct page is visible
+ current_widget = dialog.ui.sw_categories.currentWidget()
+ assert current_widget is not None
diff --git a/tidal_dl_ng/dialog.py b/tidal_dl_ng/dialog.py
index 8cb93f6..7d6a93f 100644
--- a/tidal_dl_ng/dialog.py
+++ b/tidal_dl_ng/dialog.py
@@ -144,6 +144,10 @@ class DialogPreferences(QtWidgets.QDialog):
# Run the .setupUi() method to show the GUI
self.ui.setupUi(self)
+
+ # Initialize categories
+ self._init_categories()
+
# Set data.
self.gui_populate()
# Post setup
@@ -159,6 +163,10 @@ class DialogPreferences(QtWidgets.QDialog):
"format_track",
"format_video",
"path_binary_ffmpeg",
+ "metadata_delimiter_artist",
+ "metadata_delimiter_album_artist",
+ "filename_delimiter_artist",
+ "filename_delimiter_album_artist",
]
def _init_spin_box(self):
@@ -189,6 +197,25 @@ class DialogPreferences(QtWidgets.QDialog):
"playlist_create",
]
+ def _init_categories(self):
+ """Initialize the categories list and connect to page switching."""
+ # Add categories to the list
+ self.ui.lw_categories.addItem("Flags")
+ self.ui.lw_categories.addItem("Quality")
+ self.ui.lw_categories.addItem("Numbers")
+ self.ui.lw_categories.addItem("Paths & Formats")
+ self.ui.lw_categories.addItem("Delimiters")
+
+ # Set the first category as selected
+ self.ui.lw_categories.setCurrentRow(0)
+
+ # Connect category selection to page switching
+ self.ui.lw_categories.currentRowChanged.connect(self._on_category_changed)
+
+ def _on_category_changed(self, index: int):
+ """Handle category change to switch pages."""
+ self.ui.sw_categories.setCurrentIndex(index)
+
def gui_populate(self):
self.populate_checkboxes()
self.populate_combo()
diff --git a/tidal_dl_ng/ui/dialog_settings.py b/tidal_dl_ng/ui/dialog_settings.py
index b7ae709..266c4d1 100644
--- a/tidal_dl_ng/ui/dialog_settings.py
+++ b/tidal_dl_ng/ui/dialog_settings.py
@@ -6,20 +6,26 @@
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
-from PySide6.QtCore import QCoreApplication, QMetaObject, Qt
+from PySide6.QtCore import QCoreApplication, QMetaObject, QSize, Qt
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialogButtonBox,
+ QFrame,
QGroupBox,
QHBoxLayout,
QLabel,
QLayout,
QLineEdit,
+ QListView,
+ QListWidget,
QPushButton,
QSizePolicy,
+ QSpacerItem,
QSpinBox,
+ QStackedWidget,
QVBoxLayout,
+ QWidget,
)
@@ -27,7 +33,7 @@ class Ui_DialogSettings:
def setupUi(self, DialogSettings):
if not DialogSettings.objectName():
DialogSettings.setObjectName("DialogSettings")
- DialogSettings.resize(640, 832)
+ DialogSettings.resize(808, 379)
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
sizePolicy.setHorizontalStretch(100)
sizePolicy.setVerticalStretch(100)
@@ -40,13 +46,100 @@ class Ui_DialogSettings:
self.lv_main = QVBoxLayout()
self.lv_main.setObjectName("lv_main")
self.lv_main.setContentsMargins(12, 12, 12, 12)
- self.gb_flags = QGroupBox(DialogSettings)
+ self.lh_main_content = QHBoxLayout()
+ self.lh_main_content.setObjectName("lh_main_content")
+ self.lw_categories = QListWidget(DialogSettings)
+ self.lw_categories.setObjectName("lw_categories")
+ sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
+ sizePolicy1.setHorizontalStretch(0)
+ sizePolicy1.setVerticalStretch(0)
+ sizePolicy1.setHeightForWidth(self.lw_categories.sizePolicy().hasHeightForWidth())
+ self.lw_categories.setSizePolicy(sizePolicy1)
+ self.lw_categories.setMinimumSize(QSize(150, 0))
+ self.lw_categories.setMaximumSize(QSize(200, 16777215))
+ self.lw_categories.setStyleSheet(
+ "QListWidget {\n"
+ " background-color: #2b2b2b;\n"
+ " border: 1px solid #3d3d3d;\n"
+ " border-radius: 4px;\n"
+ " padding: 4px;\n"
+ " outline: none;\n"
+ "}\n"
+ "\n"
+ "QListWidget::item {\n"
+ " padding: 4px 3px;\n"
+ " border-radius: 3px;\n"
+ " margin: 2px 0px;\n"
+ " color: #e0e0e0;\n"
+ "}\n"
+ "\n"
+ "QListWidget::item:selected {\n"
+ " background-color: #3d5a80;\n"
+ " color: white;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ "\n"
+ "QListWidget::item:hover:!selected {\n"
+ " background-color: #3a3a3a;\n"
+ " color: white;\n"
+ "}\n"
+ "\n"
+ "QListWidget::item:focus {\n"
+ " outline: none;\n"
+ "}"
+ )
+ self.lw_categories.setFrameShape(QFrame.Shape.StyledPanel)
+ self.lw_categories.setFrameShadow(QFrame.Shadow.Raised)
+ self.lw_categories.setMidLineWidth(-1)
+ self.lw_categories.setResizeMode(QListView.ResizeMode.Adjust)
+ self.lw_categories.setSpacing(0)
+
+ self.lh_main_content.addWidget(self.lw_categories)
+
+ self.sw_categories = QStackedWidget(DialogSettings)
+ self.sw_categories.setObjectName("sw_categories")
+ self.sw_categories.setStyleSheet(
+ "QStackedWidget {\n"
+ " background-color: #333333;\n"
+ " border: 1px solid #3d3d3d;\n"
+ " border-radius: 4px;\n"
+ "}\n"
+ "\n"
+ "QWidget {\n"
+ " background-color: transparent;\n"
+ "}\n"
+ "\n"
+ "QGroupBox {\n"
+ " background-color: #3a3a3a;\n"
+ " border: 1px solid #4a4a4a;\n"
+ " border-radius: 6px;\n"
+ " margin-top: 12px;\n"
+ " padding-top: 12px;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ "\n"
+ "QGroupBox::title {\n"
+ " subcontrol-origin: margin;\n"
+ " subcontrol-position: top left;\n"
+ " padding: 4px 8px;\n"
+ " background-color: #3a3a3a;\n"
+ " border-radius: 4px;\n"
+ "}"
+ )
+ self.sw_categories.setFrameShape(QFrame.Shape.StyledPanel)
+ self.sw_categories.setFrameShadow(QFrame.Shadow.Plain)
+ self.sw_categories.setMidLineWidth(1)
+ self.page_flags = QWidget()
+ self.page_flags.setObjectName("page_flags")
+ self.lv_page_flags = QVBoxLayout(self.page_flags)
+ self.lv_page_flags.setObjectName("lv_page_flags")
+ self.gb_flags = QGroupBox(self.page_flags)
self.gb_flags.setObjectName("gb_flags")
- sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- sizePolicy1.setHorizontalStretch(100)
- sizePolicy1.setVerticalStretch(100)
- sizePolicy1.setHeightForWidth(self.gb_flags.sizePolicy().hasHeightForWidth())
- self.gb_flags.setSizePolicy(sizePolicy1)
+ sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ sizePolicy2.setHorizontalStretch(100)
+ sizePolicy2.setVerticalStretch(100)
+ sizePolicy2.setHeightForWidth(self.gb_flags.sizePolicy().hasHeightForWidth())
+ self.gb_flags.setSizePolicy(sizePolicy2)
self.gb_flags.setFlat(False)
self.gb_flags.setCheckable(False)
self.lv_flags = QVBoxLayout(self.gb_flags)
@@ -57,8 +150,8 @@ class Ui_DialogSettings:
self.lv_flag_video_download.setObjectName("lv_flag_video_download")
self.cb_video_download = QCheckBox(self.gb_flags)
self.cb_video_download.setObjectName("cb_video_download")
- sizePolicy1.setHeightForWidth(self.cb_video_download.sizePolicy().hasHeightForWidth())
- self.cb_video_download.setSizePolicy(sizePolicy1)
+ sizePolicy2.setHeightForWidth(self.cb_video_download.sizePolicy().hasHeightForWidth())
+ self.cb_video_download.setSizePolicy(sizePolicy2)
self.lv_flag_video_download.addWidget(self.cb_video_download)
@@ -68,8 +161,8 @@ class Ui_DialogSettings:
self.lv_flag_video_convert.setObjectName("lv_flag_video_convert")
self.cb_video_convert_mp4 = QCheckBox(self.gb_flags)
self.cb_video_convert_mp4.setObjectName("cb_video_convert_mp4")
- sizePolicy1.setHeightForWidth(self.cb_video_convert_mp4.sizePolicy().hasHeightForWidth())
- self.cb_video_convert_mp4.setSizePolicy(sizePolicy1)
+ sizePolicy2.setHeightForWidth(self.cb_video_convert_mp4.sizePolicy().hasHeightForWidth())
+ self.cb_video_convert_mp4.setSizePolicy(sizePolicy2)
self.lv_flag_video_convert.addWidget(self.cb_video_convert_mp4)
@@ -83,11 +176,11 @@ class Ui_DialogSettings:
self.lv_flag_lyrics_embed.setObjectName("lv_flag_lyrics_embed")
self.cb_lyrics_embed = QCheckBox(self.gb_flags)
self.cb_lyrics_embed.setObjectName("cb_lyrics_embed")
- sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- sizePolicy2.setHorizontalStretch(0)
- sizePolicy2.setVerticalStretch(0)
- sizePolicy2.setHeightForWidth(self.cb_lyrics_embed.sizePolicy().hasHeightForWidth())
- self.cb_lyrics_embed.setSizePolicy(sizePolicy2)
+ sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ sizePolicy3.setHorizontalStretch(0)
+ sizePolicy3.setVerticalStretch(0)
+ sizePolicy3.setHeightForWidth(self.cb_lyrics_embed.sizePolicy().hasHeightForWidth())
+ self.cb_lyrics_embed.setSizePolicy(sizePolicy3)
self.lv_flag_lyrics_embed.addWidget(self.cb_lyrics_embed)
@@ -97,8 +190,8 @@ class Ui_DialogSettings:
self.lv_flag_lyrics_file.setObjectName("lv_flag_lyrics_file")
self.cb_lyrics_file = QCheckBox(self.gb_flags)
self.cb_lyrics_file.setObjectName("cb_lyrics_file")
- sizePolicy1.setHeightForWidth(self.cb_lyrics_file.sizePolicy().hasHeightForWidth())
- self.cb_lyrics_file.setSizePolicy(sizePolicy1)
+ sizePolicy2.setHeightForWidth(self.cb_lyrics_file.sizePolicy().hasHeightForWidth())
+ self.cb_lyrics_file.setSizePolicy(sizePolicy2)
self.lv_flag_lyrics_file.addWidget(self.cb_lyrics_file)
@@ -112,8 +205,8 @@ class Ui_DialogSettings:
self.lv_flag_download_delay.setObjectName("lv_flag_download_delay")
self.cb_download_delay = QCheckBox(self.gb_flags)
self.cb_download_delay.setObjectName("cb_download_delay")
- sizePolicy1.setHeightForWidth(self.cb_download_delay.sizePolicy().hasHeightForWidth())
- self.cb_download_delay.setSizePolicy(sizePolicy1)
+ sizePolicy2.setHeightForWidth(self.cb_download_delay.sizePolicy().hasHeightForWidth())
+ self.cb_download_delay.setSizePolicy(sizePolicy2)
self.lv_flag_download_delay.addWidget(self.cb_download_delay)
@@ -123,8 +216,8 @@ class Ui_DialogSettings:
self.lv_flag_extract_flac.setObjectName("lv_flag_extract_flac")
self.cb_extract_flac = QCheckBox(self.gb_flags)
self.cb_extract_flac.setObjectName("cb_extract_flac")
- sizePolicy2.setHeightForWidth(self.cb_extract_flac.sizePolicy().hasHeightForWidth())
- self.cb_extract_flac.setSizePolicy(sizePolicy2)
+ sizePolicy3.setHeightForWidth(self.cb_extract_flac.sizePolicy().hasHeightForWidth())
+ self.cb_extract_flac.setSizePolicy(sizePolicy3)
self.lv_flag_extract_flac.addWidget(self.cb_extract_flac)
@@ -220,33 +313,42 @@ class Ui_DialogSettings:
self.lv_flags.addLayout(self.horizontalLayout_13)
- self.lv_main.addWidget(self.gb_flags)
+ self.lv_page_flags.addWidget(self.gb_flags)
- self.gb_choices = QGroupBox(DialogSettings)
+ self.vs_page_flags = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.lv_page_flags.addItem(self.vs_page_flags)
+
+ self.sw_categories.addWidget(self.page_flags)
+ self.page_quality = QWidget()
+ self.page_quality.setObjectName("page_quality")
+ self.lv_page_quality = QVBoxLayout(self.page_quality)
+ self.lv_page_quality.setObjectName("lv_page_quality")
+ self.gb_choices = QGroupBox(self.page_quality)
self.gb_choices.setObjectName("gb_choices")
- sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
- sizePolicy3.setHorizontalStretch(0)
- sizePolicy3.setVerticalStretch(0)
- sizePolicy3.setHeightForWidth(self.gb_choices.sizePolicy().hasHeightForWidth())
- self.gb_choices.setSizePolicy(sizePolicy3)
+ sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
+ sizePolicy4.setHorizontalStretch(0)
+ sizePolicy4.setVerticalStretch(0)
+ sizePolicy4.setHeightForWidth(self.gb_choices.sizePolicy().hasHeightForWidth())
+ self.gb_choices.setSizePolicy(sizePolicy4)
self.lv_choices = QVBoxLayout(self.gb_choices)
self.lv_choices.setObjectName("lv_choices")
self.lh_choices_quality_audio = QHBoxLayout()
self.lh_choices_quality_audio.setObjectName("lh_choices_quality_audio")
self.l_icon_quality_audio = QLabel(self.gb_choices)
self.l_icon_quality_audio.setObjectName("l_icon_quality_audio")
- sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
- sizePolicy4.setHorizontalStretch(0)
- sizePolicy4.setVerticalStretch(0)
- sizePolicy4.setHeightForWidth(self.l_icon_quality_audio.sizePolicy().hasHeightForWidth())
- self.l_icon_quality_audio.setSizePolicy(sizePolicy4)
+ sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
+ sizePolicy5.setHorizontalStretch(0)
+ sizePolicy5.setVerticalStretch(0)
+ sizePolicy5.setHeightForWidth(self.l_icon_quality_audio.sizePolicy().hasHeightForWidth())
+ self.l_icon_quality_audio.setSizePolicy(sizePolicy5)
self.lh_choices_quality_audio.addWidget(self.l_icon_quality_audio)
self.l_quality_audio = QLabel(self.gb_choices)
self.l_quality_audio.setObjectName("l_quality_audio")
- sizePolicy4.setHeightForWidth(self.l_quality_audio.sizePolicy().hasHeightForWidth())
- self.l_quality_audio.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_quality_audio.sizePolicy().hasHeightForWidth())
+ self.l_quality_audio.setSizePolicy(sizePolicy5)
self.lh_choices_quality_audio.addWidget(self.l_quality_audio)
@@ -263,15 +365,15 @@ class Ui_DialogSettings:
self.lh_choices_quality_video.setObjectName("lh_choices_quality_video")
self.l_icon_quality_video = QLabel(self.gb_choices)
self.l_icon_quality_video.setObjectName("l_icon_quality_video")
- sizePolicy4.setHeightForWidth(self.l_icon_quality_video.sizePolicy().hasHeightForWidth())
- self.l_icon_quality_video.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_quality_video.sizePolicy().hasHeightForWidth())
+ self.l_icon_quality_video.setSizePolicy(sizePolicy5)
self.lh_choices_quality_video.addWidget(self.l_icon_quality_video)
self.l_quality_video = QLabel(self.gb_choices)
self.l_quality_video.setObjectName("l_quality_video")
- sizePolicy4.setHeightForWidth(self.l_quality_video.sizePolicy().hasHeightForWidth())
- self.l_quality_video.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_quality_video.sizePolicy().hasHeightForWidth())
+ self.l_quality_video.setSizePolicy(sizePolicy5)
self.lh_choices_quality_video.addWidget(self.l_quality_video)
@@ -289,25 +391,25 @@ class Ui_DialogSettings:
self.lh_choices_cover_dimension.setSizeConstraint(QLayout.SizeConstraint.SetDefaultConstraint)
self.l_icon_metadata_cover_dimension = QLabel(self.gb_choices)
self.l_icon_metadata_cover_dimension.setObjectName("l_icon_metadata_cover_dimension")
- sizePolicy4.setHeightForWidth(self.l_icon_metadata_cover_dimension.sizePolicy().hasHeightForWidth())
- self.l_icon_metadata_cover_dimension.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_metadata_cover_dimension.sizePolicy().hasHeightForWidth())
+ self.l_icon_metadata_cover_dimension.setSizePolicy(sizePolicy5)
self.lh_choices_cover_dimension.addWidget(self.l_icon_metadata_cover_dimension)
self.l_metadata_cover_dimension = QLabel(self.gb_choices)
self.l_metadata_cover_dimension.setObjectName("l_metadata_cover_dimension")
- sizePolicy4.setHeightForWidth(self.l_metadata_cover_dimension.sizePolicy().hasHeightForWidth())
- self.l_metadata_cover_dimension.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_metadata_cover_dimension.sizePolicy().hasHeightForWidth())
+ self.l_metadata_cover_dimension.setSizePolicy(sizePolicy5)
self.lh_choices_cover_dimension.addWidget(self.l_metadata_cover_dimension)
self.c_metadata_cover_dimension = QComboBox(self.gb_choices)
self.c_metadata_cover_dimension.setObjectName("c_metadata_cover_dimension")
- sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
- sizePolicy5.setHorizontalStretch(10)
- sizePolicy5.setVerticalStretch(0)
- sizePolicy5.setHeightForWidth(self.c_metadata_cover_dimension.sizePolicy().hasHeightForWidth())
- self.c_metadata_cover_dimension.setSizePolicy(sizePolicy5)
+ sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
+ sizePolicy6.setHorizontalStretch(10)
+ sizePolicy6.setVerticalStretch(0)
+ sizePolicy6.setHeightForWidth(self.c_metadata_cover_dimension.sizePolicy().hasHeightForWidth())
+ self.c_metadata_cover_dimension.setSizePolicy(sizePolicy6)
self.lh_choices_cover_dimension.addWidget(self.c_metadata_cover_dimension)
@@ -315,9 +417,18 @@ class Ui_DialogSettings:
self.lv_choices.addLayout(self.lh_choices_cover_dimension)
- self.lv_main.addWidget(self.gb_choices)
+ self.lv_page_quality.addWidget(self.gb_choices)
- self.gb_numbers = QGroupBox(DialogSettings)
+ self.vs_page_quality = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.lv_page_quality.addItem(self.vs_page_quality)
+
+ self.sw_categories.addWidget(self.page_quality)
+ self.page_numbers = QWidget()
+ self.page_numbers.setObjectName("page_numbers")
+ self.lv_page_numbers = QVBoxLayout(self.page_numbers)
+ self.lv_page_numbers.setObjectName("lv_page_numbers")
+ self.gb_numbers = QGroupBox(self.page_numbers)
self.gb_numbers.setObjectName("gb_numbers")
self.verticalLayout_8 = QVBoxLayout(self.gb_numbers)
self.verticalLayout_8.setObjectName("verticalLayout_8")
@@ -362,9 +473,18 @@ class Ui_DialogSettings:
self.verticalLayout_8.addLayout(self.horizontalLayout_11)
- self.lv_main.addWidget(self.gb_numbers)
+ self.lv_page_numbers.addWidget(self.gb_numbers)
- self.gb_path = QGroupBox(DialogSettings)
+ self.vs_page_numbers = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.lv_page_numbers.addItem(self.vs_page_numbers)
+
+ self.sw_categories.addWidget(self.page_numbers)
+ self.page_paths = QWidget()
+ self.page_paths.setObjectName("page_paths")
+ self.lv_page_paths = QVBoxLayout(self.page_paths)
+ self.lv_page_paths.setObjectName("lv_page_paths")
+ self.gb_path = QGroupBox(self.page_paths)
self.gb_path.setObjectName("gb_path")
self.horizontalLayout_2 = QHBoxLayout(self.gb_path)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
@@ -374,15 +494,15 @@ class Ui_DialogSettings:
self.lh_path_base.setObjectName("lh_path_base")
self.l_icon_download_base_path = QLabel(self.gb_path)
self.l_icon_download_base_path.setObjectName("l_icon_download_base_path")
- sizePolicy4.setHeightForWidth(self.l_icon_download_base_path.sizePolicy().hasHeightForWidth())
- self.l_icon_download_base_path.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_download_base_path.sizePolicy().hasHeightForWidth())
+ self.l_icon_download_base_path.setSizePolicy(sizePolicy5)
self.lh_path_base.addWidget(self.l_icon_download_base_path)
self.l_download_base_path = QLabel(self.gb_path)
self.l_download_base_path.setObjectName("l_download_base_path")
- sizePolicy3.setHeightForWidth(self.l_download_base_path.sizePolicy().hasHeightForWidth())
- self.l_download_base_path.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_download_base_path.sizePolicy().hasHeightForWidth())
+ self.l_download_base_path.setSizePolicy(sizePolicy4)
self.lh_path_base.addWidget(self.l_download_base_path)
@@ -392,15 +512,15 @@ class Ui_DialogSettings:
self.lh_path_fmt_track.setObjectName("lh_path_fmt_track")
self.l_icon_format_track = QLabel(self.gb_path)
self.l_icon_format_track.setObjectName("l_icon_format_track")
- sizePolicy4.setHeightForWidth(self.l_icon_format_track.sizePolicy().hasHeightForWidth())
- self.l_icon_format_track.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_format_track.sizePolicy().hasHeightForWidth())
+ self.l_icon_format_track.setSizePolicy(sizePolicy5)
self.lh_path_fmt_track.addWidget(self.l_icon_format_track)
self.l_format_track = QLabel(self.gb_path)
self.l_format_track.setObjectName("l_format_track")
- sizePolicy3.setHeightForWidth(self.l_format_track.sizePolicy().hasHeightForWidth())
- self.l_format_track.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_format_track.sizePolicy().hasHeightForWidth())
+ self.l_format_track.setSizePolicy(sizePolicy4)
self.lh_path_fmt_track.addWidget(self.l_format_track)
@@ -410,15 +530,15 @@ class Ui_DialogSettings:
self.lh_path_fmt_video.setObjectName("lh_path_fmt_video")
self.l_icon_format_video = QLabel(self.gb_path)
self.l_icon_format_video.setObjectName("l_icon_format_video")
- sizePolicy4.setHeightForWidth(self.l_icon_format_video.sizePolicy().hasHeightForWidth())
- self.l_icon_format_video.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_format_video.sizePolicy().hasHeightForWidth())
+ self.l_icon_format_video.setSizePolicy(sizePolicy5)
self.lh_path_fmt_video.addWidget(self.l_icon_format_video)
self.l_format_video = QLabel(self.gb_path)
self.l_format_video.setObjectName("l_format_video")
- sizePolicy3.setHeightForWidth(self.l_format_video.sizePolicy().hasHeightForWidth())
- self.l_format_video.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_format_video.sizePolicy().hasHeightForWidth())
+ self.l_format_video.setSizePolicy(sizePolicy4)
self.lh_path_fmt_video.addWidget(self.l_format_video)
@@ -428,18 +548,18 @@ class Ui_DialogSettings:
self.lh_path_fmt_album.setObjectName("lh_path_fmt_album")
self.l_icon_format_album = QLabel(self.gb_path)
self.l_icon_format_album.setObjectName("l_icon_format_album")
- sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
- sizePolicy6.setHorizontalStretch(0)
- sizePolicy6.setVerticalStretch(0)
- sizePolicy6.setHeightForWidth(self.l_icon_format_album.sizePolicy().hasHeightForWidth())
- self.l_icon_format_album.setSizePolicy(sizePolicy6)
+ sizePolicy7 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
+ sizePolicy7.setHorizontalStretch(0)
+ sizePolicy7.setVerticalStretch(0)
+ sizePolicy7.setHeightForWidth(self.l_icon_format_album.sizePolicy().hasHeightForWidth())
+ self.l_icon_format_album.setSizePolicy(sizePolicy7)
self.lh_path_fmt_album.addWidget(self.l_icon_format_album)
self.l_format_album = QLabel(self.gb_path)
self.l_format_album.setObjectName("l_format_album")
- sizePolicy3.setHeightForWidth(self.l_format_album.sizePolicy().hasHeightForWidth())
- self.l_format_album.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_format_album.sizePolicy().hasHeightForWidth())
+ self.l_format_album.setSizePolicy(sizePolicy4)
self.lh_path_fmt_album.addWidget(self.l_format_album)
@@ -449,15 +569,15 @@ class Ui_DialogSettings:
self.lh_fpath_mt_playlist.setObjectName("lh_fpath_mt_playlist")
self.l_icon_format_playlist = QLabel(self.gb_path)
self.l_icon_format_playlist.setObjectName("l_icon_format_playlist")
- sizePolicy4.setHeightForWidth(self.l_icon_format_playlist.sizePolicy().hasHeightForWidth())
- self.l_icon_format_playlist.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_format_playlist.sizePolicy().hasHeightForWidth())
+ self.l_icon_format_playlist.setSizePolicy(sizePolicy5)
self.lh_fpath_mt_playlist.addWidget(self.l_icon_format_playlist)
self.l_format_playlist = QLabel(self.gb_path)
self.l_format_playlist.setObjectName("l_format_playlist")
- sizePolicy3.setHeightForWidth(self.l_format_playlist.sizePolicy().hasHeightForWidth())
- self.l_format_playlist.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_format_playlist.sizePolicy().hasHeightForWidth())
+ self.l_format_playlist.setSizePolicy(sizePolicy4)
self.lh_fpath_mt_playlist.addWidget(self.l_format_playlist)
@@ -467,15 +587,15 @@ class Ui_DialogSettings:
self.lh_path_fmt_mix.setObjectName("lh_path_fmt_mix")
self.l_icon_format_mix = QLabel(self.gb_path)
self.l_icon_format_mix.setObjectName("l_icon_format_mix")
- sizePolicy6.setHeightForWidth(self.l_icon_format_mix.sizePolicy().hasHeightForWidth())
- self.l_icon_format_mix.setSizePolicy(sizePolicy6)
+ sizePolicy7.setHeightForWidth(self.l_icon_format_mix.sizePolicy().hasHeightForWidth())
+ self.l_icon_format_mix.setSizePolicy(sizePolicy7)
self.lh_path_fmt_mix.addWidget(self.l_icon_format_mix)
self.l_format_mix = QLabel(self.gb_path)
self.l_format_mix.setObjectName("l_format_mix")
- sizePolicy3.setHeightForWidth(self.l_format_mix.sizePolicy().hasHeightForWidth())
- self.l_format_mix.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_format_mix.sizePolicy().hasHeightForWidth())
+ self.l_format_mix.setSizePolicy(sizePolicy4)
self.lh_path_fmt_mix.addWidget(self.l_format_mix)
@@ -485,15 +605,15 @@ class Ui_DialogSettings:
self.lh_path_binary_ffmpeg.setObjectName("lh_path_binary_ffmpeg")
self.l_icon_path_binary_ffmpeg = QLabel(self.gb_path)
self.l_icon_path_binary_ffmpeg.setObjectName("l_icon_path_binary_ffmpeg")
- sizePolicy4.setHeightForWidth(self.l_icon_path_binary_ffmpeg.sizePolicy().hasHeightForWidth())
- self.l_icon_path_binary_ffmpeg.setSizePolicy(sizePolicy4)
+ sizePolicy5.setHeightForWidth(self.l_icon_path_binary_ffmpeg.sizePolicy().hasHeightForWidth())
+ self.l_icon_path_binary_ffmpeg.setSizePolicy(sizePolicy5)
self.lh_path_binary_ffmpeg.addWidget(self.l_icon_path_binary_ffmpeg)
self.l_path_binary_ffmpeg = QLabel(self.gb_path)
self.l_path_binary_ffmpeg.setObjectName("l_path_binary_ffmpeg")
- sizePolicy3.setHeightForWidth(self.l_path_binary_ffmpeg.sizePolicy().hasHeightForWidth())
- self.l_path_binary_ffmpeg.setSizePolicy(sizePolicy3)
+ sizePolicy4.setHeightForWidth(self.l_path_binary_ffmpeg.sizePolicy().hasHeightForWidth())
+ self.l_path_binary_ffmpeg.setSizePolicy(sizePolicy4)
self.lh_path_binary_ffmpeg.addWidget(self.l_path_binary_ffmpeg)
@@ -507,8 +627,8 @@ class Ui_DialogSettings:
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
self.le_download_base_path = QLineEdit(self.gb_path)
self.le_download_base_path.setObjectName("le_download_base_path")
- sizePolicy2.setHeightForWidth(self.le_download_base_path.sizePolicy().hasHeightForWidth())
- self.le_download_base_path.setSizePolicy(sizePolicy2)
+ sizePolicy3.setHeightForWidth(self.le_download_base_path.sizePolicy().hasHeightForWidth())
+ self.le_download_base_path.setSizePolicy(sizePolicy3)
self.le_download_base_path.setDragEnabled(True)
self.horizontalLayout_10.addWidget(self.le_download_base_path)
@@ -551,8 +671,8 @@ class Ui_DialogSettings:
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.le_format_playlist = QLineEdit(self.gb_path)
self.le_format_playlist.setObjectName("le_format_playlist")
- sizePolicy2.setHeightForWidth(self.le_format_playlist.sizePolicy().hasHeightForWidth())
- self.le_format_playlist.setSizePolicy(sizePolicy2)
+ sizePolicy3.setHeightForWidth(self.le_format_playlist.sizePolicy().hasHeightForWidth())
+ self.le_format_playlist.setSizePolicy(sizePolicy3)
self.horizontalLayout_4.addWidget(self.le_format_playlist)
@@ -571,8 +691,8 @@ class Ui_DialogSettings:
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.le_path_binary_ffmpeg = QLineEdit(self.gb_path)
self.le_path_binary_ffmpeg.setObjectName("le_path_binary_ffmpeg")
- sizePolicy2.setHeightForWidth(self.le_path_binary_ffmpeg.sizePolicy().hasHeightForWidth())
- self.le_path_binary_ffmpeg.setSizePolicy(sizePolicy2)
+ sizePolicy3.setHeightForWidth(self.le_path_binary_ffmpeg.sizePolicy().hasHeightForWidth())
+ self.le_path_binary_ffmpeg.setSizePolicy(sizePolicy3)
self.le_path_binary_ffmpeg.setDragEnabled(True)
self.horizontalLayout_3.addWidget(self.le_path_binary_ffmpeg)
@@ -588,7 +708,160 @@ class Ui_DialogSettings:
self.horizontalLayout_2.setStretch(1, 50)
- self.lv_main.addWidget(self.gb_path)
+ self.lv_page_paths.addWidget(self.gb_path)
+
+ self.vs_page_paths = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.lv_page_paths.addItem(self.vs_page_paths)
+
+ self.sw_categories.addWidget(self.page_paths)
+ self.page_delimiters = QWidget()
+ self.page_delimiters.setObjectName("page_delimiters")
+ self.lv_page_delimiters = QVBoxLayout(self.page_delimiters)
+ self.lv_page_delimiters.setObjectName("lv_page_delimiters")
+ self.gb_delimiters = QGroupBox(self.page_delimiters)
+ self.gb_delimiters.setObjectName("gb_delimiters")
+ self.lv_delimiters = QVBoxLayout(self.gb_delimiters)
+ self.lv_delimiters.setObjectName("lv_delimiters")
+ self.lh_metadata_delimiter_artist = QHBoxLayout()
+ self.lh_metadata_delimiter_artist.setObjectName("lh_metadata_delimiter_artist")
+ self.l_icon_metadata_delimiter_artist = QLabel(self.gb_delimiters)
+ self.l_icon_metadata_delimiter_artist.setObjectName("l_icon_metadata_delimiter_artist")
+ sizePolicy5.setHeightForWidth(self.l_icon_metadata_delimiter_artist.sizePolicy().hasHeightForWidth())
+ self.l_icon_metadata_delimiter_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_metadata_delimiter_artist.addWidget(self.l_icon_metadata_delimiter_artist)
+
+ self.l_metadata_delimiter_artist = QLabel(self.gb_delimiters)
+ self.l_metadata_delimiter_artist.setObjectName("l_metadata_delimiter_artist")
+ sizePolicy5.setHeightForWidth(self.l_metadata_delimiter_artist.sizePolicy().hasHeightForWidth())
+ self.l_metadata_delimiter_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_metadata_delimiter_artist.addWidget(self.l_metadata_delimiter_artist)
+
+ self.le_metadata_delimiter_artist = QLineEdit(self.gb_delimiters)
+ self.le_metadata_delimiter_artist.setObjectName("le_metadata_delimiter_artist")
+ sizePolicy4.setHeightForWidth(self.le_metadata_delimiter_artist.sizePolicy().hasHeightForWidth())
+ self.le_metadata_delimiter_artist.setSizePolicy(sizePolicy4)
+ self.le_metadata_delimiter_artist.setMaximumSize(QSize(100, 16777215))
+
+ self.lh_metadata_delimiter_artist.addWidget(self.le_metadata_delimiter_artist)
+
+ self.hs_metadata_delimiter_artist = QSpacerItem(
+ 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
+ )
+
+ self.lh_metadata_delimiter_artist.addItem(self.hs_metadata_delimiter_artist)
+
+ self.lv_delimiters.addLayout(self.lh_metadata_delimiter_artist)
+
+ self.lh_metadata_delimiter_album_artist = QHBoxLayout()
+ self.lh_metadata_delimiter_album_artist.setObjectName("lh_metadata_delimiter_album_artist")
+ self.l_icon_metadata_delimiter_album_artist = QLabel(self.gb_delimiters)
+ self.l_icon_metadata_delimiter_album_artist.setObjectName("l_icon_metadata_delimiter_album_artist")
+ sizePolicy5.setHeightForWidth(self.l_icon_metadata_delimiter_album_artist.sizePolicy().hasHeightForWidth())
+ self.l_icon_metadata_delimiter_album_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_metadata_delimiter_album_artist.addWidget(self.l_icon_metadata_delimiter_album_artist)
+
+ self.l_metadata_delimiter_album_artist = QLabel(self.gb_delimiters)
+ self.l_metadata_delimiter_album_artist.setObjectName("l_metadata_delimiter_album_artist")
+ sizePolicy5.setHeightForWidth(self.l_metadata_delimiter_album_artist.sizePolicy().hasHeightForWidth())
+ self.l_metadata_delimiter_album_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_metadata_delimiter_album_artist.addWidget(self.l_metadata_delimiter_album_artist)
+
+ self.le_metadata_delimiter_album_artist = QLineEdit(self.gb_delimiters)
+ self.le_metadata_delimiter_album_artist.setObjectName("le_metadata_delimiter_album_artist")
+ sizePolicy4.setHeightForWidth(self.le_metadata_delimiter_album_artist.sizePolicy().hasHeightForWidth())
+ self.le_metadata_delimiter_album_artist.setSizePolicy(sizePolicy4)
+ self.le_metadata_delimiter_album_artist.setMaximumSize(QSize(100, 16777215))
+
+ self.lh_metadata_delimiter_album_artist.addWidget(self.le_metadata_delimiter_album_artist)
+
+ self.hs_metadata_delimiter_album_artist = QSpacerItem(
+ 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
+ )
+
+ self.lh_metadata_delimiter_album_artist.addItem(self.hs_metadata_delimiter_album_artist)
+
+ self.lv_delimiters.addLayout(self.lh_metadata_delimiter_album_artist)
+
+ self.lh_filename_delimiter_artist = QHBoxLayout()
+ self.lh_filename_delimiter_artist.setObjectName("lh_filename_delimiter_artist")
+ self.l_icon_filename_delimiter_artist = QLabel(self.gb_delimiters)
+ self.l_icon_filename_delimiter_artist.setObjectName("l_icon_filename_delimiter_artist")
+ sizePolicy5.setHeightForWidth(self.l_icon_filename_delimiter_artist.sizePolicy().hasHeightForWidth())
+ self.l_icon_filename_delimiter_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_filename_delimiter_artist.addWidget(self.l_icon_filename_delimiter_artist)
+
+ self.l_filename_delimiter_artist = QLabel(self.gb_delimiters)
+ self.l_filename_delimiter_artist.setObjectName("l_filename_delimiter_artist")
+ sizePolicy5.setHeightForWidth(self.l_filename_delimiter_artist.sizePolicy().hasHeightForWidth())
+ self.l_filename_delimiter_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_filename_delimiter_artist.addWidget(self.l_filename_delimiter_artist)
+
+ self.le_filename_delimiter_artist = QLineEdit(self.gb_delimiters)
+ self.le_filename_delimiter_artist.setObjectName("le_filename_delimiter_artist")
+ sizePolicy4.setHeightForWidth(self.le_filename_delimiter_artist.sizePolicy().hasHeightForWidth())
+ self.le_filename_delimiter_artist.setSizePolicy(sizePolicy4)
+ self.le_filename_delimiter_artist.setMaximumSize(QSize(100, 16777215))
+
+ self.lh_filename_delimiter_artist.addWidget(self.le_filename_delimiter_artist)
+
+ self.hs_filename_delimiter_artist = QSpacerItem(
+ 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
+ )
+
+ self.lh_filename_delimiter_artist.addItem(self.hs_filename_delimiter_artist)
+
+ self.lv_delimiters.addLayout(self.lh_filename_delimiter_artist)
+
+ self.lh_filename_delimiter_album_artist = QHBoxLayout()
+ self.lh_filename_delimiter_album_artist.setObjectName("lh_filename_delimiter_album_artist")
+ self.l_icon_filename_delimiter_album_artist = QLabel(self.gb_delimiters)
+ self.l_icon_filename_delimiter_album_artist.setObjectName("l_icon_filename_delimiter_album_artist")
+ sizePolicy5.setHeightForWidth(self.l_icon_filename_delimiter_album_artist.sizePolicy().hasHeightForWidth())
+ self.l_icon_filename_delimiter_album_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_filename_delimiter_album_artist.addWidget(self.l_icon_filename_delimiter_album_artist)
+
+ self.l_filename_delimiter_album_artist = QLabel(self.gb_delimiters)
+ self.l_filename_delimiter_album_artist.setObjectName("l_filename_delimiter_album_artist")
+ sizePolicy5.setHeightForWidth(self.l_filename_delimiter_album_artist.sizePolicy().hasHeightForWidth())
+ self.l_filename_delimiter_album_artist.setSizePolicy(sizePolicy5)
+
+ self.lh_filename_delimiter_album_artist.addWidget(self.l_filename_delimiter_album_artist)
+
+ self.le_filename_delimiter_album_artist = QLineEdit(self.gb_delimiters)
+ self.le_filename_delimiter_album_artist.setObjectName("le_filename_delimiter_album_artist")
+ sizePolicy4.setHeightForWidth(self.le_filename_delimiter_album_artist.sizePolicy().hasHeightForWidth())
+ self.le_filename_delimiter_album_artist.setSizePolicy(sizePolicy4)
+ self.le_filename_delimiter_album_artist.setMaximumSize(QSize(100, 16777215))
+
+ self.lh_filename_delimiter_album_artist.addWidget(self.le_filename_delimiter_album_artist)
+
+ self.hs_filename_delimiter_album_artist = QSpacerItem(
+ 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
+ )
+
+ self.lh_filename_delimiter_album_artist.addItem(self.hs_filename_delimiter_album_artist)
+
+ self.lv_delimiters.addLayout(self.lh_filename_delimiter_album_artist)
+
+ self.lv_page_delimiters.addWidget(self.gb_delimiters)
+
+ self.vs_page_delimiters = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.lv_page_delimiters.addItem(self.vs_page_delimiters)
+
+ self.sw_categories.addWidget(self.page_delimiters)
+
+ self.lh_main_content.addWidget(self.sw_categories)
+
+ self.lv_main.addLayout(self.lh_main_content)
self.bb_dialog = QDialogButtonBox(DialogSettings)
self.bb_dialog.setObjectName("bb_dialog")
@@ -603,6 +876,8 @@ class Ui_DialogSettings:
self.bb_dialog.accepted.connect(DialogSettings.accept)
self.bb_dialog.rejected.connect(DialogSettings.reject)
+ self.sw_categories.setCurrentIndex(0)
+
QMetaObject.connectSlotsByName(DialogSettings)
# setupUi
@@ -656,5 +931,18 @@ class Ui_DialogSettings:
self.l_path_binary_ffmpeg.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
self.pb_download_base_path.setText(QCoreApplication.translate("DialogSettings", "...", None))
self.pb_path_binary_ffmpeg.setText(QCoreApplication.translate("DialogSettings", "...", None))
+ self.gb_delimiters.setTitle(QCoreApplication.translate("DialogSettings", "Delimiters", None))
+ self.l_icon_metadata_delimiter_artist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
+ self.l_metadata_delimiter_artist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
+ self.l_icon_metadata_delimiter_album_artist.setText(
+ QCoreApplication.translate("DialogSettings", "TextLabel", None)
+ )
+ self.l_metadata_delimiter_album_artist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
+ self.l_icon_filename_delimiter_artist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
+ self.l_filename_delimiter_artist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
+ self.l_icon_filename_delimiter_album_artist.setText(
+ QCoreApplication.translate("DialogSettings", "TextLabel", None)
+ )
+ self.l_filename_delimiter_album_artist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
# retranslateUi
diff --git a/tidal_dl_ng/ui/dialog_settings.ui b/tidal_dl_ng/ui/dialog_settings.ui
index 3161095..77dd59a 100644
--- a/tidal_dl_ng/ui/dialog_settings.ui
+++ b/tidal_dl_ng/ui/dialog_settings.ui
@@ -6,8 +6,8 @@
0
0
- 640
- 832
+ 808
+ 379
@@ -50,749 +50,1195 @@
12
-
-
-
-
- 100
- 100
-
-
-
- Flags
-
-
- false
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
- 100
- 100
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
-
- 100
- 100
-
-
-
- CheckBox
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
-
- 100
- 100
-
-
-
- CheckBox
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- 100
- 100
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- CheckBox
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
- CheckBox
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
- CheckBox
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
- CheckBox
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- CheckBox
-
-
-
-
-
- -
-
-
-
-
-
- CheckBox
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Choices
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
-
- -
-
-
- QLayout::SizeConstraint::SetDefaultConstraint
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 10
- 0
-
-
-
-
-
-
-
-
-
- -
-
-
- Numbers
-
-
-
-
-
-
-
-
-
- TextLabel
-
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- 4
-
-
-
-
-
- -
-
-
-
-
-
- TextLabel
-
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- 1
-
-
- 5
-
-
-
-
-
-
-
-
- -
-
-
- Path
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- TextLabel
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- true
-
-
-
- -
-
-
- ...
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- true
-
-
-
- -
-
-
- ...
-
-
-
-
-
-
-
-
-
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ QListWidget {
+ background-color: #2b2b2b;
+ border: 1px solid #3d3d3d;
+ border-radius: 4px;
+ padding: 4px;
+ outline: none;
+}
+
+QListWidget::item {
+ padding: 4px 3px;
+ border-radius: 3px;
+ margin: 2px 0px;
+ color: #e0e0e0;
+}
+
+QListWidget::item:selected {
+ background-color: #3d5a80;
+ color: white;
+ font-weight: bold;
+}
+
+QListWidget::item:hover:!selected {
+ background-color: #3a3a3a;
+ color: white;
+}
+
+QListWidget::item:focus {
+ outline: none;
+}
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+ -1
+
+
+ QListView::ResizeMode::Adjust
+
+
+ 0
+
+
+
+ -
+
+
+ QStackedWidget {
+ background-color: #333333;
+ border: 1px solid #3d3d3d;
+ border-radius: 4px;
+}
+
+QWidget {
+ background-color: transparent;
+}
+
+QGroupBox {
+ background-color: #3a3a3a;
+ border: 1px solid #4a4a4a;
+ border-radius: 6px;
+ margin-top: 12px;
+ padding-top: 12px;
+ font-weight: bold;
+}
+
+QGroupBox::title {
+ subcontrol-origin: margin;
+ subcontrol-position: top left;
+ padding: 4px 8px;
+ background-color: #3a3a3a;
+ border-radius: 4px;
+}
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Plain
+
+
+ 1
+
+
+ 0
+
+
+
+
-
+
+
+
+ 100
+ 100
+
+
+
+ Flags
+
+
+ false
+
+
+ false
+
+
+
-
+
+
-
+
+
-
+
+
+
+ 100
+ 100
+
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 100
+ 100
+
+
+
+ CheckBox
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 100
+ 100
+
+
+
+ CheckBox
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 100
+ 100
+
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ CheckBox
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+ -
+
+
-
+
+
+ CheckBox
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Choices
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+
+ -
+
+
+ QLayout::SizeConstraint::SetDefaultConstraint
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 10
+ 0
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+ Numbers
+
+
+
-
+
+
-
+
+
+ TextLabel
+
+
+
+ -
+
+
+ TextLabel
+
+
+
+ -
+
+
+ 4
+
+
+
+
+
+ -
+
+
-
+
+
+ TextLabel
+
+
+
+ -
+
+
+ TextLabel
+
+
+
+ -
+
+
+ 1
+
+
+ 5
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+ Path
+
+
+
-
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+ -
+
+
+ ...
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+ -
+
+
+ ...
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+ Delimiters
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ TextLabel
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
-