1
0
Fork 0

first release of guitar-songs app

master
Tom Wallroth 2023-07-08 11:58:21 +02:00
parent 99c3731ed4
commit 5db8249ac9
8 changed files with 421 additions and 0 deletions

View File

@ -0,0 +1 @@
0.01: First Release

View File

@ -0,0 +1,22 @@
# Guitar Songs
Upload lyrics and chords to your BangleJS2. Play songs at the camp fire.
![screenshot](screenshot.png)
## Usage
Install the app. Use the App Loader to add songs to the watch.
You can scroll through the chords by dragging your finger left and right.
You can scroll the lyrics by dragging it up and down.
## Attribution
[Fire icon created by Freepik - Flaticon](https://www.flaticon.com/free-icons/fire)
## Credits
Created by: devsnd
Inspired by: NovaDawn999

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwg967oABCaHQAYUNDCISBC4wfDC5gQCC4YYPC5BJOC65MFJCQXIGCIXGGAY0MC5K2EC54TCMhoWGC5YHEC5RBIIxREKC4gWHC5QLDFwXu9oXSCAXuDAoXILIYDD7wYBeJhxHC4QwEC6QwEaB4WCGAgXYDIxGKVQwXJLAQXESJYXGCYPjmYXPFYPtFwQXTAAczmc+C6fj+YXNa4QXEn//C43QC5kz/4XBMAPtVIQXM8YWBC4bBDC4xgCC4RFBC5AWGC4guDC55IBC4IuDC4xGHC5peJPAfdCQIAEVAdACglEABVDDAM97vUqgGBkUAggXLGAQXBotUCwM0oAWLC4cz6tUokyGANCIxwABooGBmkzmQuMoQXDDwQXBmgXMI4gGCmRFBoR3PFIYECVYJgOAwtEbQ4AoA"))

157
apps/guitarsongs/app.js Normal file
View File

@ -0,0 +1,157 @@
const chords = {
// name: [name, ...finger_placement, fret],
c: ["C", "0X", "33", "22", "x", "11", "x", 0],
d: ["D", "0X", "0X", "x", "21", "33", "22", 0],
dm: ["Dm", "0x", "0x", "x", "22", "33", "11", 0],
e: ["E", "x", "22", "23", "11", "x", "x", 0],
em: ["Em", "x", "22", "23", "x", "x", "x", 0],
em7: ["Em7", "x", "11", "x", "x", "x", "x", 0],
f: ["F", "0x", "0x", "33", "22", "11", "11", 0],
g: ["G", "32", "21", "x", "x", "x", "33", 0],
am: ["Am", "0X", "x", "21", "22", "23", "x", 0],
a: ["A", "0x", "x", "23", "22", "11", "x", 0],
b7: ["B7", "0x", "22", "11", "23", "x", "24", 0],
cadd9: ["Cadd9", "0x", "32", "21", "x", "33", "34", 0],
dadd11: ["Dadd11", "0x", "33", "22", "x", "11", "x", 3],
csus2: ["Csus2", "0x", "33", "x", "x", "11", "0x", 0],
gadd9: ["Gadd9", "32", "0x", "x", "21", "x", "33", 0],
aadd9: ["Aadd9", "11", "33", "34", "22", "x", "x", 5],
fsharp7add11: ["F#7add11", "21", "43", "44", "32", "x", "x", 0],
d9: ["D9", "0x", "22", "11", "23", "23", "0x", 4],
g7: ["G7", "33", "22", "x", "x", "34", "11", 0],
bflatd: ["Bb/D", "0x", "33", "11", "11", "11", "0x", 3],
e7sharp9: ["E7#9", "0x", "22", "11", "23", "34", "0x", 6],
a11: ["A11", "33", "0x", "34", "22", "11", "0x", 0],
a9: ["A9", "32", "0x", "33", "21", "34", "0x", 3],
}
const chordCache = {};
function drawChordCached(chord, x, y, options) {
let image;
if (chordCache[chord[0]]) {
image = chordCache[chord[0]]
} else {
arrbuff = Graphics.createArrayBuffer(60,65,1,{msb:true});
drawChord(arrbuff, chord, 0, 0, options);
image = {width: arrbuff.getWidth(), height: arrbuff.getHeight(), bpp:arrbuff.getBPP(), buffer: arrbuff.buffer, transparent:0}
chordCache[chord[0]] = image;
}
g.drawImage(image, x, y);
}
function drawChord(buffer, chord, x, y, options) {
const stringWidths = options.stringWidths;
const fretHeight = options.fretHeight;
const circleSize = options.circleSize;
const drawFinger = options.drawFinger;
const drawCircleRim = options.drawCircleRim;
const name = chord[0];
chord = chord.slice(1);
x += 2;
buffer.setColor(0x1).setFontAlign(-1, -1).drawString(name, x, y);
y += 15;
for (let i = 0; i < 6; i++) {
buffer.drawLine(x + i * stringWidths, y, x + i * stringWidths, y + fretHeight*4);
}
for (let i = 0; i < 5; i++) {
buffer.fillRect(x - 1, y + i * fretHeight - 1, x + stringWidths * 5 + 1, y + i * fretHeight + 1);
}
for (let i = 0; i < 6; i++) {
const xPos = x + i * stringWidths;
let yPos = y + fretHeight * parseInt(chord[i][0]) - fretHeight/2
if (chord[i] === "x") {
buffer.setColor(0x1).drawCircle(xPos, y - 5, 2);
continue;
}
if (chord[i] === "0x") {
buffer.setFontAlign(0, 0);
buffer.setColor(0x1).drawString('x', xPos, y - 5);
continue;
}
buffer.setFontAlign(0, 0);
if (drawFinger) {
buffer.setColor(0x0).fillCircle(xPos, yPos, circleSize);
if (drawCircleRim) {
buffer.setColor(0x1).drawCircle(xPos, yPos, circleSize);
}
buffer.setColor(0x1).drawString(chord[i][1], xPos, yPos);
} else {
buffer.setColor(0x1).fillCircle(xPos, yPos, circleSize);
buffer.setFontAlign(0, -1)
buffer.setColor(0x1).drawString(chord[i][1], xPos, y + fretHeight*4 + 2);
}
}
if (chord[6] !== 0) {
buffer.setFontAlign(-1, -1);
buffer.drawString(chord[6] + 'fr', x + 5 * stringWidths + 2, y);
}
}
const chordOptions = {
stringWidths: 8,
fretHeight: 10,
circleSize: 2,
drawFinger: false,
drawCircleRim: false,
}
function drawApp(lyricsLines, chordsDraw, scrollY, chordScrollX) {
const R = Bangle.appRect;
g.setFont('6x8');
if (scrollY < 60) {
for (let i=0; i<chordsDraw.length; i++) {
drawChordCached(chordsDraw[i], 3 + i*60 + chordScrollX, R.y + scrollY, chordOptions);
}
}
const lineHeight = g.stringMetrics(' ').height;
for (let i=0; i<lyricsLines.length; i++){
const y = R.y + lineHeight * i + 60 + scrollY;
if (y < -lineHeight) continue;
if (y > R.y2) break;
g.setFontAlign(-1, -1).drawString(lyricsLines[i], R.x, y);
}
}
let currentScrollY = 0;
let chordScrollX = 0;
let currentChordScroll = 0;
let lyricsHeight = 0;
function main(song) {
const lyrics = song.lyrics;
const foundChords = song.chords;
const lyricsLines = lyrics.split('\n');
const chordsDraw = Object.values(chords).filter(c=>foundChords.includes(c[0]));
g.clear();
drawApp(lyricsLines, chordsDraw, currentScrollY, chordScrollX);
lyricsHeight = g.stringMetrics(lyrics).height;
Bangle.on('drag', (event) => {
currentScrollY = Math.min(0, currentScrollY + event.dy);
chordScrollX = Math.max(Math.min(0, chordScrollX + event.dx), -(chordsDraw.length - 3)*60);
g.clear();
drawApp(lyricsLines, chordsDraw, currentScrollY, chordScrollX);
})
}
function mainMenu () {
const songs = (
require("Storage").readJSON("guitar_songs.json", true) ||
[{'name': 'No songs', 'lyrics': 'Em\nPlease upload a song\nAm\nusing the Bangle App Loader', 'chords': ['Am', 'Em']}]
);
const menu = {
"": {"title": "Guitar Songs"},
};
for (let i=0; i<songs.length; i++) {
const song = songs[i];
menu[song.name] = function() { main(song) };
}
E.showMenu(menu);
}
mainMenu();

BIN
apps/guitarsongs/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,223 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../core/lib/interface.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<div id="app">
<h4>List of Songs</h4>
<div v-if="songsState === 'loading'">
<button v-on:click="loadSongs">Load Songs</button>
</div>
<div v-else>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Lyrics</th>
<th>chords</th>
<th>size</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody id="songs">
<tr v-for="(song, index) in localSongs">
<td>{{song.name}}</td>
<td>{{song.lyrics.slice(0, 30)}}</td>
<td>{{song.chords.join(' ')}}</td>
<td>{{song.lyrics.length + song.name.length}}</td>
<td><button class="btn" v-on:click="setEditSong(song)">edit</button></td>
<td><button class="btn" v-on:click="deleteSong(index)">delete</button></td>
</tr>
<tr><th colspan="3">
<button v-on:click="addNewSong" class="btn">Add new song</button>
</th></tr>
</tbody>
</table>
<div v-if="editSong">
<div class="modal active" id="modal-id">
<a href="#close" class="modal-overlay" aria-label="Close"></a>
<div class="modal-container" style="max-height: 95vh;">
<div class="modal-header">
<div class="modal-title h5">Edit Song</div>
</div>
<div class="modal-body">
<div class="content">
<edit-song :song="editSong"></edit-song>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" v-on:click="setEditSong(null)">Done editing</button>
</div>
</div>
</div>
</div>
<br>
<br>
<div class="card">
<div class="card-body">
Size on Bangle Flash: {{watchSongsSize}} Bytes
<div v-if="watchSongsSize != localSongsSize">
New size: {{localSongsSize}} Bytes
</div>
</div>
<div class="card-footer">
<button class="btn" v-on:click="uploadSongs">Upload</button>
</div>
</div>
</div>
</div>
<script>
Vue.component('edit-song', {
props: ['song'],
data: function () {
return {
newChord: '',
builtInChords: [
"C", "D", "Dm", "E", "Em", "Em7", "F", "G", "Am", "A", "B7", "Cadd9", "Dadd11", "Csus2",
"Gadd9", "Aadd9", "F#7add11", "D9", "G7", "Bb/D", "E7#9", "A11", "A9",
]
}
},
computed: {
availableChords: function () {
const chords = [...this.builtInChords];
for (const chord of this.song.chords) {
chords.splice(chords.indexOf(chord), 1)
}
return chords
}
},
methods: {
removeEmptyLines: function () {
this.song.lyrics = this.song.lyrics.replace(/\n\s*\n/g, '\n');
},
autoDetectChords: function () {
const regex = '((' + Object.values(this.builtInChords).join('|') + ')(\\n|\\s))';
console.log(regex)
const matches = this.song.lyrics.match(new RegExp(regex, 'g'));
const dedup = matches.reduce((accu, val) => { accu[val.slice(0, -1)]=true; return accu}, {});
console.log(dedup);
this.song.chords = Object.keys(dedup);
},
addChord: function (chord) {
this.song.chords.push(chord);
},
removeChord: function (idx) {
this.song.chords.splice(idx, 1);
}
},
template: `
<div id="add_song_form">
<div class="columns">
<div class="column col-4 col-xs-12">
<input v-model="song.name" class="form-input input-sm" type="text" placeholder="Name">
</div>
<div>
<h3>Chords in this Song</h3>
<span v-if="song.chords.length === 0" style="font-size: 80%">
Please add chords by clicking the bubbles below, or use the Auto-Detect Button after
having inserted the lyrics.
</span>
<span class="chip" v-for="(chord, idx) in song.chords">
{{chord}}
<span v-on:click="removeChord(idx)" class="badge" data-badge="x"></span>
</span>
<h3>Available Chords</h3>
<span class="chip" v-for="(chord, idx) in availableChords" v-on:click="addChord(chord)">
{{chord}}
</span>
</div>
<div class="column col-12 col-xs-12">
<span style="font-size: 80%">
Please paste the lyrics and chords in the box below. The width of the text area below is twice the width of the BangleJS2 screen. Everything in the gray area cannot be seen on the screen
</span>
<div style="width: 60ch;">
<textarea
v-model="song.lyrics"
class="form-input input-sm"
aria-label="Song Text"
ref="songText"
style="
width: 60ch;
font-family: monospace;
min-height: 200px;
resize:none;
overflow-y: scroll;
background: rgb(255,255,255);
background: linear-gradient(90deg, rgba(255,255,255,1) 50%, rgba(0,0,0,0.20) 50%);
"></textarea>
</div>
</div>
<div class="column col-12 col-xs-12">
<span class="btn btn-sm" id="remove-empty-lines" v-on:click="removeEmptyLines">Remove empty lines</span>
<span class="btn btn-sm" id="remove-empty-lines" v-on:click="autoDetectChords">Auto Detect Chords</span>
</div>
</div>
<br><br>
</div>
`
});
var app = new Vue({
el: '#app',
data: {
songsState: 'loading',
localSongs: [],
watchSongs: [],
editSong: null,
},
computed: {
localSongsSize: function() {
return JSON.stringify(this.localSongs).length;
},
watchSongsSize: function() {
return JSON.stringify(this.watchSongs).length;
},
},
mounted: function () {
this.loadSongs();
},
methods: {
addNewSong: function () {
this.songsState = 'modified';
const newSong = {
name: '', lyrics: '', chords: [],
}
this.localSongs.push(newSong);
this.editSong = newSong;
},
uploadSongs: function () {
Util.writeStorage('guitar_songs.json', JSON.stringify(this.localSongs), () => {
this.watchSongs = JSON.parse(JSON.stringify(this.localSongs));
alert('Songs written!');
})
},
deleteSong: function (index) {
this.localSongs.splice(index, 1);
},
setEditSong: function (song) {
this.editSong = song;
},
loadSongs: function () {
Util.readStorage('guitar_songs.json', (contents) => {
this.songsState = 'loaded';
this.localSongs = JSON.parse(contents) || [];
this.watchSongs = JSON.parse(JSON.stringify(this.localSongs));
});
window.setTimeout(() => {
if (!this.localSongs.length) {
this.songsState = 'loaded';
}
}, 2000);
}
}
})
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
{ "id": "guitarsongs",
"name": "Guitar Songs",
"shortName":"Guitar Songs",
"version":"0.01",
"description": "Songs lyrics and guitar chords",
"icon": "app.png",
"screenshots": [{"url": "screenshot.png"}],
"tags": "guitar, song, lyrics, chords",
"supports" : ["BANGLEJS2"],
"interface": "manage_songs.html",
"readme": "README.md",
"storage": [
{"name":"guitarsongs.app.js","url":"app.js"},
{"name":"guitarsongs.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name": "guitar_songs.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB