Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.2.1 - 2026-04-05

- Added a packaged application icon and surfaced it in the About dialog
- Continued browser polish around hidden-file controls and tree navigation behavior

## 0.2.0 - 2026-04-05

- Refined the desktop UI with a darker theme, tighter toolbar, custom item icons, and cleaner loading feedback
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "sview"
version = "0.2.0"
version = "0.2.1"
description = "Sequence-aware filesystem browser"
readme = "README.md"
requires-python = ">=3.8"
Expand All @@ -24,3 +24,6 @@ sview = "sview.qt.app:main"

[tool.setuptools]
packages = ["sview", "sview.qt"]

[tool.setuptools.package-data]
"sview.qt" = ["sview_icon.png"]
2 changes: 1 addition & 1 deletion sview/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
on usability and a clean interface.
"""

__version__ = "0.2.0"
__version__ = "0.2.1"
37 changes: 34 additions & 3 deletions sview/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def __init__(self, scanner: DirectoryScanner | None = None) -> None:
self._current_items: list[BrowserItem] = []
self._raw_items: list[BrowserItem] = []
self._grouped_items: list[BrowserItem] = []
self._sequence_metadata_cache: dict[str, dict[str, object]] = {}
self._grouped_view = True
self._expanded_sequence: BrowserItem | None = None

Expand Down Expand Up @@ -103,22 +104,52 @@ def apply_scan_result(self, result: ScanResult) -> list[BrowserItem]:
self._current_path = result.path
self._grouped_items = result.grouped_items
self._raw_items = result.raw_items
self._apply_cached_sequence_metadata()
self._expanded_sequence = None
self._refresh_current_items()
return self.current_items

def update_sequence_metadata(
self, item_path: str, size_bytes: int, modified_time: float
self,
item_path: str,
size_bytes: int | None = None,
modified_time: float | None = None,
missing: list[int] | None = None,
) -> BrowserItem | None:
cache_entry = self._sequence_metadata_cache.setdefault(item_path, {})
if size_bytes is not None:
cache_entry["size_bytes"] = size_bytes
if modified_time is not None:
cache_entry["modified_time"] = modified_time
if missing is not None:
cache_entry["missing"] = list(missing)
for item in self._grouped_items:
if item.path != item_path:
continue
item.size_bytes = size_bytes
item.modified_time = modified_time
if size_bytes is not None:
item.size_bytes = size_bytes
if modified_time is not None:
item.modified_time = modified_time
if missing is not None:
item.missing = list(missing)
return item
return None

def _refresh_current_items(self) -> None:
self._current_items = (
self._grouped_items if self._grouped_view else self._raw_items
)

def _apply_cached_sequence_metadata(self) -> None:
for item in self._grouped_items:
if item.item_type is not ItemType.SEQUENCE:
continue
cached = self._sequence_metadata_cache.get(item.path)
if not cached:
continue
if "size_bytes" in cached:
item.size_bytes = int(cached["size_bytes"])
if "modified_time" in cached:
item.modified_time = float(cached["modified_time"])
if "missing" in cached:
item.missing = [int(frame) for frame in cached["missing"]]
20 changes: 20 additions & 0 deletions sview/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,23 @@ def from_dict(cls, data: dict[str, object]) -> BrowserItem:
if data["child_paths"] is not None
else None,
)


def format_frame_ranges(frames: list[int] | None) -> str:
if not frames:
return "-"

ordered = sorted(dict.fromkeys(int(frame) for frame in frames))
ranges: list[str] = []
start = ordered[0]
end = ordered[0]

for frame in ordered[1:]:
if frame == end + 1:
end = frame
continue
ranges.append(str(start) if start == end else f"{start}-{end}")
start = end = frame

ranges.append(str(start) if start == end else f"{start}-{end}")
return ", ".join(ranges)
27 changes: 26 additions & 1 deletion sview/qt/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import sys
from pathlib import Path

from PySide6.QtGui import QColor, QPalette
from PySide6.QtGui import QColor, QIcon, QPalette
from PySide6.QtWidgets import QApplication, QMessageBox

from sview import __version__
Expand All @@ -61,10 +61,12 @@ def main() -> int:
return 1
app.setApplicationName("sview")
app.setApplicationVersion(__version__)
app.setWindowIcon(_app_icon())
app.setStyle("Fusion")
_apply_dark_theme(app)
initial_path = _parse_initial_path(sys.argv[1:])
window = MainWindow(initial_path=initial_path, debug=debug)
window.setWindowIcon(_app_icon())
window.show()
return app.exec()

Expand All @@ -85,6 +87,10 @@ def _wants_debug(args: list[str]) -> bool:
return "--debug" in args


def _app_icon() -> QIcon:
return QIcon(str(Path(__file__).with_name("sview_icon.png")))


def _apply_dark_theme(app: QApplication) -> None:
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor("#101519"))
Expand Down Expand Up @@ -294,13 +300,32 @@ def _apply_dark_theme(app: QApplication) -> None:
background-color: #1d262e;
border-color: #2b3741;
}
QWidget#iconCard[missing="true"] {
background-color: #2a211f;
border: 1px solid #4a3731;
border-radius: 6px;
}
QWidget#iconCard[missing="true"]:hover {
background-color: #332824;
border-color: #5a433b;
}
QLabel#iconCardTitle {
font-size: 13px;
font-weight: 600;
color: #eef3f7;
background: transparent;
}
QLabel#iconCardSubtitle {
color: #99a8b3;
background: transparent;
}
QLabel#iconCardTitle[missing="true"] {
background: transparent;
color: #f3e7e2;
}
QLabel#iconCardSubtitle[missing="true"] {
background: transparent;
color: #ccb7ae;
}
"""
)
Expand Down
14 changes: 13 additions & 1 deletion sview/qt/icon_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ def set_items(self, items: list[BrowserItem]) -> None:
list_item.setSizeHint(QSize(self.CARD_WIDTH - 12, self.CARD_HEIGHT - 10))
self.addItem(list_item)
self.setItemWidget(list_item, self._build_card(item))
if self.count() > 0:
self.setCurrentRow(0)

