272 lines
11 KiB
Python
272 lines
11 KiB
Python
"""The two dialogs behind File → Export Playlist…
|
|
|
|
`ExportKindDialog` asks folder-or-website; `WebMixDialog` collects the bits of
|
|
a web mix that can't be inferred — title, blurb, images, accent color —
|
|
and whether to upload it to the web server set in Preferences (Round 81).
|
|
Per-track liner notes are deliberately absent: the hand-made mixes carry a numbered paragraph
|
|
per track, which is far too much to type into a form, so the generated
|
|
index.html leaves a commented-out block in the right place instead.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from PyQt6.QtCore import Qt
|
|
from PyQt6.QtGui import QColor
|
|
from PyQt6.QtWidgets import (
|
|
QCheckBox, QColorDialog, QDialog, QDialogButtonBox, QFileDialog,
|
|
QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPlainTextEdit,
|
|
QPushButton, QRadioButton, QVBoxLayout, QWidget)
|
|
|
|
from lintunes import theme
|
|
from lintunes.export import exporter, upload
|
|
from lintunes.gui.file_filters import WEB_IMAGE_FILTER as _IMAGE_FILTER
|
|
|
|
|
|
class ExportKindDialog(QDialog):
|
|
"""Folder of music files, or a self-contained web mix."""
|
|
|
|
def __init__(self, playlist_name: str, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Export Playlist")
|
|
self.setMinimumWidth(420)
|
|
layout = QVBoxLayout(self)
|
|
|
|
layout.addWidget(QLabel(f"Export “{playlist_name}” as:"))
|
|
|
|
self._folder = QRadioButton("A folder of music files")
|
|
self._folder.setChecked(True)
|
|
layout.addWidget(self._folder)
|
|
hint = QLabel("The audio files, named “Artist - Title”, plus an .m3u "
|
|
"playlist. Files are copied exactly as they are.")
|
|
hint.setWordWrap(True)
|
|
hint.setIndent(24)
|
|
hint.setEnabled(False)
|
|
layout.addWidget(hint)
|
|
|
|
self._web = QRadioButton("A website")
|
|
layout.addWidget(self._web)
|
|
web_hint = QLabel(
|
|
"A self-contained page with a player and the tracklist, ready to "
|
|
"copy into a web directory. Anything a browser can't play is "
|
|
"converted to FLAC, losslessly.")
|
|
web_hint.setWordWrap(True)
|
|
web_hint.setIndent(24)
|
|
web_hint.setEnabled(False)
|
|
layout.addWidget(web_hint)
|
|
|
|
layout.addSpacing(8)
|
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok
|
|
| QDialogButtonBox.StandardButton.Cancel)
|
|
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("Continue")
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
def kind(self) -> str:
|
|
return exporter.WEB if self._web.isChecked() else exporter.FOLDER
|
|
|
|
|
|
class WebMixDialog(QDialog):
|
|
"""Title, description, images and accent color for a web mix.
|
|
|
|
The accent isn't remembered between exports: it opens on
|
|
``exporter.DEFAULT_ACCENT`` every time, so an export nobody touched comes
|
|
out looking exactly like the mixes already online.
|
|
|
|
`config` is config.json's contents, read for the web server. The upload
|
|
box is never ticked by default, and a ticked box with no server set up
|
|
keeps the dialog open rather than failing later.
|
|
"""
|
|
|
|
def __init__(self, playlist_name: str, parent=None, config=None):
|
|
super().__init__(parent)
|
|
self._server = upload.server_settings(config or {})
|
|
self.setWindowTitle("Export Web Mix")
|
|
self.setMinimumWidth(520)
|
|
layout = QVBoxLayout(self)
|
|
form = QFormLayout()
|
|
form.setFieldGrowthPolicy(
|
|
QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
|
|
|
self._title = QLineEdit(playlist_name)
|
|
form.addRow("Title:", self._title)
|
|
|
|
self._description = QPlainTextEdit()
|
|
self._description.setPlainText(exporter.DEFAULT_DESCRIPTION)
|
|
self._description.setMinimumHeight(140)
|
|
form.addRow("Description:", self._description)
|
|
|
|
# One row per image, always with an empty one at the end: choosing a
|
|
# file in the last row adds another below it, and clearing any other
|
|
# row removes it.
|
|
images = QWidget()
|
|
self._image_rows = QVBoxLayout(images)
|
|
self._image_rows.setContentsMargins(0, 0, 0, 0)
|
|
self._images: list[QLineEdit] = []
|
|
self._add_image_row()
|
|
form.addRow("Images:", images)
|
|
|
|
color_row = QHBoxLayout()
|
|
self._accent = exporter.DEFAULT_ACCENT
|
|
self._accent_button = QPushButton()
|
|
self._accent_button.clicked.connect(self._choose_accent)
|
|
color_row.addWidget(self._accent_button)
|
|
reset = QPushButton("Reset")
|
|
reset.clicked.connect(
|
|
lambda: self._set_accent(exporter.DEFAULT_ACCENT))
|
|
color_row.addWidget(reset)
|
|
color_row.addStretch()
|
|
form.addRow("Accent:", color_row)
|
|
self._set_accent(self._accent)
|
|
layout.addLayout(form)
|
|
|
|
note = QLabel(
|
|
"The accent colors the hover background on links and tracklist "
|
|
"rows, and the player's progress bar. "
|
|
"The description is plain HTML — links and <br> work. "
|
|
"Per-track liner notes aren't asked for here; index.html has a "
|
|
"commented-out block in the right spot for them.")
|
|
note.setWordWrap(True)
|
|
note.setTextFormat(Qt.TextFormat.RichText)
|
|
note.setEnabled(False)
|
|
layout.addWidget(note)
|
|
|
|
layout.addSpacing(8)
|
|
self._upload = QCheckBox("Upload to web server")
|
|
layout.addWidget(self._upload)
|
|
upload_form = QFormLayout()
|
|
upload_form.setFieldGrowthPolicy(
|
|
QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
|
upload_form.setContentsMargins(24, 0, 0, 0)
|
|
# The slug follows the title until it is edited by hand.
|
|
self._slug = QLineEdit(upload.slugify(playlist_name))
|
|
self._slug_edited = False
|
|
self._slug.textEdited.connect(self._on_slug_edited)
|
|
self._slug.textChanged.connect(self._refresh_upload_note)
|
|
self._title.textEdited.connect(self._on_title_edited)
|
|
upload_form.addRow("Folder on server:", self._slug)
|
|
layout.addLayout(upload_form)
|
|
self._upload_note = QLabel()
|
|
self._upload_note.setWordWrap(True)
|
|
self._upload_note.setIndent(24)
|
|
self._upload_note.setEnabled(False)
|
|
layout.addWidget(self._upload_note)
|
|
self._upload.toggled.connect(self._refresh_upload_note)
|
|
self._refresh_upload_note()
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok
|
|
| QDialogButtonBox.StandardButton.Cancel)
|
|
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("Export")
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
def _on_title_edited(self, text: str):
|
|
if not self._slug_edited:
|
|
self._slug.setText(upload.slugify(text))
|
|
|
|
def _on_slug_edited(self, _text: str):
|
|
self._slug_edited = True
|
|
|
|
def _refresh_upload_note(self):
|
|
on = self._upload.isChecked()
|
|
self._slug.setEnabled(on)
|
|
if self._server is None:
|
|
text = ("Not set up yet — fill in the web server in Preferences → "
|
|
"Integrations → Web Server.")
|
|
else:
|
|
host, path = self._server
|
|
slug = self._slug.text().strip() or "…"
|
|
try:
|
|
where = f"{host}:{upload.remote_dir(path, slug)}"
|
|
except upload.UploadError:
|
|
where = f"{host}:{path.rstrip('/')}/{slug}"
|
|
text = (f"Uploads to {where} instead of a folder here. If a mix "
|
|
"with that folder name is already on the server, it is "
|
|
"replaced by this one.")
|
|
self._upload_note.setText(text)
|
|
|
|
def accept(self):
|
|
if self._upload.isChecked():
|
|
if self._server is None:
|
|
QMessageBox.warning(
|
|
self, "Web server not set up",
|
|
"Fill in the web server details in Preferences → "
|
|
"Integrations to upload this mix, or uncheck "
|
|
"“Upload to web server” to export it to a folder here.")
|
|
return
|
|
if not upload.valid_slug(self._slug.text().strip()):
|
|
QMessageBox.warning(
|
|
self, "Folder on server",
|
|
"Give the mix a folder name for the server — letters, "
|
|
"numbers and dashes work best, since it becomes part "
|
|
"of the web address.")
|
|
return
|
|
super().accept()
|
|
|
|
def _add_image_row(self):
|
|
row = QWidget()
|
|
layout = QHBoxLayout(row)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
field = QLineEdit()
|
|
field.setReadOnly(True)
|
|
field.setPlaceholderText("(a gray placeholder)" if not self._images
|
|
else "(add another image)")
|
|
layout.addWidget(field, stretch=1)
|
|
browse = QPushButton("Browse…")
|
|
browse.clicked.connect(lambda: self._choose_image(field))
|
|
layout.addWidget(browse)
|
|
clear = QPushButton("Clear")
|
|
clear.clicked.connect(lambda: self._clear_image(field))
|
|
layout.addWidget(clear)
|
|
self._images.append(field)
|
|
self._image_rows.addWidget(row)
|
|
|
|
def _choose_image(self, field: QLineEdit):
|
|
# Start where this row's image is, or where the last one came from.
|
|
near = field.text() or next(
|
|
(f.text() for f in reversed(self._images) if f.text()), "")
|
|
start = str(Path(near).parent) if near else ""
|
|
name, _filter = QFileDialog.getOpenFileName(
|
|
self, "Choose Image", start, _IMAGE_FILTER)
|
|
if name:
|
|
self._set_image(field, name)
|
|
|
|
def _set_image(self, field: QLineEdit, path: str):
|
|
field.setText(path)
|
|
if field is self._images[-1]:
|
|
self._add_image_row()
|
|
|
|
def _clear_image(self, field: QLineEdit):
|
|
if field is self._images[-1]:
|
|
field.clear()
|
|
return
|
|
index = self._images.index(field)
|
|
self._images.pop(index)
|
|
row = self._image_rows.takeAt(index).widget()
|
|
row.deleteLater()
|
|
# The first row's placeholder says what an export with none gets.
|
|
self._images[0].setPlaceholderText("(a gray placeholder)")
|
|
|
|
def _set_accent(self, color_hex: str):
|
|
self._accent = exporter.normalize_accent(color_hex)
|
|
self._accent_button.setIcon(theme.swatch_icon(self._accent))
|
|
self._accent_button.setText(self._accent)
|
|
|
|
def _choose_accent(self):
|
|
color = QColorDialog.getColor(
|
|
QColor(self._accent), self, "Accent Color")
|
|
if color.isValid():
|
|
self._set_accent(color.name())
|
|
|
|
def values(self) -> dict:
|
|
return {
|
|
"title": self._title.text().strip(),
|
|
"description": self._description.toPlainText().strip(),
|
|
"images": [Path(f.text().strip()) for f in self._images
|
|
if f.text().strip()],
|
|
"accent": self._accent,
|
|
"upload": self._upload.isChecked(),
|
|
"slug": self._slug.text().strip(),
|
|
}
|