From 5a0727abac286b754477578910a073706d767c5a Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Sun, 20 Jul 2025 22:14:06 +0200 Subject: [PATCH] initial version --- hipsterfy/hipsterfy.py | 54 ++++++++++++------ hipsterfy/main.py | 4 +- hipsterfy/panel_page.py | 122 +++++++++++++++++++++++++++++++++++----- 3 files changed, 145 insertions(+), 35 deletions(-) diff --git a/hipsterfy/hipsterfy.py b/hipsterfy/hipsterfy.py index 843ab8c..1ff890e 100644 --- a/hipsterfy/hipsterfy.py +++ b/hipsterfy/hipsterfy.py @@ -31,33 +31,51 @@ class Hipsterfy(object): class HipsterfyPlaylistItem(object): def _generate_qr_code_base64(self, url): - qr: qrcode.QRCode[PilImage | PyPNGImage] = qrcode.QRCode(box_size=2, border=2) + qr: qrcode.QRCode[PilImage | PyPNGImage] = qrcode.QRCode(box_size=6, border=6) qr.add_data(url) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffered = BytesIO() img.save(buffered, format="PNG") img_str = base64.b64encode(buffered.getvalue()).decode() - return f'QR Code' + img_html = f'QR Code' - def generate_hipsterfy_front_card(self, include_preview:bool = False): + return img_html + + def generate_hipsterfy_front_card(self, include_preview:bool = False, print_mode:bool = False): # generates the square front card only with the centered qr code preview_html = self.embed_html if include_preview else "" if not self._qr_html: return "" + if print_mode: + # Minimalistisches Layout für Druck: kein Hintergrund, kein Schatten, kein Rahmen + card_style = """ + width: 300px; height: 300px; + border: 1.5px; + border-radius: 18px; + display: flex; + align-items: center; + justify-content: center; + background: none; + margin: 18px; + box-shadow: none; + transition: none; + """ + else: + card_style = """ + width: 300px; height: 300px; + border: 1.5px solid #222; + border-radius: 18px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #f9f9f9 60%, #e0e0e0 100%); + margin: 18px; + box-shadow: 0 6px 24px rgba(0,0,0,0.13), 0 1.5px 4px rgba(0,0,0,0.07); + transition: box-shadow 0.2s; + """ return f""" -
+
{self._qr_html} @@ -65,13 +83,13 @@ class HipsterfyPlaylistItem(object):
- Scan for Spotify Preview + Scan Code to play Sample
""" - def generate_hipsterfy_back_card(self, primary_property, secondary_property, additional_properties:List[str]=None, album_cover_gb:bool = True): + def generate_hipsterfy_back_card(self, primary_property, secondary_property, additional_properties:List[str]=None, album_cover_gb:bool = True, print_mode:bool = False): # generates the back card with primary and secondary property and additional properties additional_html = "" if additional_properties: @@ -109,8 +127,8 @@ class HipsterfyPlaylistItem(object): transition: box-shadow 0.2s; ">
-

{getattr(self, primary_property)}

{getattr(self, secondary_property)}

+

{getattr(self, primary_property)}

{additional_html}
diff --git a/hipsterfy/main.py b/hipsterfy/main.py index 31d92b4..7cb432c 100644 --- a/hipsterfy/main.py +++ b/hipsterfy/main.py @@ -19,8 +19,8 @@ def panel_main(): pn.extension() # Example usage of Hipsterfy - playlist_uri = 'https://open.spotify.com/playlist/294v6cT4ZWxtpsKQPZyC5h' # Replace with your playlist URI - app = create_panel_page(hipsterfy, playlist_uri) + playlist_uri = 'https://open.spotify.com/playlist/5grJs3PKyLE0cL5NYmkGIF' # Replace with your playlist URI + app = lambda: create_panel_page(hipsterfy, playlist_uri) # Serve the Panel app pn.serve(app, port=args.port, websocket_origin='*', show=False) diff --git a/hipsterfy/panel_page.py b/hipsterfy/panel_page.py index 1cd7fef..6f7c132 100644 --- a/hipsterfy/panel_page.py +++ b/hipsterfy/panel_page.py @@ -1,36 +1,113 @@ import panel as pn +import base64 +from io import BytesIO from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem +pn.extension("filedownload", "notifications", "location") + def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Template: # create widgets playlist_uri = pn.widgets.TextInput(name='Playlist URI', value=playlist_uri or '', placeholder='Enter Spotify Playlist URI') primary_item = pn.widgets.Select(name='Primary Item', options=HipsterfyPlaylistItem.get_available_properties(), value="release_year") secondary_item = pn.widgets.Select(name='Secondary Item', options=HipsterfyPlaylistItem.get_available_properties(), value="title") - additional_items = pn.widgets.MultiChoice(name='Additional Items', options=HipsterfyPlaylistItem.get_available_properties(), value=["artist", "album"]) + additional_items = pn.widgets.MultiChoice(name='Additional Items', options=HipsterfyPlaylistItem.get_available_properties(), value=["artists", "album", "popularity"]) enable_album_art = pn.widgets.Checkbox(name='Enable Album Art', value=True) + print_mode_in_preview = pn.widgets.Checkbox(name='Print Mode in preview', value=True) + cards_per_row_widget = pn.widgets.IntSlider(name="Cards per Row in print", start=1, end=6, value=4, step=1) + pn.state.location.sync(playlist_uri, {"value": "playlist_uri"}) + pn.state.location.sync(primary_item, {"value": "primary_item"}) + pn.state.location.sync(secondary_item, {"value": "secondary_item"}) + pn.state.location.sync(additional_items, {"value": "additional_items"}) + pn.state.location.sync(enable_album_art, {"value": "enable_album_art"}) create_preview_button = pn.widgets.Button(name='Create Preview', button_type='primary') front_cards_column = pn.Column(sizing_mode='stretch_width') back_cards_column = pn.Column(sizing_mode='stretch_width') - # add widgets to the sidebar of the template - template = pn.template.FastListTemplate( - title='Hipsterfy Playlist Manager', - sidebar=[playlist_uri, primary_item, secondary_item, additional_items, enable_album_art, create_preview_button], - main=[pn.Row(front_cards_column, back_cards_column, sizing_mode='stretch_width')], - accent_base_color='indigo', - header_background='indigo', - header_color='white' + # FileDownload-Buttons (werden erst nach Preview-Generierung angezeigt) + download_front_html_button = pn.widgets.FileDownload( + label="Download Front Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_front_cards.html" + ) + download_back_html_button = pn.widgets.FileDownload( + label="Download Back Cards (HTML)", button_type="success", visible=False, filename="hipsterfy_back_cards.html" ) + print_instructions = pn.pane.HTML( + """ +
+