def current_browser_item(self) -> BrowserItem | None:
current = self.currentItem()
Expand All @@ -97,7 +99,11 @@ def update_item(self, item: BrowserItem) -> None:
if browser_item is None or browser_item.path != item.path:
continue
list_item.setData(Qt.ItemDataRole.UserRole, item)
self.setItemWidget(list_item, self._build_card(item))
card = self._build_card(item)
self.setItemWidget(list_item, card)
card.style().unpolish(card)
card.style().polish(card)
card.update()
return

def _emit_context_request(self, position) -> None:
Expand All @@ -118,6 +124,8 @@ def _icon(self, item: BrowserItem):
def _build_card(self, item: BrowserItem) -> QWidget:
card = QWidget()
card.setObjectName("iconCard")
if item.item_type is ItemType.SEQUENCE and item.missing_count:
card.setProperty("missing", True)
layout = QHBoxLayout(card)
layout.setContentsMargins(14, 12, 14, 12)
layout.setSpacing(14)
Expand All @@ -130,10 +138,14 @@ def _build_card(self, item: BrowserItem) -> QWidget:
name_label = QLabel(item.display_name)
name_label.setObjectName("iconCardTitle")
name_label.setWordWrap(True)
if item.item_type is ItemType.SEQUENCE and item.missing_count:
name_label.setProperty("missing", True)

subtitle_label = QLabel(self._secondary_text(item))
subtitle_label.setObjectName("iconCardSubtitle")
subtitle_label.setWordWrap(True)
if item.item_type is ItemType.SEQUENCE and item.missing_count:
subtitle_label.setProperty("missing", True)

text_layout = QVBoxLayout()
text_layout.setContentsMargins(0, 0, 0, 0)
Expand Down
47 changes: 47 additions & 0 deletions sview/qt/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def browser_item_icon(item: BrowserItem, size: int = 18) -> QIcon:
return _file_icon(size)


def sidebar_icon(expanded: bool, size: int = 18) -> QIcon:
return _sidebar_icon(expanded, size)


@lru_cache(maxsize=12)
def _folder_icon(size: int) -> QIcon:
pixmap = QPixmap(size, size)
Expand Down Expand Up @@ -144,3 +148,46 @@ def _sequence_icon(size: int) -> QIcon:
painter.drawRoundedRect(badge, 2, 2)
painter.end()
return QIcon(pixmap)


@lru_cache(maxsize=24)
def _sidebar_icon(expanded: bool, size: int) -> QIcon:
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)

painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setPen(Qt.PenStyle.NoPen)

outer = QRectF(size * 0.10, size * 0.16, size * 0.80, size * 0.68)
sidebar = QRectF(size * 0.14, size * 0.20, size * 0.20, size * 0.60)
content = QRectF(size * 0.38, size * 0.20, size * 0.48, size * 0.60)

painter.setBrush(QColor("#24303a"))
painter.drawRoundedRect(outer, 2.5, 2.5)
painter.setBrush(QColor("#d16f6f") if expanded else QColor("#485663"))
painter.drawRoundedRect(sidebar, 2, 2)
painter.setBrush(QColor("#d8e0e7"))
painter.drawRoundedRect(content, 2, 2)

painter.setPen(QPen(QColor("#34414c"), max(1, size // 16)))
if expanded:
painter.drawLine(
QPointF(size * 0.24, size * 0.42),
QPointF(size * 0.18, size * 0.50),
)
painter.drawLine(
QPointF(size * 0.18, size * 0.50),
QPointF(size * 0.24, size * 0.58),
)
else:
painter.drawLine(
QPointF(size * 0.18, size * 0.42),
QPointF(size * 0.24, size * 0.50),
)
painter.drawLine(
QPointF(size * 0.24, size * 0.50),
QPointF(size * 0.18, size * 0.58),
)
painter.end()
return QIcon(pixmap)
16 changes: 12 additions & 4 deletions sview/qt/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
QWidget,
)

from sview.model import BrowserItem, ItemType
from sview.model import BrowserItem, ItemType, format_frame_ranges


class InspectorPanel(QWidget):
Expand All @@ -69,6 +69,7 @@ def __init__(self) -> None:
self._range_value = QLabel("-")
self._count_value = QLabel("-")
self._missing_value = QLabel("-")
self._missing_value.setWordWrap(True)
self._padding_value = QLabel("-")
self._size_value = QLabel("-")
self._modified_value = QLabel("-")
Expand All @@ -86,6 +87,15 @@ def __init__(self) -> None:
)
self.close_button.setToolTip("Close inspector")
self.close_button.setFixedWidth(24)
for button in (
self.copy_path_button,
self.copy_pattern_button,
self.find_missing_button,
self.get_size_button,
self.expand_button,
):
button.setMinimumHeight(30)
button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
Expand Down Expand Up @@ -185,9 +195,7 @@ def set_item(self, item: BrowserItem) -> None:
self._type_value.setText(self._type_text(item))
self._range_value.setText(item.frame_range or "-")
self._count_value.setText(str(item.count) if item.count else "-")
self._missing_value.setText(
", ".join(str(frame) for frame in item.missing) if item.missing else "-"
)
self._missing_value.setText(format_frame_ranges(item.missing))
self._padding_value.setText(item.pad or "-")
self._size_value.setText(self._format_size(item.size_bytes))
self._modified_value.setText(self._format_modified(item.modified_time))
Expand Down
Loading
Loading