v0.31.7: fixed 16px gaps in the top row, panel fills the rest; right inset
The row of boxes keeps one 16px gap before, between and after its controls at every width; the now-playing panel and the seek row under it take all the remaining width. The track lists get the same 8px of bare window on the right that the playlists have on the left. README updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -286,8 +286,7 @@ through a free program called **Syncthing**, and LinTunes just tells Syncthing
|
|||||||
what to do. Neither of you can change or delete anything on the other's
|
what to do. Neither of you can change or delete anything on the other's
|
||||||
computer.
|
computer.
|
||||||
|
|
||||||
You'll both need LinTunes and Syncthing. You **don't** need to be online at the
|
You'll both need LinTunes and Syncthing.
|
||||||
same time — things simply finish the next time both computers are on.
|
|
||||||
|
|
||||||
### 1. Install and start Syncthing (once)
|
### 1. Install and start Syncthing (once)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
||||||
|
|
||||||
__version__ = "0.31.6"
|
__version__ = "0.31.7"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from lintunes.gui.cassette_ui import CassetteUi
|
|||||||
from lintunes.gui.device_sync_dialog import DeviceSyncSettingsDialog
|
from lintunes.gui.device_sync_dialog import DeviceSyncSettingsDialog
|
||||||
from lintunes.gui.export_dialog import ExportKindDialog, WebMixDialog
|
from lintunes.gui.export_dialog import ExportKindDialog, WebMixDialog
|
||||||
from lintunes.gui.identify_dialog import IdentifyDialog
|
from lintunes.gui.identify_dialog import IdentifyDialog
|
||||||
from lintunes.gui.sidebar import SidebarPanel
|
from lintunes.gui.sidebar import SIDEBAR_INSET, SidebarPanel
|
||||||
from lintunes.gui.library_view import LibraryView
|
from lintunes.gui.library_view import LibraryView
|
||||||
from lintunes.gui.playlist_view import PlaylistView
|
from lintunes.gui.playlist_view import PlaylistView
|
||||||
from lintunes.gui.transport import TransportBar
|
from lintunes.gui.transport import TransportBar
|
||||||
@@ -125,6 +125,9 @@ class MainWindow(QMainWindow):
|
|||||||
splitter.addWidget(self._sidebar)
|
splitter.addWidget(self._sidebar)
|
||||||
|
|
||||||
self._content = QStackedWidget()
|
self._content = QStackedWidget()
|
||||||
|
# The same strip of bare window on the right as the sidebar keeps on
|
||||||
|
# the left, so the lists sit symmetrically in the window.
|
||||||
|
self._content.setContentsMargins(0, 0, SIDEBAR_INSET, 0)
|
||||||
splitter.addWidget(self._content)
|
splitter.addWidget(self._content)
|
||||||
splitter.setSizes([220, 1060])
|
splitter.setSizes([220, 1060])
|
||||||
splitter.setCollapsible(1, False)
|
splitter.setCollapsible(1, False)
|
||||||
|
|||||||
+17
-45
@@ -20,15 +20,11 @@ ICON_GRAY = QColor("#4A4A4A")
|
|||||||
TOP_MARGIN = 9
|
TOP_MARGIN = 9
|
||||||
# Between the row of boxes and the header row under it.
|
# Between the row of boxes and the header row under it.
|
||||||
ROW_GAP = 4
|
ROW_GAP = 4
|
||||||
# The now-playing panel is this share of the bar's width (700px on a 1920px
|
# The row of boxes is knolled: one GAP before the first control, between
|
||||||
# screen), within these bounds. The row floats: the space before the first
|
# each pair and after the last, always. The now-playing panel (and the seek
|
||||||
# control, between each pair and after the last is one equal gap, so the
|
# row under it) takes all the width that's left.
|
||||||
# whole row scales with the window instead of bunching up anywhere.
|
GAP = 16
|
||||||
NOW_PLAYING_SHARE = 700 / 1920
|
NOW_PLAYING_MIN = 180
|
||||||
NOW_PLAYING_MIN = 220
|
|
||||||
NOW_PLAYING_MAX = 1100
|
|
||||||
# Floor for each of those gaps.
|
|
||||||
MIN_GAP = 12
|
|
||||||
|
|
||||||
# Shared height for the boxed side controls (transport, shuffle, bpm) and the
|
# Shared height for the boxed side controls (transport, shuffle, bpm) and the
|
||||||
# visualizer, so they line up. ~midway between their old heights: the stretchy
|
# visualizer, so they line up. ~midway between their old heights: the stretchy
|
||||||
@@ -381,20 +377,12 @@ class TransportBar(QWidget):
|
|||||||
# window could never shrink past the width it last had.
|
# window could never shrink past the width it last had.
|
||||||
outer.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
|
outer.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
layout.setContentsMargins(MIN_GAP, 0, MIN_GAP, 0)
|
layout.setContentsMargins(GAP, 0, GAP, 0)
|
||||||
layout.setSpacing(0)
|
layout.setSpacing(0)
|
||||||
outer.addLayout(layout)
|
outer.addLayout(layout)
|
||||||
|
|
||||||
# Knolled: every control keeps its size and the gaps between them
|
|
||||||
# are equal; _size_now_playing sizes them (and the two ends).
|
|
||||||
self._row = layout
|
|
||||||
self._gaps: list[QSpacerItem] = []
|
|
||||||
|
|
||||||
def gap():
|
def gap():
|
||||||
spacer = QSpacerItem(MIN_GAP, 0, QSizePolicy.Policy.Fixed,
|
layout.addSpacing(GAP)
|
||||||
QSizePolicy.Policy.Minimum)
|
|
||||||
self._gaps.append(spacer)
|
|
||||||
layout.addSpacerItem(spacer)
|
|
||||||
|
|
||||||
# Rounded button boxes carry a user-tunable grayscale fill, decoupled
|
# Rounded button boxes carry a user-tunable grayscale fill, decoupled
|
||||||
# from the stripe color they used to share via palette(alternate-base).
|
# from the stripe color they used to share via palette(alternate-base).
|
||||||
@@ -513,7 +501,7 @@ class TransportBar(QWidget):
|
|||||||
# the seek row hangs underneath the panel, in the header row.
|
# the seek row hangs underneath the panel, in the header row.
|
||||||
self._now_playing.setFixedHeight(CONTROL_HEIGHT)
|
self._now_playing.setFixedHeight(CONTROL_HEIGHT)
|
||||||
# Its width is set by _size_now_playing: all the room the controls
|
# Its width is set by _size_now_playing: all the room the controls
|
||||||
# and minimal gaps leave, up to NOW_PLAYING_MAX.
|
# and gaps leave.
|
||||||
self._now_playing.setFixedWidth(NOW_PLAYING_MIN)
|
self._now_playing.setFixedWidth(NOW_PLAYING_MIN)
|
||||||
layout.addWidget(self._now_playing, alignment=TOP)
|
layout.addWidget(self._now_playing, alignment=TOP)
|
||||||
self._now_playing.installEventFilter(self)
|
self._now_playing.installEventFilter(self)
|
||||||
@@ -602,33 +590,17 @@ class TransportBar(QWidget):
|
|||||||
self._place_header()
|
self._place_header()
|
||||||
|
|
||||||
def _controls_width(self) -> int:
|
def _controls_width(self) -> int:
|
||||||
"""Everything in the row of boxes but the now-playing panel, with
|
"""Everything in the row of boxes but the now-playing panel: the
|
||||||
each gap at its floor."""
|
controls and a GAP before, between and after them."""
|
||||||
return (2 * MIN_GAP
|
# A fixed width shows in the minimum, not the size hint.
|
||||||
# A fixed width shows in the minimum, not the size hint.
|
return (sum(max(w.sizeHint().width(), w.minimumWidth())
|
||||||
+ sum(max(w.sizeHint().width(), w.minimumWidth())
|
for w in self._fixed_controls)
|
||||||
for w in self._fixed_controls)
|
+ (len(self._fixed_controls) + 2) * GAP)
|
||||||
+ len(self._fixed_controls) * MIN_GAP)
|
|
||||||
|
|
||||||
def _size_now_playing(self):
|
def _size_now_playing(self):
|
||||||
"""The panel takes its share of the width; the rest is split into
|
"""The panel fills whatever the controls and gaps leave."""
|
||||||
equal gaps before, between and after the controls."""
|
self._now_playing.setFixedWidth(
|
||||||
spaces = len(self._gaps) + 2
|
max(NOW_PLAYING_MIN, self.width() - self._controls_width()))
|
||||||
fixed = sum(max(w.sizeHint().width(), w.minimumWidth())
|
|
||||||
for w in self._fixed_controls)
|
|
||||||
panel = round(self.width() * NOW_PLAYING_SHARE)
|
|
||||||
panel = min(panel, self.width() - fixed - spaces * MIN_GAP)
|
|
||||||
panel = max(NOW_PLAYING_MIN, min(NOW_PLAYING_MAX, panel))
|
|
||||||
self._now_playing.setFixedWidth(panel)
|
|
||||||
free = max(0, self.width() - fixed - panel)
|
|
||||||
gap = max(MIN_GAP, free // spaces)
|
|
||||||
for spacer in self._gaps:
|
|
||||||
spacer.changeSize(gap, 0, QSizePolicy.Policy.Fixed,
|
|
||||||
QSizePolicy.Policy.Minimum)
|
|
||||||
# Integer rounding's leftover pixels go to the right end.
|
|
||||||
end = max(MIN_GAP, free - gap * len(self._gaps))
|
|
||||||
self._row.setContentsMargins(gap, 0, max(MIN_GAP, end - gap), 0)
|
|
||||||
self._row.invalidate()
|
|
||||||
|
|
||||||
def minimumSizeHint(self):
|
def minimumSizeHint(self):
|
||||||
hint = super().minimumSizeHint()
|
hint = super().minimumSizeHint()
|
||||||
|
|||||||
+19
-11
@@ -3,8 +3,8 @@
|
|||||||
The Library button, the view's header (search box or playlist name) and the
|
The Library button, the view's header (search box or playlist name) and the
|
||||||
seek row share one row under the transport boxes: the Library button as wide
|
seek row share one row under the transport boxes: the Library button as wide
|
||||||
as the sidebar, the header where the view starts, the seek row exactly under
|
as the sidebar, the header where the view starts, the seek row exactly under
|
||||||
the now-playing panel. The panel stops at NOW_PLAYING_MAX and the gaps between
|
the now-playing panel. The row of boxes keeps one GAP before, between and
|
||||||
the controls share what's left.
|
after its controls, and the panel fills the rest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from PyQt6.QtCore import QPoint
|
from PyQt6.QtCore import QPoint
|
||||||
@@ -112,18 +112,29 @@ def test_header_row_shares_the_seek_band(qapp, tmp_path):
|
|||||||
assert library.mapTo(bar, QPoint()).y() >= row.geometry().top()
|
assert library.mapTo(bar, QPoint()).y() >= row.geometry().top()
|
||||||
|
|
||||||
|
|
||||||
def test_panel_scales_with_the_window(qapp, tmp_path):
|
def test_panel_fills_what_the_gaps_leave(qapp, tmp_path):
|
||||||
window = _window(qapp, tmp_path, width=1920)
|
window = _window(qapp, tmp_path, width=1920)
|
||||||
bar = window._transport
|
bar = window._transport
|
||||||
assert abs(bar._now_playing.width() - 700) <= 1
|
wide = bar._now_playing.width()
|
||||||
|
window.resize(1280, 500)
|
||||||
|
qapp.processEvents()
|
||||||
|
assert wide - bar._now_playing.width() == 640
|
||||||
window.resize(800, 500)
|
window.resize(800, 500)
|
||||||
qapp.processEvents()
|
qapp.processEvents()
|
||||||
assert NOW_PLAYING_MIN <= bar._now_playing.width() < 700
|
assert bar._now_playing.width() >= NOW_PLAYING_MIN
|
||||||
bpm = bar._bpm_button.parentWidget()
|
bpm = bar._bpm_button.parentWidget()
|
||||||
assert bar._now_playing.geometry().right() < bpm.geometry().left()
|
assert bar._now_playing.geometry().right() < bpm.geometry().left()
|
||||||
assert bpm.geometry().right() < bar.width()
|
assert bpm.geometry().right() < bar.width()
|
||||||
|
|
||||||
|
|
||||||
|
def test_lists_have_bare_window_on_both_sides(qapp, tmp_path):
|
||||||
|
from lintunes.gui.sidebar import SIDEBAR_INSET
|
||||||
|
window = _window(qapp, tmp_path)
|
||||||
|
lists, view = window.sidebar.list_area, window._library_view
|
||||||
|
assert _gx(lists) - _gx(window) == SIDEBAR_INSET
|
||||||
|
assert _gx(window, window.width()) - _gx(view, view.width()) == SIDEBAR_INSET
|
||||||
|
|
||||||
|
|
||||||
def test_views_seek_row_never_overlaps_the_header(qapp, tmp_path):
|
def test_views_seek_row_never_overlaps_the_header(qapp, tmp_path):
|
||||||
window = _window(qapp, tmp_path, width=800)
|
window = _window(qapp, tmp_path, width=800)
|
||||||
strip = window._library_view.header
|
strip = window._library_view.header
|
||||||
@@ -144,7 +155,7 @@ def test_long_name_shrinks_then_elides(qapp):
|
|||||||
|
|
||||||
|
|
||||||
def test_row_is_knolled_with_equal_gaps_and_ends(qapp, tmp_path):
|
def test_row_is_knolled_with_equal_gaps_and_ends(qapp, tmp_path):
|
||||||
from lintunes.gui.transport import MIN_GAP
|
from lintunes.gui.transport import GAP
|
||||||
window = _window(qapp, tmp_path)
|
window = _window(qapp, tmp_path)
|
||||||
bar = window._transport
|
bar = window._transport
|
||||||
for width in (900, 1280, 1920):
|
for width in (900, 1280, 1920):
|
||||||
@@ -155,9 +166,6 @@ def test_row_is_knolled_with_equal_gaps_and_ends(qapp, tmp_path):
|
|||||||
bar._bpm_button.parentWidget()]
|
bar._bpm_button.parentWidget()]
|
||||||
edges = [(w.geometry().left(), w.geometry().right()) for w in row]
|
edges = [(w.geometry().left(), w.geometry().right()) for w in row]
|
||||||
gaps = [b[0] - a[1] - 1 for a, b in zip(edges, edges[1:])]
|
gaps = [b[0] - a[1] - 1 for a, b in zip(edges, edges[1:])]
|
||||||
assert max(gaps) - min(gaps) <= 1
|
# The same GAP everywhere, ends included, at every width.
|
||||||
assert gaps[0] >= MIN_GAP
|
|
||||||
# The ends get the same space as the gaps: the row floats.
|
|
||||||
left_end, right_end = edges[0][0], bar.width() - 1 - edges[-1][1]
|
left_end, right_end = edges[0][0], bar.width() - 1 - edges[-1][1]
|
||||||
for end in (left_end, right_end):
|
assert gaps + [left_end, right_end] == [GAP] * 7
|
||||||
assert abs(end - gaps[0]) <= 7
|
|
||||||
|
|||||||
Reference in New Issue
Block a user