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 @@
# | \ -Track | \ -Artist | \ -Album | \ -Year | \ +# | \ +Track | \ +Artist | \ +Album | \ +Year | \Duration | \' + track.id + ' | ' + trTag.innerHTML += '' + (options.trackids ? track.id : (track.track || ' ')) + ' | ' trTag.innerHTML += '' + title + ' | ' trTag.innerHTML += '' + (track.artist || '') + ' | ' trTag.innerHTML += '' + (track.album || '') + ' | ' @@ -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
---|