mirror of https://github.com/espruino/BangleApps
first release of guitar-songs app
parent
99c3731ed4
commit
5db8249ac9
|
@ -0,0 +1 @@
|
|||
0.01: First Release
|
|
@ -0,0 +1,22 @@
|
|||
# Guitar Songs
|
||||
|
||||
Upload lyrics and chords to your BangleJS2. Play songs at the camp fire.
|
||||
|
||||

|
||||
|
||||
## 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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwg967oABCaHQAYUNDCISBC4wfDC5gQCC4YYPC5BJOC65MFJCQXIGCIXGGAY0MC5K2EC54TCMhoWGC5YHEC5RBIIxREKC4gWHC5QLDFwXu9oXSCAXuDAoXILIYDD7wYBeJhxHC4QwEC6QwEaB4WCGAgXYDIxGKVQwXJLAQXESJYXGCYPjmYXPFYPtFwQXTAAczmc+C6fj+YXNa4QXEn//C43QC5kz/4XBMAPtVIQXM8YWBC4bBDC4xgCC4RFBC5AWGC4guDC55IBC4IuDC4xGHC5peJPAfdCQIAEVAdACglEABVDDAM97vUqgGBkUAggXLGAQXBotUCwM0oAWLC4cz6tUokyGANCIxwABooGBmkzmQuMoQXDDwQXBmgXMI4gGCmRFBoR3PFIYECVYJgOAwtEbQ4AoA"))
|
|
@ -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();
|
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
|
@ -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>
|
|
@ -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 |
Loading…
Reference in New Issue