--- /dev/null
+build/
+deploy/
+config.json
+listing.json
--- /dev/null
+[submodule "www/enyo"]
+ path = www/enyo
+ url = https://github.com/enyojs/enyo.git
+[submodule "www/lib/onyx"]
+ path = www/lib/onyx
+ url = https://github.com/enyojs/onyx.git
+[submodule "www/lib/layout"]
+ path = www/lib/layout
+ url = https://github.com/enyojs/layout.git
+[submodule "enyo"]
+ path = enyo
+ url = https://github.com/enyojs/enyo.git
+[submodule "lib/onyx"]
+ path = lib/onyx
+ url = https://github.com/enyojs/onyx.git
+[submodule "lib/layout"]
+ path = lib/layout
+ url = https://github.com/enyojs/layout.git
--- /dev/null
+{
+ "mediaURL": "http://server.example.com/media/audio/",
+ "mediaPath": "/media/audio/",
+ "listing": "http://server.example.com/spotter/listing.js"
+}
--- /dev/null
+#!/usr/bin/perl
+use Audio::TagLib;
+use File::Find;
+use JSON;
+use Encode qw/encode decode/;
+use strict;
+
+open CONFIG, 'config.json'
+ or die "Could not open config.json\n";
+my $config_json = join('', <CONFIG>);
+close CONFIG;
+my $config = JSON->new->utf8->decode($config_json);
+
+my @dirs;
+if (@ARGV) {
+ @dirs = @ARGV;
+} else {
+ @dirs = ($config->{mediaPath});
+}
+
+for my $d (@dirs) {
+ if ($d !~ /^$config->{mediaPath}/) {
+ die "$d is not within mediaPath\n";
+ } elsif (! -d $d) {
+ die "$d is not a valid directory\n";
+ }
+}
+
+my $json = JSON->new->utf8;
+
+print '[';
+
+find(sub {
+ return unless /\.mp3$/i;
+ my $f = Audio::TagLib::FileRef->new($_);
+ my $tags = $f->tag;
+ my $file = $File::Find::name;
+ $file =~ s|^$config->{mediaPath}||;
+ my $e = {
+ file => decode('utf8', $file)
+ };
+ if ($tags) {
+ my $artist = $tags->artist->toCString(1);
+ my $album = $tags->album->toCString(1);
+ my $title = $tags->title->toCString(1);
+ $e->{artist} = $artist
+ if $artist;
+ $e->{album} = $album
+ if $album;
+ $e->{title} = $title
+ if $title;
+ }
+ print $json->encode($e), ',';
+
+}, @dirs);
+
+print 'null]'
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title>Spotter (Debug)</title>
+ <link rel="shortcut icon" href="assets/favicon.ico"/>
+ <!-- -->
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8"/>
+ <meta name="apple-mobile-web-app-capable" content="yes"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+ <!-- Less.js (for client-side rendering of less stylesheets; comment to use pre-compiled CSS) -->
+ <script src="enyo/tools/less.js"></script>
+ <!-- enyo (debug) -->
+ <script src="enyo/enyo.js" charset="utf-8"></script>
+ <!-- application (debug) -->
+ <script src="source/package.js" charset="utf-8"></script>
+ </head>
+ <body class="enyo-unselectable">
+ </body>
+</html>
--- /dev/null
+{
+ "enyo": "./enyo",
+ "packagejs": "./package.js",
+ "assets": ["./icon.png", "./index.html", "./assets"],
+ "libs": ["./lib/onyx", "./lib/layout"]
+}
--- /dev/null
+Subproject commit 4e7164be9532ce77beffd0afa5b604c055068dda
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title>Spotter</title>
+ <link rel="shortcut icon" href="assets/favicon.ico"/>
+ <!-- -->
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8"/>
+ <meta name="apple-mobile-web-app-capable" content="yes"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+ <!-- css -->
+ <link href="build/enyo.css" rel="stylesheet"/>
+ <link href="build/app.css" rel="stylesheet"/>
+ <!-- js -->
+ <script src="build/enyo.js" charset="utf-8"></script>
+ <script src="build/app.js" charset="utf-8"></script>
+ </head>
+ <body class="enyo-unselectable">
+ <script type="text/javascript">if (undefined === window.enyo) location = "debug.html";</script>
+ </body>
+</html>
--- /dev/null
+Subproject commit 11002ee2dfb85ec77961196679204791f2fca3dd
--- /dev/null
+Subproject commit 48e936d540f2a0b4d959fe18c7c79794e3661c8f
--- /dev/null
+/*
+ == DO NOT EDIT THIS FILE! ==
+ This is necessary to keep paths correct for the minification process
+*/
+enyo.depends("source");
--- /dev/null
+enyo.kind({
+ name: "Spotter",
+ kind: "enyo.Application",
+ view: "spotter.Main"
+});
--- /dev/null
+enyo.ready(function () {
+ new Spotter();
+});
--- /dev/null
+enyo.kind({
+ name: "SongCollection",
+ kind: "enyo.Collection",
+ model: "SongModel"
+});
+
+enyo.kind({
+ name: "RemoteSongCollection",
+ kind: "SongCollection",
+ url: "http://yomiko.bytex64.net/spotter/listing.json"
+});
--- /dev/null
+enyo.kind({
+ name: "SongModel",
+ kind: "enyo.Model",
+ properties: {
+ filename: ""
+ },
+ bindings: [
+ {from: ".file", to: ".filename", transform: "filenameStrip"}
+ ],
+ filenameStrip: function(val) {
+ return val.replace(/^.*\//, '').replace(/\.mp3$/, '');
+ }
+});
--- /dev/null
+enyo.depends(
+ "SongModel.js",
+ "SongCollection.js"
+);
--- /dev/null
+enyo.depends(
+ "$lib/layout",
+ "$lib/onyx",
+ "style",
+ "data",
+ "spotter",
+ "Spotter.js",
+ "boot.js"
+);
--- /dev/null
+config = {};
+(function() {
+ var req = new enyo.Ajax({
+ url: "config.json"
+ });
+ req.response(this, function(inSender, inResponse) {
+ config = inResponse;
+ });
+ req.error(this, function() {
+ enyo.log("Could not fetch config.json. Trying reasonable defaults.");
+ config = {
+ mediaURL: window.location + 'media/'
+ }
+ });
+ req.go();
+})();
+
+enyo.kind({
+ name: "spotter.Main",
+ kind: "FittableRows",
+ components: [
+ {name: "audioPlayer", kind: "Audio",
+ onPause: "sendPause",
+ onPlay: "sendPlay",
+ onEnded: "sendNextSong"},
+ {kind: "Signals",
+ onShowPlaylist: "showPlaylist",
+ onSongChange: "songChange",
+ onDoPause: "doPause",
+ onDoPlay: "doPlay"},
+ {name: "panels", kind: "Panels", arrangerKind: "CollapsingArranger", fit: true, components: [
+ {kind: "spotter.Search", classes: "spotter-search"},
+ {kind: "spotter.Playlist", classes: "spotter-playlist panel-shadow"}
+ ]},
+ {name: "playControl", kind: "spotter.PlayControl"}
+ ],
+ songChange: function(inSender, inEvent) {
+ this.$.audioPlayer.setSrc(config.mediaURL + inEvent.get('file'));
+ this.$.audioPlayer.play();
+ },
+ doPause: function(inSender, inEvent) {
+ this.$.audioPlayer.pause();
+ },
+ doPlay: function(inSender, inEvent) {
+ this.$.audioPlayer.play();
+ },
+ sendPause: function() {
+ enyo.Signals.send('onPaused');
+ },
+ sendPlay: function() {
+ enyo.Signals.send('onPlayed');
+ },
+ sendNextSong: function() {
+ enyo.Signals.send('onNextSong');
+ },
+ showPlaylist: function(inSender, inEvent) {
+ this.$.panels.setIndex(1);
+ }
+});
--- /dev/null
+enyo.kind({
+ name: "spotter.PlayControl",
+ kind: "onyx.Toolbar",
+ layoutKind: "FittableColumnsLayout",
+ classes: "spotter-playcontrol",
+ components: [
+ {kind: "Signals",
+ onSongChange: "songChange",
+ onPaused: "paused",
+ onPlayed: "played"},
+ {components: [
+ {name: "playButton", kind: "onyx.Button", content: "▶", ontap: "doPlay"},
+ {name: "pauseButton", kind: "onyx.Button", content: "❙❙", ontap: "doPause", showing: false}
+ ]},
+ {name: "playingDisplay", classes: "spotter-playcontrol-display", fit: true}
+ ],
+ create: enyo.inherit(function(sup) {
+ return function() {
+ sup.apply(this, arguments);
+ };
+ }),
+ songChange: function(inSender, inEvent) {
+ var title;
+ if (inEvent.get('artist') && inEvent.get('title')) {
+ title = inEvent.get('artist') + ' - ' + inEvent.get('title');
+ } else {
+ title = inEvent.get('title');
+ }
+ this.$.playingDisplay.setContent(title);
+ },
+ paused: function(inSender, inEvent) {
+ this.$.pauseButton.hide();
+ this.$.playButton.show();
+ },
+ played: function(inSender, inEvent) {
+ this.$.playButton.hide();
+ this.$.pauseButton.show();
+ },
+ doPause: function(inSender, inEvent) {
+ enyo.Signals.send('onDoPause');
+ },
+ doPlay: function(inSender, inEvent) {
+ enyo.Signals.send('onDoPlay');
+ }
+});
--- /dev/null
+enyo.kind({
+ name: "spotter.Playlist",
+ kind: "FittableRows",
+ published: {
+ collection: null
+ },
+ components: [
+ {kind: "Signals",
+ onAddToPlaylist: "addToPlaylist",
+ onNextSong: "nextSong"},
+ {kind: "onyx.Toolbar", layoutKind: "FittableColumnsLayout", components: [
+ {content: "Playlist", classes: "toolbar-label"},
+ {fit: true},
+ {name: "playlistStatus", content: "0 songs", classes: "toolbar-label"},
+ {kind: "onyx.Button", content: "Clear", ontap: "clearPlaylist"}
+ ]},
+ {name: "playList", kind: "enyo.DataList", fit: true, components: [
+ {kind: "spotter.SongItem", ontap: "playSong"}
+ ]}
+ ],
+ bindings: [
+ {from: ".collection", to: ".$.playList.collection"},
+ {from: ".collection.length", to: ".$.playlistStatus.content",
+ transform: function(v) {
+ return v + " song" + (v == 1 ? '' : 's');
+ }},
+ ],
+ create: enyo.inherit(function(sup) {
+ return function() {
+ this.collection = new SongCollection();
+ sup.apply(this, arguments);
+ }
+ }),
+ addToPlaylist: function(inSender, inEvent) {
+ this.collection.add(inEvent);
+ if (this.collection.length == 1) {
+ this.$.playList.select(0);
+ setTimeout(function() {
+ enyo.Signals.send('onSongChange', inEvent);
+ }, 0);
+ }
+ },
+ clearPlaylist: function(inSender, inEvent) {
+ this.collection.removeAll();
+ },
+ playSong: function(inSender, inEvent) {
+ enyo.Signals.send('onSongChange', inEvent.model);
+ },
+ nextSong: function(inSender, inEvent) {
+ var index = this.collection.indexOf(this.$.playList.selected());
+ index++;
+ if (index == this.collection.length) {
+ this.$.playList.deselectAll();
+ } else {
+ this.$.playList.select(index);
+ enyo.Signals.send('onSongChange', this.collection.at(index));
+ }
+ }
+});
--- /dev/null
+enyo.kind({
+ name: "spotter.Search",
+ kind: "FittableRows",
+ published: {
+ collection: null
+ },
+ searchDelay: null,
+ components: [
+ {kind: "onyx.Toolbar", components: [
+ {kind: "onyx.InputDecorator", classes: "search-input-decorator", components: [
+ {name: "searchInput", kind: "onyx.Input", placeholder: "Search", onkeyup: "deferredSearch"}
+ ]}
+ ]},
+ {name: "searchList", kind: "enyo.DataList", selection: false, fit: true, components: [
+ {kind: "spotter.SongItem", ontap: "addToPlaylist"}
+ ]}
+ ],
+ bindings: [
+ {from: ".collection", to: ".$.searchList.collection", transform: "searchFilter"}
+ ],
+ create: enyo.inherit(function(sup) {
+ return function() {
+ this.collection = new RemoteSongCollection({instanceAllRecords: true});
+ this.collection.fetch();
+ sup.apply(this, arguments);
+ };
+ }),
+ searchFilter: function(val, dir) {
+ if (this.$.searchInput.getValue() == "") {
+ return val;
+ }
+ try {
+ var term = new RegExp(this.$.searchInput.getValue(), 'i');
+ } catch(e) {
+ return val;
+ }
+ return new SongCollection(val.filter(function(v) {
+ var artist = v.get('artist');
+ var album = v.get('album');
+ var title = v.get('title');
+ return (artist && artist.search(term) != -1) ||
+ (album && album.search(term) != -1) ||
+ (title && title.search(term) != -1) ||
+ v.get('filename').search(term) != -1;
+ }));
+ },
+ deferredSearch: function(inSender, inEvent) {
+ if (this.searchDelay != null) {
+ clearTimeout(this.searchDelay);
+ }
+
+ this.searchDelay = setTimeout(function() {
+ this.bindings[0].sync();
+ }.bind(this), 250);
+ },
+ goToPlaylist: function(inSender, inEvent) {
+ enyo.Signals.send("onShowPlaylist");
+ },
+ addToPlaylist: function(inSender, inEvent) {
+ enyo.Signals.send("onAddToPlaylist", inEvent.model);
+ }
+});
--- /dev/null
+enyo.kind({
+ name: "spotter.SongItem",
+ kind: "Control",
+ classes: "spotter-song-item",
+ published: {
+ filename: ""
+ },
+ components: [
+ {name: "artist", classes: "artist-label"},
+ {name: "album", classes: "album-label"},
+ {name: "title", classes: "title-label"}
+ ],
+ bindings: [
+ {from: ".model.artist", to: ".$.artist.content"},
+ {from: ".model.album", to: ".$.album.content"},
+ {from: ".model.title", to: ".$.title.content"},
+ {from: ".model.filename", to: ".filename"}
+ ],
+ filenameChanged: function() {
+ if (this.$.title.getContent() == null) {
+ // Let's attempt some heuristics to suss out the actual values here
+ var parts = this.filename.split(/\s*-\s*/);
+ if (parts.length > 1) {
+ while (parts[0].search(/^\d+$/) != -1) {
+ parts.shift();
+ }
+ if (parts.length == 3) {
+ this.$.artist.setContent(parts[0]);
+ this.$.album.setContent(parts[1]);
+ this.$.title.setContent(parts[2]);
+ } else if (parts.length == 2) {
+ this.$.artist.setContent(parts[0]);
+ this.$.title.setContent(parts[1]);
+ } else {
+ this.$.title.setContent(parts[0]);
+ }
+ } else {
+ this.$.title.setContent(this.filename);
+ }
+ }
+ }
+});
--- /dev/null
+enyo.depends(
+ "SongItem.js",
+ "PlayControl.js",
+ "Search.js",
+ "Playlist.js",
+ "Main.js"
+);
--- /dev/null
+enyo.depends(
+ "spotter.less"
+);
--- /dev/null
+@primary-color: #3C85CF;
+@background-color: #F9F9FC;
+
+body {
+ background-color: @background-color;
+}
+
+.onyx-toolbar {
+ background-color: darken(@primary-color, 5%);
+ height: 55px;
+}
+
+.toolbar-label {
+ padding: 5px 0;
+}
+
+.spotter-search {
+ min-width: 50%;
+}
+
+.spotter-playlist {
+ background-color: @background-color;
+}
+
+.spotter-song-item {
+ background-color: @background-color;
+ color: darken(@primary-color, 15%);
+ font-size: 20px;
+ padding: 6px 10px;
+ height: 60px;
+ border-bottom: 1px solid darken(@primary-color, 15%);
+}
+
+.spotter-song-item:active, .spotter-song-item.selected {
+ background-color: lighten(@primary-color, 40%);
+ border-bottom: 1px solid darken(@primary-color, 15%);
+}
+
+.spotter-song-item > * {
+ display: inline-block;
+ vertical-align: middle;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.spotter-song-item > .artist-label {
+ font-size: 14px;
+ width: 49%;
+ padding-right: 1%;
+}
+
+.spotter-song-item > .album-label {
+ font-size: 14px;
+ width: 49%;
+ padding-right: 1%;
+}
+
+.spotter-song-item > .title-label {
+ width: 100%;
+ font-size: 22px;
+ margin-top: 6px;
+}
+
+.spotter-playcontrol {
+ height: 64px;
+}
+
+.spotter-playcontrol .onyx-button {
+ height: 43px;
+ width: 57px;
+}
+
+.spotter-playcontrol-display {
+ display: inline-block;
+ margin-left: 6px;
+ padding: 9px 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+@media screen and (max-width: 700px) {
+ .spotter-search {
+ min-width: 100%;
+ }
+
+ .spotter-song-item {
+ height: 56px;
+ padding: 0px 6px;
+ }
+
+ .spotter-song-item > * {
+ padding: 0;
+ }
+
+ .spotter-song-item > .artist-label {
+ font-size: 12px;
+ height: 14px;
+ width: 39%;
+ }
+
+ .spotter-song-item > .album-label {
+ font-size: 12px;
+ height: 14px;
+ width: 59%;
+ }
+
+ .spotter-song-item > .title-label {
+ width: 100%;
+ font-size: 18px;
+ margin-top: 0;
+ height: auto;
+ }
+}
+
+.search-input-decorator {
+ width: 99.5%;
+ color: @background-color;
+}
+
+.search-input-decorator > .onyx-input {
+ width: 100%;
+ font-size: 18px !important;
+}
+
+.panel-shadow {
+ box-shadow: -4px 0px 8px rgba(0,0,0,0.3);
+}
+
+.centered-spinner {
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ margin: -29px -28px;
+ z-index: 100;
+}
--- /dev/null
+@REM don't watch the sausage being made
+@ECHO OFF
+
+REM the folder this script is in (*/bootplate/tools)
+SET TOOLS=%~DP0
+
+REM application source location
+SET SRC=%TOOLS%\..
+
+REM enyo location
+SET ENYO=%SRC%\enyo
+
+REM deploy script location
+SET DEPLOY=%ENYO%\tools\deploy.js
+
+REM node location
+SET NODE=node.exe
+
+REM use node to invoke deploy.js with imported parameters
+ECHO %NODE% "%DEPLOY%" -T -s "%SRC%" -o "%SRC%\deploy" %*
+%NODE% "%DEPLOY%" -T -s "%SRC%" -o "%SRC%\deploy" %*
+
+REM copy files and package if deploying to cordova webos
+:again
+if not "%1" == "" (
+
+ if "%1" == "--cordova-webos" (
+ REM copy appinfo.json and cordova*.js files
+ for %%A in ("%~dp0./..") do SET DEST=%TOOLS%..\deploy\%%~nA
+ copy %SRC%\appinfo.json %DEST%
+ copy %SRC%\cordova*.js %DEST%
+
+ REM package it up
+ if not exist %SRC%\bin mkdir %SRC%\bin
+ palm-package.bat %DEST% --outdir=%SRC%\bin
+ )
+
+ shift
+ goto again
+)
--- /dev/null
+#!/bin/bash
+
+# the folder this script is in (*/bootplate/tools)
+TOOLS=$(cd `dirname $0` && pwd)
+
+# application root
+SRC="$TOOLS/.."
+
+# enyo location
+ENYO="$SRC/enyo"
+
+# deploy script location
+DEPLOY="$ENYO/tools/deploy.js"
+
+# check for node, but quietly
+if command -v node >/dev/null 2>&1; then
+ # use node to invoke deploy with imported parameters
+ echo "node $DEPLOY -T -s $SRC -o $SRC/deploy $@"
+ node "$DEPLOY" -T -s "$SRC" -o "$SRC/deploy" $@
+else
+ echo "No node found in path"
+ exit 1
+fi
+
+# copy files and package if deploying to cordova webos
+while [ "$1" != "" ]; do
+ case $1 in
+ -w | --cordova-webos )
+ # copy appinfo.json and cordova*.js files
+ DEST="$TOOLS/../deploy/"${PWD##*/}
+
+ cp "$SRC"/appinfo.json "$DEST" -v
+ cp "$SRC"/cordova*.js "$DEST" -v
+
+ # package it up
+ mkdir -p "$DEST/bin"
+ palm-package "$DEST/bin"
+ ;;
+ esac
+ shift
+done