songs2slides

A tool that automatically finds song lyrics and creates lyric slideshows
git clone https://git.ashermorgan.net/songs2slides/
Log | Files | Refs | README

commit f45fd2a4d792ca7980b153de765588e1f0902bd8
parent 9886598e21b902463cc87a841004b854611c6a37
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Tue,  9 Apr 2024 17:18:31 -0700

Create step 3 page for slideshow options

Diffstat:
Msongs2slides/routes.py | 13+++++++++++++
Dsongs2slides/static/create-step-1.js | 32--------------------------------
Dsongs2slides/static/create-step-2.js | 41-----------------------------------------
Asongs2slides/static/create.js | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msongs2slides/templates/create-step-1.html | 10+++++++---
Msongs2slides/templates/create-step-2.html | 51++++++++++++++++++---------------------------------
Asongs2slides/templates/create-step-3.html | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_routes.py | 21+++++++++++++++++++++
8 files changed, 195 insertions(+), 109 deletions(-)

diff --git a/songs2slides/routes.py b/songs2slides/routes.py @@ -73,6 +73,19 @@ def create_step_2(): # Return song data return render_template('create-step-2.html', songs=songs, missing=missing) +@bp.get('/create/step-3/') +def create_step_3_get(): + # GET requests not allowed, redirect to step 1 + return redirect(url_for('.create_step_1'), 302) + +@bp.post('/create/step-3/') +def create_step_3(): + # Parse form data + songs = parse_form(request.form) + + # Return song data + return render_template('create-step-3.html', songs=songs) + @bp.get('/post-download/') def post_download(): return render_template('post-download.html') diff --git a/songs2slides/static/create-step-1.js b/songs2slides/static/create-step-1.js @@ -1,32 +0,0 @@ -addEventListener('submit', () => { - // Show loading spinner - document.getElementById('post-submit').hidden = false -}) - -addEventListener('pageshow', () => { - // Correct page state after returning via browser back button - document.getElementById('post-submit').hidden = true - document.getElementById('step-1').hidden = false -}) - -function add_song() { - let row = document.getElementById('row-template').content.children[0].cloneNode(true) - document.getElementById('songs').appendChild(row) - renumber_songs() -} - -function remove_song(n) { - document.getElementsByTagName('tr')[n].remove() - renumber_songs() - if (document.getElementsByTagName('tr').length === 1) add_song() -} - -function renumber_songs() { - const songs = document.getElementsByTagName('tr') - for (let i = 1; i < songs.length - 1; i++) { - songs[i].children[0].textContent = `${i}.` - songs[i].children[1].children[0].name = `title-${i}` - songs[i].children[2].children[0].name = `artist-${i}` - songs[i].children[3].children[0].onclick = () => remove_song(i) - } -} diff --git a/songs2slides/static/create-step-2.js b/songs2slides/static/create-step-2.js @@ -1,41 +0,0 @@ -addEventListener('submit', () => { - // Save settings - const form = document.getElementById('step-2') - storage_set('title-slides', form['title-slides'].checked) - storage_set('blank-slides', form['blank-slides'].checked) - storage_set('output-type', form['output-type'].value) - - // Redirect to post download message - if (form['output-type'].value === 'pptx') { - setTimeout(() => { - // On Chrome, redirecting after a form submission doesn't work - // unless setTimeout is used - // (REDIRECT_URL set in create-step-2.html template) - window.location.href = REDIRECT_URL - }, 100) - } -}) - -addEventListener('pageshow', () => { - // Load settings - const form = document.getElementById('step-2') - form['title-slides'].checked = storage_get('title-slides', true) - form['blank-slides'].checked = storage_get('blank-slides', true) - form['output-type'].value = storage_get('output-type', 'html') -}) - -// Global Songs2Slides localStorage prefix -const PREFIX = 's2s' - -function storage_get(key, default_value) { - try { - value = JSON.parse(localStorage.getItem(`${PREFIX}.${key}`)) - } catch { - return clonedDefault - } - return value === null ? default_value : value -} - -function storage_set(key, value) { - localStorage.setItem(`${PREFIX}.${key}`, JSON.stringify(value)) -} diff --git a/songs2slides/static/create.js b/songs2slides/static/create.js @@ -0,0 +1,77 @@ +// Global Songs2Slides localStorage prefix +const PREFIX = 's2s' + +// Page load/reload handler +addEventListener('pageshow', () => { + if (STEP === 1 || STEP == 2) { + // Correct page state after returning via browser back button + document.getElementById('post-submit').hidden = true + } else if (STEP === 3) { + // Load settings + const form = document.getElementById('create-form') + form['title-slides'].checked = storage_get('title-slides', true) + form['blank-slides'].checked = storage_get('blank-slides', true) + form['output-type'].value = storage_get('output-type', 'html') + } +}) + +// Form submit handler +addEventListener('submit', () => { + if (STEP === 1 || STEP === 2) { + // Show loading spinner + document.getElementById('post-submit').hidden = false + } else if (STEP === 3) { + // Save settings + const form = document.getElementById('create-form') + storage_set('title-slides', form['title-slides'].checked) + storage_set('blank-slides', form['blank-slides'].checked) + storage_set('output-type', form['output-type'].value) + + // Redirect to post download message + if (form['output-type'].value === 'pptx') { + setTimeout(() => { + // On Chrome, redirecting after a form submission doesn't work + // unless setTimeout is used + // (REDIRECT_URL set in create-step-3.html template) + window.location.href = REDIRECT_URL + }, 100) + } + } +}) + +// Step 1 functions +function add_song() { + let row = document.getElementById('row-template').content.children[0].cloneNode(true) + document.getElementById('songs').appendChild(row) + renumber_songs() +} + +function remove_song(n) { + document.getElementsByTagName('tr')[n].remove() + renumber_songs() + if (document.getElementsByTagName('tr').length === 1) add_song() +} + +function renumber_songs() { + const songs = document.getElementsByTagName('tr') + for (let i = 1; i < songs.length - 1; i++) { + songs[i].children[0].textContent = `${i}.` + songs[i].children[1].children[0].name = `title-${i}` + songs[i].children[2].children[0].name = `artist-${i}` + songs[i].children[3].children[0].onclick = () => remove_song(i) + } +} + +// Step 3 helper functions +function storage_get(key, default_value) { + try { + value = JSON.parse(localStorage.getItem(`${PREFIX}.${key}`)) + } catch { + return clonedDefault + } + return value === null ? default_value : value +} + +function storage_set(key, value) { + localStorage.setItem(`${PREFIX}.${key}`, JSON.stringify(value)) +} diff --git a/songs2slides/templates/create-step-1.html b/songs2slides/templates/create-step-1.html @@ -2,11 +2,11 @@ {% block head %} <link rel="stylesheet" href="{{ url_for('static', filename='create.css') }}"/> -<script src="{{ url_for('static', filename='create-step-1.js') }}"></script> +<script src="{{ url_for('static', filename='create.js') }}"></script> {% endblock head %} {% block main %} -<form id="step-1" method="POST" action="{{ url_for('.create_step_2') }}"> +<form method="POST" action="{{ url_for('.create_step_2') }}"> <h1>Step 1: Select Songs</h1> <p> @@ -80,8 +80,12 @@ <div id="post-submit" class="loading-modal" hidden> <div> - <p>Loading your song lyrics...</p> + <p>Searching for your song lyrics...</p> <div class="spinner"></div> </div> </div> + +<script> + const STEP = 1 +</script> {% endblock main %} diff --git a/songs2slides/templates/create-step-2.html b/songs2slides/templates/create-step-2.html @@ -2,26 +2,29 @@ {% block head %} <link rel="stylesheet" href="{{ url_for('static', filename='create.css') }}"/> -<script src="{{ url_for('static', filename='create-step-2.js') }}"></script> +<script src="{{ url_for('static', filename='create.js') }}"></script> {% endblock head %} {% set format_hint = -'A blank line represents the start of a new slide and three blank ' + -'lines represent an empty slide.' +'Each stanza will appear on its own slide. ' + +'Stanzas must be separated by one blank line. ' + +'Three blank lines represent an empty slide.' %} {% block main %} -<form id="step-2" method="POST" action="{{ url_for('.slides') }}"> +<form method="POST" action="{{ url_for('.create_step_3') }}"> <h1>Step 2: Review Lyrics</h1> <p> Review the parsed song lyrics below and make any necessary corrections. + </p> + <p> {{ format_hint }} </p> {% if missing > 0 %} <p> - Lyrics must be entered manually for - {{ missing }} {% if missing == 1 %} song. {% else %} songs. {% endif %} + Lyrics must be entered manually for <strong>{{ missing }} + song{% if missing != 1 %}s{% endif %}</strong>. </p> {% endif %} @@ -60,40 +63,22 @@ {% endfor %} </div> - <div> - <fieldset> - <legend>Extra slides:</legend> - <label> - <input type="checkbox" name="title-slides" checked/> - Include a title slide before each song - </label> - <label> - <input type="checkbox" name="blank-slides" checked/> - Include a blank slide between each song - </label> - </fieldset> - <fieldset> - <legend>Output type:</legend> - <label> - <input type="radio" name="output-type" value="html" checked/> - Web View - </label> - <label> - <input type="radio" name="output-type" value="pptx"/> - PowerPoint download - </label> - </fieldset> - </div> - <div id="actions"> <input onclick="history.back()" type="button" value="Back"/> <button type="submit"> - Create Slide Show + Next </button> </div> </form> +<div id="post-submit" class="loading-modal" hidden> + <div> + <p>Updating your song lyrics...</p> + <div class="spinner"></div> + </div> +</div> + <script> - const REDIRECT_URL = "{{ url_for('.post_download') }}" + const STEP = 2 </script> {% endblock main %} diff --git a/songs2slides/templates/create-step-3.html b/songs2slides/templates/create-step-3.html @@ -0,0 +1,59 @@ +{% extends "layout.html" %} + +{% block head %} +<link rel="stylesheet" href="{{ url_for('static', filename='create.css') }}"/> +<script src="{{ url_for('static', filename='create.js') }}"></script> +{% endblock head %} + +{% block main %} +<form id="create-form" method="POST" action="{{ url_for('.slides') }}"> + <h1>Step 3: Create Slideshow</h1> + <p> + Customize your slideshow with the options below. + </p> + + {% for song in songs %} + <input hidden name="title-{{ loop.index }}" + value="{{ song.title }}"/> + <input hidden name="artist-{{ loop.index }}" + value="{{ song.artist }}"/> + <textarea hidden name="lyrics-{{ loop.index }}" + >{{ song.lyrics }}</textarea> + {% endfor %} + + <fieldset> + <legend>Extra slides:</legend> + <label> + <input type="checkbox" name="title-slides" checked/> + Include a title slide before each song + </label> + <label> + <input type="checkbox" name="blank-slides" checked/> + Include a blank slide between each song + </label> + </fieldset> + <fieldset> + <legend>Output type:</legend> + <label> + <input type="radio" name="output-type" value="html" checked/> + Web View + </label> + <label> + <input type="radio" name="output-type" value="pptx"/> + PowerPoint download + </label> + </fieldset> + + <div id="actions"> + <input onclick="history.back()" type="button" value="Back"/> + <button type="submit"> + Create + </button> + </div> +</form> + +<script> + const STEP = 3 + const REDIRECT_URL = "{{ url_for('.post_download') }}" +</script> +{% endblock main %} diff --git a/tests/test_routes.py b/tests/test_routes.py @@ -76,6 +76,27 @@ class TestRoutes(unittest.TestCase): # Assert response has 400 status code self.assertEqual(res.status_code, 400) + def test_update_lyrics(self): + with patch('songs2slides.routes.render_template') as mocked_render: + + # Send request + self.client.post('/create/step-3/', data={ + 'title-1': 'T1', + 'artist-1': 'A1', + 'lyrics-1': 'L1', + 'title-2': 'T2', + 'artist-2': 'A2', + 'lyrics-2': 'L2', + 'output-type': 'html', + 'title-slides': 'on', + }) + + # Assert render_template called correctly + mocked_render.assert_called_with('create-step-3.html', songs=[ + core.SongData('T1', 'A1', 'L1'), + core.SongData('T2', 'A2', 'L2'), + ]) + def test_create_slides_basic(self): with patch('songs2slides.core.assemble_slides') as mocked_assemble, \ patch('songs2slides.core.create_pptx') as mocked_create, \