diff --git a/public/icon/clear-button.svg b/public/icon/clear-button.svg new file mode 100644 index 0000000..35f1a0a --- /dev/null +++ b/public/icon/clear-button.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icon/drop-down-arrow.svg b/public/icon/drop-down-arrow.svg new file mode 100644 index 0000000..2a2a354 --- /dev/null +++ b/public/icon/drop-down-arrow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icon/drop-up-arrow.svg b/public/icon/drop-up-arrow.svg new file mode 100644 index 0000000..09718c0 --- /dev/null +++ b/public/icon/drop-up-arrow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/index.css b/public/index.css index 59b7660..a442129 100644 --- a/public/index.css +++ b/public/index.css @@ -1,32 +1,32 @@ * { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } body { - background-color: #0d171e; - font-family: sans-serif; - color: #fff; + background-color: #0d171e; + font-family: sans-serif; + color: #fff; } .container { - display: flex; - flex-direction: column; - flex-grow: 1; - min-height: 0; - height: 100vh; + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 0; + height: 100vh; } #search { - color: #fff; - background-color: #152b3a; - border: 0; - padding: 10px; - font-size: 2.5em; - border-left: 10px solid #112330; - display: block; + color: #fff; + background-color: #152b3a; + border: 0; + padding: 10px; + font-size: 2.5em; + border-left: 10px solid #112330; + display: block; } .table-container { - flex-grow: 1; - overflow: auto; - overflow-x: hidden; + flex-grow: 1; + overflow: auto; + overflow-x: hidden; } .player { display: flex; @@ -34,65 +34,79 @@ body { flex-direction: column; bottom: 0; width: 100%; - background-color: #152b3a; + background-color: #152b3a; } .player audio { - width: 100%; + width: 100%; } .player #playing { - padding: 10px; + padding: 10px; } table { - table-layout: fixed; - border-spacing: 0; - border-collapse: collapse; + table-layout: fixed; + border-spacing: 0; + border-collapse: collapse; } th { - text-align: left; + text-align: left; + cursor: pointer; +} +th .order { + width: 12px; + height: 12px; + display: inline-block; + background-image: url(icon/drop-up-arrow.svg); + background-repeat: no-repeat; + background-size: 16px; + background-position: center; + margin-left: 10px; +} +th .order.dn { + background-image: url(icon/drop-down-arrow.svg); } th.small { - width: 5%; + width: 5%; } td { - cursor: pointer; + cursor: pointer; } td,th { - padding: 5px; + padding: 5px; } tr:nth-child(even) { - background-color: #0f1b23; + background-color: #0f1b23; } .pages { - display: flex; - flex-direction: row; - min-height: max-content; + display: flex; + flex-direction: row; + min-height: max-content; } .pages .paging { - flex: 1; - text-align: center; - font-size: 3em; + flex: 1; + text-align: center; + font-size: 3em; } .paging.btn { - background-color: #112635; - cursor: pointer; + background-color: #112635; + cursor: pointer; } .paging.btn.inner { - background-color: #102331; + background-color: #102331; } .paging.bg { - background-color: #0e202c; + background-color: #0e202c; } .player-controls { - display: flex; - flex-direction: row; - height: 40px; - background-color: #0c2233; + display: flex; + flex-direction: row; + height: 40px; + background-color: #0c2233; } .player-controls .grow { - flex-grow: 1; + flex-grow: 1; } .player-controls span { - display: block; + display: block; } .player-controls .icon { width: 30px; @@ -105,49 +119,49 @@ tr:nth-child(even) { padding: 0 5px; } .player-controls .timestamp { - margin-top: 0.9em; - font-size: 0.8em; - padding: 0 5px; - min-width: 8em; - text-align: center; + margin-top: 0.9em; + font-size: 0.8em; + padding: 0 5px; + min-width: 8em; + text-align: center; } .play-btn { - background-image: url('icon/play.svg'); + background-image: url('icon/play.svg'); } .pause-btn { - background-image: url('icon/pause.svg'); + background-image: url('icon/pause.svg'); } .mute-btn { - background-image: url('icon/volume.svg'); + background-image: url('icon/volume.svg'); } .mute1-btn { - background-image: url('icon/volume-low.svg'); + background-image: url('icon/volume-low.svg'); } .muted-btn { - background-image: url('icon/volume-off.svg'); + background-image: url('icon/volume-off.svg'); } .next-btn { - background-image: url('icon/play-next.svg'); + background-image: url('icon/play-next.svg'); } .prev-btn { - background-image: url('icon/play-prev.svg'); + background-image: url('icon/play-prev.svg'); } .volume { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } .volume .volume-bar { - width: 5em; + width: 5em; } .volume .volume-bar .seek-inner { - width: 100%; + width: 100%; } .seek-container { - height: 8px; - width: 100%; - margin: 16px 5px 0 5px; - background-color: #00131b; - cursor: pointer; + height: 8px; + width: 100%; + margin: 16px 5px 0 5px; + background-color: #00131b; + cursor: pointer; } .seek-inner { height: 100%; @@ -155,27 +169,27 @@ tr:nth-child(even) { background-color: #00b7ff; } .ctx-menu { - position: absolute; - background-color: #0c2233; - border: 2px solid #031421; + position: absolute; + background-color: #0c2233; + border: 2px solid #031421; } .ctx-menu ul { - list-style: none; - display: block; + list-style: none; + display: block; } .ctx-menu ul li { - display: block; + display: block; } .ctx-item, .dropdown-content div { - padding: 10px; - cursor: pointer; - display: block; + padding: 10px; + cursor: pointer; + display: block; } .ctx-item:nth-child(even), .dropdown-content div:nth-child(even) { - background-color: #0b1f2f; + background-color: #0b1f2f; } .ctx-item:hover, .dropdown-content div:hover { - background-color: #0c273c; + background-color: #0c273c; } .inline-flex { display: flex; @@ -184,20 +198,24 @@ tr:nth-child(even) { position: relative; } .inline-flex input { - flex-grow: 1; - min-width: 0; + flex-grow: 1; + min-width: 0; } #search-clear { - display: none; + display: none; } .btn-clear { - cursor: pointer; - position: absolute; - right: 10px; - top: 12px; - font-size: 2em; - opacity: 0.5; - padding: 0 10px; + cursor: pointer; + position: absolute; + right: 10px; + top: 12px; + bottom: 12px; + font-size: 2em; + opacity: 0.5; + padding: 0 10px; + background: url(icon/clear-button.svg) no-repeat; + background-size: 16px; + background-position: center; } .queue-tag { padding: 10px; @@ -213,15 +231,15 @@ tr:nth-child(even) { cursor: pointer; } .dropdown-button::after { - content: ""; - background: url(icon/down-arrow.svg) no-repeat; + content: ""; + background: url(icon/down-arrow.svg) no-repeat; background-size: auto auto; - background-size: 16px; - position: absolute; - width: 16px; - height: 16px; - right: 5px; - top: 40%; + background-size: 16px; + position: absolute; + width: 16px; + height: 16px; + right: 5px; + top: 40%; } .dropdown-content { display: none; @@ -232,7 +250,7 @@ tr:nth-child(even) { z-index: 1; } .dropdown-content div { - padding: 20px; + padding: 20px; } .dropdown:hover .dropdown-content { display: block; @@ -243,39 +261,95 @@ tr:nth-child(even) { position: relative; } .playing-bar span { - z-index: 10; + z-index: 10; } canvas#visualizer { position: absolute; display: block; opacity: 0.4; - z-index: 9; + z-index: 9; +} +.sidebar.active.background { + pointer-events: initial; +} +.sidebar.active .drop { + opacity: 0.5; +} +.sidebar.active .bar { + max-width: 340px; +} +.sidebar.background { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + pointer-events: none; +} +.sidebar.sb-abs { + position: absolute; + width: 100%; + height: 100%; +} +.sidebar.drop { + background-color: #060606; + z-index: 11; + opacity: 0; + transition: opacity 0.2s ease-out; +} +.sidebar.bar { + z-index: 12; + max-width: 0px; + background-color: rgba(17, 35, 48, 0.9); + overflow-x: hidden; + transition: max-width 0.2s linear; +} +.sidebar.bar h2 { + padding: 18px; +} +.sidebar.content { + width: 340px; +} +.sidebar .option { + padding: 10px; + display: flex; +} +.sidebar .option.checkbox { + flex-direction: row; +} +.sidebar .option label { + flex-grow: 1; +} +.sidebar .option.checkbox input { + margin: 4px 10px; } @media only screen and (max-width: 600px) { tr td:nth-child(1), th:nth-child(1) { - display: none; + display: none; } tr td:nth-child(3), th:nth-child(3) { - display: none; + display: none; } tr td:nth-child(4), th:nth-child(4) { - display: none; + display: none; } tr td:nth-child(5), th:nth-child(5) { - display: none; + display: none; } tr td:nth-child(6), th:nth-child(6) { - display: none; + display: none; } .pages .paging { - flex: 1; - text-align: center; - font-size: 1.3em; - } - .allowance { - margin-bottom: 100px !important; - } - .volume .volume-bar { - display: none; - } + flex: 1; + text-align: center; + font-size: 1.3em; + } + .allowance { + margin-bottom: 100px !important; + } + .volume .volume-bar { + display: none; + } } diff --git a/public/index.html b/public/index.html index a65c8aa..9102378 100644 --- a/public/index.html +++ b/public/index.html @@ -14,10 +14,11 @@ - x +
@@ -73,6 +74,39 @@
  • Download
  • + diff --git a/public/index.js b/public/index.js index a119a57..3685078 100644 --- a/public/index.js +++ b/public/index.js @@ -4,15 +4,17 @@ var clear = document.getElementById('search-clear') var audio = document.getElementById('player') var playing = document.getElementById('playing') + var optdrop = document.getElementById('options-drop') + var optmenu = document.getElementById('options') var menu = document.getElementById('menu') var tableHead = ' \ - \ - \ - \ - \ - \ + \ + \ + \ + \ + \ \ ' var nowPlaying = 0 @@ -35,6 +37,14 @@ var playlist = null var pSel = document.getElementById('playlist-select') + // Options + var options = { + autoplay: true, + trackids: true, + sortby: 'id', + sortdir: 'asc', + } + window.mobilecheck = function() { var check = false; (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); @@ -81,6 +91,16 @@ return time } + function elementsToArray (elms) { + let array = [] + for (let i in elms) { + let el = elms[i] + if (!(el instanceof Element)) continue + array.push(el) + } + return array + } + function httpGet (url, callback) { return new Promise(function (resolve, reject) { var xmlHttp = new XMLHttpRequest() @@ -237,7 +257,7 @@ let trTag = document.createElement('tr') trTag.className = 'track' trTag.setAttribute('data-id', track.id) - trTag.innerHTML += '' + trTag.innerHTML += '' trTag.innerHTML += '' trTag.innerHTML += '' trTag.innerHTML += '' @@ -246,6 +266,32 @@ return trTag } + function handleHeads () { + if (playlist === 0) return + let els = elementsToArray(document.querySelectorAll('th')) + let sortarr = document.createElement('span') + sortarr.className = 'order' + (options.sortdir === 'desc' ? ' dn' : '') + for (let i in els) { + let head = els[i] + let sortattr = head.getAttribute('data-sort-by') + if (!sortattr || sortattr === '') continue + if (sortattr === options.sortby) { + head.appendChild(sortarr) + } + + head.addEventListener('click', function (e) { + if (options.sortby !== sortattr) { + options.sortdir = 'asc' + options.sortby = sortattr + return showTracks(pageNum) + } + + options.sortdir = (options.sortdir === 'asc' ? 'desc' : 'asc') + showTracks(pageNum) + }) + } + } + function constructList (tracks) { table.innerHTML = tableHead @@ -264,6 +310,8 @@ table.appendChild(tag) } + + handleHeads() } function recursionQueueList (index) { @@ -320,7 +368,7 @@ } if (query.trim() === '') { - return httpGet('/api/tracks?page=' + page).then(function (data) { + return httpGet('/api/tracks?page=' + page + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (data) { pageNum = page pages = data.pageCount constructList(data.tracks) @@ -335,7 +383,7 @@ clear.style.display = 'block' } - httpGet('/api/tracks/search?q=' + query + '&page=' + page).then(function (data) { + httpGet('/api/tracks/search?q=' + query + '&page=' + page + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (data) { pageNum = page pages = data.pageCount constructList(data.tracks) @@ -372,17 +420,27 @@ } + var ignoreFirst = false + function toggleOptions () { + if (optdrop.className.indexOf('active') !== -1) { + optdrop.className = 'sidebar background' + } else { + optdrop.className = 'sidebar background active' + ignoreFirst = true + } + } + function handleSelect () { let btn = pSel.querySelector('.dropdown-button') - let btns = pSel.querySelectorAll('.dropdown-content div') + let btns = elementsToArray(pSel.querySelectorAll('.dropdown-content div')) for (let i in btns) { let btni = btns[i] - if (!(btni instanceof Element)) continue let cnt = btni.getAttribute('data-value') btni.addEventListener('click', function (e) { - btn.innerHTML = btni.innerHTML + let last = btn.innerHTML + let set = btni.innerHTML switch (cnt) { case 'all': playlist = null @@ -390,19 +448,72 @@ case 'queue': playlist = 0 break + case 'options': + toggleOptions() + set = last + break default: showPlaylist(cnt) } + btn.innerHTML = set showTracks(pageNum) }, false) } } + function saveOptions () { + if (!window.localStorage) return + let itms = JSON.stringify(options) + window.localStorage.setItem('options', itms) + } + + function loadOptions () { + if (!window.localStorage) return + let itms = window.localStorage.getItem('options') + if (!itms || itms === '') return + let obj = {} + try { + obj = JSON.parse(itms) + } catch (e) {} + options = Object.assign({}, options, obj) + } + + function handleOptions () { + let elms = elementsToArray(optmenu.querySelectorAll('input')) + elms = elms.concat(elementsToArray(optmenu.querySelectorAll('select'))) + for (let i in elms) { + let inpt = elms[i] + if (!(inpt instanceof Element)) continue + let opt = inpt.getAttribute('name') + let type = inpt.getAttribute('type') + + if (type == 'checkbox') { + inpt.checked = options[opt] + inpt.addEventListener('change', function (event) { + options[opt] = event.target.checked + showTracks(pageNum) + saveOptions() + }) + } else { + inpt.value = options[opt] + inpt.addEventListener('change', function (event) { + options[opt] = event.target.value + showTracks(pageNum) + saveOptions() + }) + } + } + } + input.addEventListener('keyup', function (e) { e.which === 13 && showTracks(input.value.trim() === '' ? (pagePrev !== 0 ? pagePrev : 1) : 1) }, false) - audio.addEventListener('ended', playNext, false) + audio.addEventListener('ended', function (e) { + if (options.autoplay) { + playNext(e) + } + }, false) clear.addEventListener('click', function () { input.value = '' @@ -454,7 +565,21 @@ } }) + var optcont = document.querySelector('.sidebar.bar') + document.addEventListener('click', function (event) { + // event.target.closest(optcont) === null + if (!optcont.contains(event.target) && optdrop.className.indexOf('active') !== -1) { + if (ignoreFirst) { + ignoreFirst = false + return + } + toggleOptions() + } + }) + + loadOptions() showTracks(1) handleHash(window.location.hash) handleSelect() + handleOptions() })() diff --git a/src/server.js b/src/server.js index 8b64d4f..ac941ed 100644 --- a/src/server.js +++ b/src/server.js @@ -17,12 +17,25 @@ const port = process.env.PORT || 3000 const router = express.Router() +const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year'] +const srchcategories = ['title', 'artist', 'album'] + router.get('/tracks', async (req, res) => { let page = parseInt(req.query.page) || 1 if (isNaN(page)) { page = 1 } + let sort = req.query.sort + if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) { + sort = 'artist' + } + + let sortdir = req.query.sortdir + if (!sortdir || (sortdir !== 'desc' && sortdir !== 'asc')) { + sortdir = 'asc' + } + let db = await dbPromise let count = (await db.get('SELECT COUNT(*) FROM Track'))['COUNT(*)'] @@ -31,7 +44,7 @@ router.get('/tracks', async (req, res) => { if (page > pageCount) page = pageCount let offset = (page - 1) * tracksPerPage - let tracks = await db.all('SELECT * FROM Track LIMIT ? OFFSET ?', tracksPerPage, offset) + let tracks = await db.all(`SELECT * FROM Track ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`, tracksPerPage, offset) for (let i in tracks) { delete tracks[i].file @@ -42,7 +55,6 @@ router.get('/tracks', async (req, res) => { }) }) -let srchcategories = ['title', 'artist', 'album'] router.get('/tracks/search', async (req, res) => { let query = req.query.q let qr = '' @@ -72,6 +84,16 @@ router.get('/tracks/search', async (req, res) => { if (!exact) query = `%${query}%` + let sort = req.query.sort + if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) { + sort = 'artist' + } + + let sortdir = req.query.sortdir + if (!sortdir || (sortdir !== 'desc' && sortdir !== 'asc')) { + sortdir = 'asc' + } + // Paging let page = parseInt(req.query.page) || 1 if (isNaN(page)) { @@ -85,7 +107,8 @@ router.get('/tracks/search', async (req, res) => { if (page > pageCount) page = pageCount let offset = (page - 1) * tracksPerPage - let tracks = await db.all(`SELECT * FROM Track WHERE ${qr} LIKE ? LIMIT ? OFFSET ?`, query, tracksPerPage, offset) + let tracks = await db.all(`SELECT * FROM Track WHERE ${qr} LIKE ? ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`, + query, tracksPerPage, offset) for (let i in tracks) { delete tracks[i].file
    #TrackArtistAlbumYear#TrackArtistAlbumYearDuration
    ' + track.id + '' + (options.trackids ? track.id : (track.track || ' ')) + '' + title + '' + (track.artist || '') + '' + (track.album || '') + '