Print Instructions

+

To print the cards:

+
    +
  1. Click on the "Download Front Cards" and "Download Back Cards" buttons to download the HTML files.
  2. +
  3. Open the downloaded files in your web browser.
  4. +
  5. Use your browser's print function (usually Ctrl+P or Cmd+P) to print the cards.
  6. +
  7. Make sure to first print all front cards, then all back cards on the back side of the paper.
  8. +
  9. Adjust the print settings to ensure the cards fit well on the page.
  10. +
+ """, + sizing_mode='stretch_width' + ) + + print_instructions.visible = False # Initially hidden, will be shown after preview generation + + def generate_html(cards, row_reverse=False): + html = "Print Cards" + html += """ + + """ + html += "" + cards_per_row = cards_per_row_widget.value # <-- Widget-Wert verwenden + # Karten in Reihen zu je 3 anordnen + for i in range(0, len(cards), cards_per_row): + row = cards[i:i+cards_per_row] + if row_reverse: + row = row[::-1] + html += '
' + for card in row: + html += f'
{card}
' + html += '
' + html += "" + return html.encode() # FileDownload erwartet Bytes + + def update_download_buttons(): + # Front Cards + cards = [pane.object for pane in front_cards_column[1:]] + download_front_html_button.callback = lambda: BytesIO(generate_html(cards, row_reverse=False)) + download_front_html_button.visible = True + # Back Cards + cards_back = [pane.object for pane in back_cards_column[1:]] + download_back_html_button.callback = lambda: BytesIO(generate_html(cards_back, row_reverse=True)) + download_back_html_button.visible = True + + print_instructions.visible = True # Show print instructions after generating preview + + pn.state.notifications.success("Download buttons updated successfully!. Download and print them from the sidebar") + def create_preview(event): + pn.state.notifications.info("Generating preview for playlist...") # clear previous cards front_cards_column.clear() back_cards_column.clear() + front_cards_column.append(pn.pane.HTML("

Front Cards

", sizing_mode='stretch_width')) + back_cards_column.append(pn.pane.HTML("

Back Cards

", sizing_mode='stretch_width')) + # get playlist URI uri = playlist_uri.value.strip() if not uri: @@ -42,20 +119,35 @@ def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Templa # generate front and back cards for each item in the playlist for item in hipsterfy_playlist.get_tracks_data(): - front_card = item.generate_hipsterfy_front_card(include_preview=False) + front_card = item.generate_hipsterfy_front_card(include_preview=False, print_mode=print_mode_in_preview.value) back_card = item.generate_hipsterfy_back_card( primary_property=primary_item.value, secondary_property=secondary_item.value, additional_properties=additional_items.value, - album_cover_gb=enable_album_art.value + album_cover_gb=enable_album_art.value, + print_mode=print_mode_in_preview.value ) front_cards_column.append(pn.pane.HTML(front_card, sizing_mode='stretch_width')) back_cards_column.append(pn.pane.HTML(back_card, sizing_mode='stretch_width')) - # notify user of success - # bind the create_preview function to the button click event - create_preview_button.on_click(create_preview) + pn.state.notifications.success("Preview generated successfully!") + pn.state.notifications.info("Prepare to download the cards as HTML files...") + update_download_buttons() + + create_preview_button.on_click(create_preview) + + template = pn.template.FastListTemplate( + title='Hipsterfy Playlist Manager', + sidebar=[ + playlist_uri, primary_item, secondary_item, additional_items, enable_album_art, + cards_per_row_widget, + create_preview_button, print_instructions, download_front_html_button, download_back_html_button + ], + main=[pn.Row(front_cards_column, back_cards_column, sizing_mode='stretch_width')], + accent_base_color='indigo', + header_background='indigo', + header_color='white' + ) - # return the template return template