From 8a146f59d5ef190612b3a52684c3d0e9d22961dc Mon Sep 17 00:00:00 2001
From: Roberto Tonino <roberto.tonino5@gmail.com>
Date: Sun, 26 Apr 2020 19:33:09 +0200
Subject: [PATCH] implemented download tab resizable (width), with minimum and
 maximum values and storing of actual value

---
 public/css/modules/download-tab.css    |  23 ++-
 public/index.html                      |  98 +++++++---
 public/js/app.js                       |   9 +-
 public/js/modules/downloads.js         | 108 +++++++---
 public/js/modules/link-analyzer-tab.js |  48 ++---
 public/js/modules/search.js            |  10 +-
 public/js/modules/tabs.js              |  10 +-
 public/js/modules/track-preview.js     | 137 ++++++-------
 public/js/modules/utils.js             | 261 ++++++++++++++++++++++++-
 9 files changed, 531 insertions(+), 173 deletions(-)

diff --git a/public/css/modules/download-tab.css b/public/css/modules/download-tab.css
index d35d2ab..df9d80b 100644
--- a/public/css/modules/download-tab.css
+++ b/public/css/modules/download-tab.css
@@ -1,16 +1,17 @@
 #download_tab_container {
-	width: 300px;
+	/* width: 300px; */
 	height: 100%;
 	background-color: var(--panels-background);
 	color: var(--panels-text);
 	display: block;
 	flex-direction: column;
-	transition: all 250ms ease-in-out;
+	/* transition: all 250ms ease-in-out; */
 }
 
 #toggle_download_tab {
 	width: 25px;
 	height: 25px;
+	margin-left: 20px;
 }
 
 #toggle_download_tab::before {
@@ -134,7 +135,7 @@
 }
 
 #download_tab_container #download_list {
-	width: 300px;
+	/* width: 300px; */
 }
 
 #download_tab_container #download_tab_label {
@@ -143,11 +144,27 @@
 	transition: all 250ms ease-in-out;
 }
 
+#download_tab_drag_handler {
+	width: 15px;
+	height: 100%;
+	position: absolute;
+	background-color: #333;
+	cursor: ew-resize;
+}
+
 /* ===== Hidden tab styles ===== */
 #download_tab_container.tab_hidden {
 	width: 32px;
 }
 
+#download_tab_container.tab_hidden #toggle_download_tab {
+	margin-left: 4px;
+}
+
+#download_tab_container.tab_hidden #download_tab_drag_handler {
+	display: none;
+}
+
 #download_tab_container.tab_hidden #toggle_download_tab::before {
 	font-family: 'Material Icons';
 	font-style: normal;
diff --git a/public/index.html b/public/index.html
index a700eb5..3d3fb4e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -16,10 +16,10 @@
 
 <body>
 	<aside id="sidebar" role="navigation">
-    <span id="main_home_tablink" class="main_tablinks" role="link" aria-label="home"><i
-      class="material-icons side_icon">home</i><span class="main_tablinks_text">Home</span></span>
+		<span id="main_home_tablink" class="main_tablinks" role="link" aria-label="home"><i
+				class="material-icons side_icon">home</i><span class="main_tablinks_text">Home</span></span>
 		<span id="main_search_tablink" class="main_tablinks" role="link" aria-label="search"><i
-      class="material-icons side_icon">search</i><span class="main_tablinks_text">Search</span></span>
+				class="material-icons side_icon">search</i><span class="main_tablinks_text">Search</span></span>
 		<span id="main_charts_tablink" class="main_tablinks" role="link" aria-label="charts"><i
 				class="material-icons side_icon">bubble_chart</i><span class="main_tablinks_text">Charts</span></span>
 		<span id="main_favorites_tablink" class="main_tablinks" role="link" aria-label="favorites"><i
@@ -160,8 +160,13 @@ <h1>No Tracks found</h1>
 									</tr>
 									<tr v-for="track in results.trackTab.data" class="track_row">
 										<td style="width: 48px; text-align: center;">
-                      <a href="#" @click="playPausePreview" v-bind:class="'rounded' + (track.preview ? ' single-cover' : '')" v-bind:data-preview="track.preview"><i @mouseenter="previewMouseEnter" @mouseleave="previewMouseLeave" v-if="track.preview" class="material-icons preview_controls">play_arrow</i><img class="rounded coverart" v-bind:src="track.album.cover_small">
-                    </td>
+											<a href="#" @click="playPausePreview"
+												v-bind:class="'rounded' + (track.preview ? ' single-cover' : '')"
+												v-bind:data-preview="track.preview"><i @mouseenter="previewMouseEnter"
+													@mouseleave="previewMouseLeave" v-if="track.preview"
+													class="material-icons preview_controls">play_arrow</i><img class="rounded coverart"
+													v-bind:src="track.album.cover_small">
+										</td>
 										<td class="breakline">{{track.title + (track.title_version ? ' '+track.title_version : '')}}</td>
 										<td class="breakline clickable" @click="artistView" v-bind:data-id="track.artist.id">
 											{{track.artist.name}}</td>
@@ -257,24 +262,54 @@ <h1>Favorites</h1>
 					</div>
 
 					<div id="analyzer_tab" class="main_tabcontent">
-            <h1>Link Analyzer</h1>
-            <h1>{{ title }}</h1>
+						<h1>Link Analyzer</h1>
+						<h1>{{ title }}</h1>
 						<h2>{{ subtitle }}</h2>
-            <table>
-              <tr v-if="data.isrc"><td>ISRC</td><td>{{ data.isrc }}</td></tr>
-              <tr v-if="data.upc"><td>UPC</td><td>{{ data.upc }}</td></tr>
-              <tr v-if="data.duration"><td>Duration</td><td>{{ convertDuration(data.duration) }}</td></tr>
-              <tr v-if="data.disk_number"><td>Disk Number</td><td>{{ data.disk_number }}</td></tr>
-              <tr v-if="data.track_position"><td>Track Number</td><td>{{ data.track_position }}</td></tr>
-              <tr v-if="data.release_date"><td>Release Date</td><td>{{ data.release_date }}</td></tr>
-              <tr v-if="data.bpm"><td>BPM</td><td>{{ data.bpm }}</td></tr>
-              <tr v-if="data.label"><td>Label</td><td>{{ data.label }}</td></tr>
-              <tr v-if="data.record_type"><td>Record Type</td><td>{{ data.record_type }}</td></tr>
-              <tr v-if="data.genres && data.genres.data.length"><td>Genres</td><td>{{ data.genres.data.map(x => x.name).join("; ") }}</td></tr>
-            </table>
-            <div v-if="countries.length">
-              <p v-for="country in countries">{{ country[0] }} - {{ country[1] }}</p>
-            </div>
+						<table>
+							<tr v-if="data.isrc">
+								<td>ISRC</td>
+								<td>{{ data.isrc }}</td>
+							</tr>
+							<tr v-if="data.upc">
+								<td>UPC</td>
+								<td>{{ data.upc }}</td>
+							</tr>
+							<tr v-if="data.duration">
+								<td>Duration</td>
+								<td>{{ convertDuration(data.duration) }}</td>
+							</tr>
+							<tr v-if="data.disk_number">
+								<td>Disk Number</td>
+								<td>{{ data.disk_number }}</td>
+							</tr>
+							<tr v-if="data.track_position">
+								<td>Track Number</td>
+								<td>{{ data.track_position }}</td>
+							</tr>
+							<tr v-if="data.release_date">
+								<td>Release Date</td>
+								<td>{{ data.release_date }}</td>
+							</tr>
+							<tr v-if="data.bpm">
+								<td>BPM</td>
+								<td>{{ data.bpm }}</td>
+							</tr>
+							<tr v-if="data.label">
+								<td>Label</td>
+								<td>{{ data.label }}</td>
+							</tr>
+							<tr v-if="data.record_type">
+								<td>Record Type</td>
+								<td>{{ data.record_type }}</td>
+							</tr>
+							<tr v-if="data.genres && data.genres.data.length">
+								<td>Genres</td>
+								<td>{{ data.genres.data.map(x => x.name).join("; ") }}</td>
+							</tr>
+						</table>
+						<div v-if="countries.length">
+							<p v-for="country in countries">{{ country[0] }} - {{ country[1] }}</p>
+						</div>
 					</div>
 
 					<div id="settings_tab" class="main_tabcontent fixed_footer">
@@ -551,10 +586,13 @@ <h2 class="inline-flex"><span v-if="metadata">{{ metadata }}</span><span class="
 							<tbody>
 								<template v-for="track in body">
 									<tr v-if="track.type == 'track'">
-										<td><i @click=playPausePreview v-bind:class="'material-icons' + (track.preview ? ' preview_playlist_controls' : '')" v-bind:data-preview="track.preview">play_arrow</i></td>
+										<td><i @click=playPausePreview
+												v-bind:class="'material-icons' + (track.preview ? ' preview_playlist_controls' : '')"
+												v-bind:data-preview="track.preview">play_arrow</i></td>
 										<td>{{ track.track_position }}</td>
 										<td class="inline-flex"><i v-if="track.explicit_lyrics"
-												class="material-icons">explicit</i>{{ track.title + (track.title_version && track.title.indexOf(track.title_version) == -1 ? ' '+track.title_version : '') }} </td>
+												class="material-icons">explicit</i>{{ track.title + (track.title_version && track.title.indexOf(track.title_version) == -1 ? ' '+track.title_version : '') }}
+										</td>
 										<td class="clickable" @click="artistView" v-bind:data-id="track.artist.id">
 											{{ track.artist.name }}</td>
 										<td class="clickable" v-if="type == 'Playlist'" @click="albumView" v-bind:data-id="track.album.id">
@@ -586,8 +624,8 @@ <h2 class="inline-flex"><span v-if="metadata">{{ metadata }}</span><span class="
 		</div>
 
 		<div id="download_tab_container" class="tab_hidden">
+			<div id="download_tab_drag_handler"></div>
 			<i id="toggle_download_tab" class="material-icons download_bar_icon"></i>
-			<!-- <div id="queue_buttons" class="right"> -->
 			<div id="queue_buttons">
 				<i id="clean_queue" class="material-icons download_bar_icon">clear_all</i>
 				<i id="cancel_queue" class="material-icons download_bar_icon">delete_sweep</i>
@@ -596,11 +634,11 @@ <h2 class="inline-flex"><span v-if="metadata">{{ metadata }}</span><span class="
 		</div>
 	</main>
 
-  <audio id="preview-track">
-  	<source id="preview-track_source" src="" type="audio/mpeg">
-  </audio>
+	<audio id="preview-track">
+		<source id="preview-track_source" src="" type="audio/mpeg">
+	</audio>
 
-  <div id="modal_quality" class="smallmodal">
+	<div id="modal_quality" class="smallmodal">
 		<!-- Modal content -->
 		<div class="smallmodal-content">
 			<button class="quality-button" data-quality-value="9">Download FLAC</button><br>
@@ -620,4 +658,4 @@ <h2 class="inline-flex"><span v-if="metadata">{{ metadata }}</span><span class="
 
 <script type="module" src="/public/js/app.js"></script>
 
-</html>
+</html>
\ No newline at end of file
diff --git a/public/js/app.js b/public/js/app.js
index 2874d79..bd76e90 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -71,11 +71,11 @@ socket.on('logged_out', function () {
 
 /* ===== App initialization ===== */
 function startApp() {
-	Downloads.linkListeners()
+	Downloads.init()
 	QualityModal.init()
 	Tabs.linkListeners()
 	Search.linkListeners()
-  initTrackPreview()
+	initTrackPreview()
 
 	if (localStorage.getItem('arl')) {
 		let arl = localStorage.getItem('arl')
@@ -84,11 +84,6 @@ function startApp() {
 		$('#login_input_arl').val(arl)
 	}
 
-	// Check if download tab should be open
-	if ('true' === localStorage.getItem('downloadTabOpen')) {
-		document.querySelector('#download_tab_container').classList.remove('tab_hidden')
-	}
-
 	// Open default tab
 	document.getElementById('main_home_tablink').click()
 }
diff --git a/public/js/modules/downloads.js b/public/js/modules/downloads.js
index 57644bd..4dc2ff9 100644
--- a/public/js/modules/downloads.js
+++ b/public/js/modules/downloads.js
@@ -1,14 +1,36 @@
 import { socket } from './socket.js'
 import { toast } from './toasts.js'
+import Utils from './utils.js'
 
+/* ===== Locals ===== */
+const tabMinWidth = 250
+const tabMaxWidth = 500
+let cachedTabWidth = parseInt(localStorage.getItem('downloadTabWidth')) || 300
 let queueList = {}
 let queue = []
 let queueComplete = []
+let tabContainerEl
+let listEl
+let dragHandlerEl
 
-const downloadListEl = document.getElementById('download_list')
+function init() {
+	// Find download DOM elements
+	tabContainerEl = document.getElementById('download_tab_container')
+	listEl = document.getElementById('download_list')
+	dragHandlerEl = document.getElementById('download_tab_drag_handler')
+
+	// Check if download tab should be open
+	if ('true' === localStorage.getItem('downloadTabOpen')) {
+		tabContainerEl.classList.remove('tab_hidden')
+
+		setTabWidth(cachedTabWidth)
+	}
+
+	linkListeners()
+}
 
 function linkListeners() {
-	downloadListEl.addEventListener('click', handleListClick)
+	listEl.addEventListener('click', handleListClick)
 	document.getElementById('toggle_download_tab').addEventListener('click', toggleDownloadTab)
 
 	// Queue buttons
@@ -19,6 +41,48 @@ function linkListeners() {
 	document.getElementById('cancel_queue').addEventListener('click', () => {
 		socket.emit('cancelAllDownloads')
 	})
+
+	dragHandlerEl.addEventListener('mousedown', event => {
+		event.preventDefault()
+
+		document.addEventListener('mousemove', handleDrag)
+	})
+
+	document.addEventListener('mouseup', () => {
+		document.removeEventListener('mousemove', handleDrag)
+	})
+
+	tabContainerEl.addEventListener('transitionend', () => {
+		tabContainerEl.style.transition = ''
+	})
+
+	window.addEventListener('beforeunload', () => {
+		localStorage.setItem('downloadTabWidth', cachedTabWidth)
+	})
+}
+
+function setTabWidth(newWidth) {
+	if (undefined === newWidth) {
+		tabContainerEl.style.width = ''
+		listEl.style.width = ''
+	} else {
+		tabContainerEl.style.width = newWidth + 'px'
+		listEl.style.width = newWidth + 'px'
+	}
+}
+
+function handleDrag(event) {
+	let newWidth = window.innerWidth - event.pageX + 2
+
+	if (newWidth < tabMinWidth) {
+		newWidth = tabMinWidth
+	} else if (newWidth > tabMaxWidth) {
+		newWidth = tabMaxWidth
+	}
+
+	cachedTabWidth = newWidth
+
+	setTabWidth(newWidth)
 }
 
 function sendAddToQueue(url, bitrate = null) {
@@ -39,7 +103,7 @@ function addToQueue(queueItem, current = false) {
 	} else {
 		queue.push(queueItem.uuid)
 	}
-	$(downloadListEl).append(
+	$(listEl).append(
 		`<div class="download_object" id="download_${queueItem.uuid}" data-deezerid="${queueItem.id}">
 		<div class="download_info">
 			<img width="75px" class="rounded coverart" src="${queueItem.cover}" alt="Cover ${queueItem.title}"/>
@@ -124,7 +188,16 @@ function handleListClick(event) {
 function toggleDownloadTab(ev) {
 	ev.preventDefault()
 
-	let isHidden = document.querySelector('#download_tab_container').classList.toggle('tab_hidden')
+	setTabWidth()
+
+	tabContainerEl.style.transition = 'all 250ms ease-in-out'
+
+	// Toggle returns a Boolean based on the action it performed
+	let isHidden = tabContainerEl.classList.toggle('tab_hidden')
+
+	if (!isHidden) {
+		setTabWidth(cachedTabWidth)
+	}
 
 	localStorage.setItem('downloadTabOpen', !isHidden)
 }
@@ -141,18 +214,8 @@ function removeFromQueue(uuid) {
 	}
 }
 
-// Needs:
-// 1. socket
-// 2. queue
-// 3. queueList
 socket.on('removedFromQueue', removeFromQueue)
 
-// Needs:
-// 1. socket
-// 2. queue
-// 3. queueList
-// 4. queueComplete
-// 5. toast
 function finishDownload(uuid) {
 	if (queue.indexOf(uuid) > -1) {
 		toast(`${queueList[uuid].title} finished downloading.`, 'done')
@@ -178,18 +241,12 @@ function finishDownload(uuid) {
 
 socket.on('finishDownload', finishDownload)
 
-// Needs:
-// 1. socket
-// 2. queueComplete
-// 3. queue
-// 4. queueList
-
 function removeAllDownloads(currentItem) {
 	queueComplete = []
 	if (currentItem == '') {
 		queue = []
 		queueList = {}
-		$(downloadListEl).html('')
+		$(listEl).html('')
 	} else {
 		queue = [currentItem]
 		let tempQueueItem = queueList[currentItem]
@@ -203,9 +260,6 @@ function removeAllDownloads(currentItem) {
 
 socket.on('removedAllDownloads', removeAllDownloads)
 
-// Needs:
-// 1. socket
-// 2. queueComplete
 function removedFinishedDownloads() {
 	queueComplete.forEach(item => {
 		$('#download_' + item).remove()
@@ -215,10 +269,6 @@ function removedFinishedDownloads() {
 
 socket.on('removedFinishedDownloads', removedFinishedDownloads)
 
-// Needs:
-// 1. socket
-// 2. queue
-// 3. queueList
 function updateQueue(update) {
 	if (update.uuid && queue.indexOf(update.uuid) > -1) {
 		if (update.downloaded) {
@@ -250,7 +300,7 @@ function updateQueue(update) {
 socket.on('updateQueue', updateQueue)
 
 export default {
-	linkListeners,
+	init,
 	sendAddToQueue,
 	addToQueue
 }
diff --git a/public/js/modules/link-analyzer-tab.js b/public/js/modules/link-analyzer-tab.js
index 2139d31..862f01d 100644
--- a/public/js/modules/link-analyzer-tab.js
+++ b/public/js/modules/link-analyzer-tab.js
@@ -2,59 +2,59 @@ import { socket } from './socket.js'
 import { albumView } from './tabs.js'
 import Utils from './utils.js'
 
-const COUNTRIES = {"AF": "Afghanistan","AX": "\u00c5land Islands","AL": "Albania","DZ": "Algeria","AS": "American Samoa","AD": "Andorra","AO": "Angola","AI": "Anguilla","AQ": "Antarctica","AG": "Antigua and Barbuda","AR": "Argentina","AM": "Armenia","AW": "Aruba","AU": "Australia","AT": "Austria","AZ": "Azerbaijan","BS": "Bahamas","BH": "Bahrain","BD": "Bangladesh","BB": "Barbados","BY": "Belarus","BE": "Belgium","BZ": "Belize","BJ": "Benin","BM": "Bermuda","BT": "Bhutan","BO": "Bolivia, Plurinational State of","BQ": "Bonaire, Sint Eustatius and Saba","BA": "Bosnia and Herzegovina","BW": "Botswana","BV": "Bouvet Island","BR": "Brazil","IO": "British Indian Ocean Territory","BN": "Brunei Darussalam","BG": "Bulgaria","BF": "Burkina Faso","BI": "Burundi","KH": "Cambodia","CM": "Cameroon","CA": "Canada","CV": "Cape Verde","KY": "Cayman Islands","CF": "Central African Republic","TD": "Chad","CL": "Chile","CN": "China","CX": "Christmas Island","CC": "Cocos (Keeling) Islands","CO": "Colombia","KM": "Comoros","CG": "Congo","CD": "Congo, the Democratic Republic of the","CK": "Cook Islands","CR": "Costa Rica","CI": "C\u00f4te d'Ivoire","HR": "Croatia","CU": "Cuba","CW": "Cura\u00e7ao","CY": "Cyprus","CZ": "Czech Republic","DK": "Denmark","DJ": "Djibouti","DM": "Dominica","DO": "Dominican Republic","EC": "Ecuador","EG": "Egypt","SV": "El Salvador","GQ": "Equatorial Guinea","ER": "Eritrea","EE": "Estonia","ET": "Ethiopia","FK": "Falkland Islands (Malvinas)","FO": "Faroe Islands","FJ": "Fiji","FI": "Finland","FR": "France","GF": "French Guiana","PF": "French Polynesia","TF": "French Southern Territories","GA": "Gabon","GM": "Gambia","GE": "Georgia","DE": "Germany","GH": "Ghana","GI": "Gibraltar","GR": "Greece","GL": "Greenland","GD": "Grenada","GP": "Guadeloupe","GU": "Guam","GT": "Guatemala","GG": "Guernsey","GN": "Guinea","GW": "Guinea-Bissau","GY": "Guyana","HT": "Haiti","HM": "Heard Island and McDonald Islands","VA": "Holy See (Vatican City State)","HN": "Honduras","HK": "Hong Kong","HU": "Hungary","IS": "Iceland","IN": "India","ID": "Indonesia","IR": "Iran, Islamic Republic of","IQ": "Iraq","IE": "Ireland","IM": "Isle of Man","IL": "Israel","IT": "Italy","JM": "Jamaica","JP": "Japan","JE": "Jersey","JO": "Jordan","KZ": "Kazakhstan","KE": "Kenya","KI": "Kiribati","KP": "Korea, Democratic People's Republic of","KR": "Korea, Republic of","KW": "Kuwait","KG": "Kyrgyzstan","LA": "Lao People's Democratic Republic","LV": "Latvia","LB": "Lebanon","LS": "Lesotho","LR": "Liberia","LY": "Libya","LI": "Liechtenstein","LT": "Lithuania","LU": "Luxembourg","MO": "Macao","MK": "Macedonia, the Former Yugoslav Republic of","MG": "Madagascar","MW": "Malawi","MY": "Malaysia","MV": "Maldives","ML": "Mali","MT": "Malta","MH": "Marshall Islands","MQ": "Martinique","MR": "Mauritania","MU": "Mauritius","YT": "Mayotte","MX": "Mexico","FM": "Micronesia, Federated States of","MD": "Moldova, Republic of","MC": "Monaco","MN": "Mongolia","ME": "Montenegro","MS": "Montserrat","MA": "Morocco","MZ": "Mozambique","MM": "Myanmar","NA": "Namibia","NR": "Nauru","NP": "Nepal","NL": "Netherlands","NC": "New Caledonia","NZ": "New Zealand","NI": "Nicaragua","NE": "Niger","NG": "Nigeria","NU": "Niue","NF": "Norfolk Island","MP": "Northern Mariana Islands","NO": "Norway","OM": "Oman","PK": "Pakistan","PW": "Palau","PS": "Palestine, State of","PA": "Panama","PG": "Papua New Guinea","PY": "Paraguay","PE": "Peru","PH": "Philippines","PN": "Pitcairn","PL": "Poland","PT": "Portugal","PR": "Puerto Rico","QA": "Qatar","RE": "R\u00e9union","RO": "Romania","RU": "Russian Federation","RW": "Rwanda","BL": "Saint Barth\u00e9lemy","SH": "Saint Helena, Ascension and Tristan da Cunha","KN": "Saint Kitts and Nevis","LC": "Saint Lucia","MF": "Saint Martin (French part)","PM": "Saint Pierre and Miquelon","VC": "Saint Vincent and the Grenadines","WS": "Samoa","SM": "San Marino","ST": "Sao Tome and Principe","SA": "Saudi Arabia","SN": "Senegal","RS": "Serbia","SC": "Seychelles","SL": "Sierra Leone","SG": "Singapore","SX": "Sint Maarten (Dutch part)","SK": "Slovakia","SI": "Slovenia","SB": "Solomon Islands","SO": "Somalia","ZA": "South Africa","GS": "South Georgia and the South Sandwich Islands","SS": "South Sudan","ES": "Spain","LK": "Sri Lanka","SD": "Sudan","SR": "Suriname","SJ": "Svalbard and Jan Mayen","SZ": "Swaziland","SE": "Sweden","CH": "Switzerland","SY": "Syrian Arab Republic","TW": "Taiwan, Province of China","TJ": "Tajikistan","TZ": "Tanzania, United Republic of","TH": "Thailand","TL": "Timor-Leste","TG": "Togo","TK": "Tokelau","TO": "Tonga","TT": "Trinidad and Tobago","TN": "Tunisia","TR": "Turkey","TM": "Turkmenistan","TC": "Turks and Caicos Islands","TV": "Tuvalu","UG": "Uganda","UA": "Ukraine","AE": "United Arab Emirates","GB": "United Kingdom","US": "United States","UM": "United States Minor Outlying Islands","UY": "Uruguay","UZ": "Uzbekistan","VU": "Vanuatu","VE": "Venezuela, Bolivarian Republic of","VN": "Viet Nam","VG": "Virgin Islands, British","VI": "Virgin Islands, U.S.","WF": "Wallis and Futuna","EH": "Western Sahara","YE": "Yemen","ZM": "Zambia","ZW": "Zimbabwe"}
-
 const LinkAnalyzerTab = new Vue({
 	data() {
 		return {
-      title: '',
+			title: '',
 			subtitle: '',
-      image: '',
+			image: '',
 			data: {},
 			type: '',
 			link: '',
-      countries: []
+			countries: []
 		}
 	},
 	methods: {
 		albumView,
-    convertDuration: Utils.convertDuration,
+		convertDuration: Utils.convertDuration,
 		reset() {
 			this.title = 'Loading...'
 			this.subtitle = ''
-      this.image = ''
+			this.image = ''
 			this.data = {}
 			this.type = ''
 			this.link = ''
-      this.countries = []
+			this.countries = []
 		},
 		showTrack(data) {
-			this.title = data.title + (data.title_version && data.title.indexOf(data.title_version) == -1 ? ' '+data.title_version : '')
-      this.subtitle = `by ${data.artist.name}\nin ${data.album.title}`
-      this.image = data.album.cover_xl
+			this.title =
+				data.title +
+				(data.title_version && data.title.indexOf(data.title_version) == -1 ? ' ' + data.title_version : '')
+			this.subtitle = `by ${data.artist.name}\nin ${data.album.title}`
+			this.image = data.album.cover_xl
 			this.type = 'track'
 			this.link = data.link
-      data.available_countries.forEach((cc)=>{
-        let temp = []
-        let chars = [...cc].map(c => c.charCodeAt() + 127397)
-        temp.push(String.fromCodePoint(...chars))
-        temp.push(COUNTRIES[cc])
-        this.countries.push(temp)
-      })
-      this.data = data
+			data.available_countries.forEach(cc => {
+				let temp = []
+				let chars = [...cc].map(c => c.charCodeAt() + 127397)
+				temp.push(String.fromCodePoint(...chars))
+				temp.push(Utils.COUNTRIES[cc])
+				this.countries.push(temp)
+			})
+			this.data = data
 		},
 		showAlbum(data) {
-      console.log(data)
+			console.log(data)
 			this.title = data.title
-      this.subtitle = `by ${data.artist.name}\n${data.nb_tracks} tracks`
-      this.image = data.cover_xl
+			this.subtitle = `by ${data.artist.name}\n${data.nb_tracks} tracks`
+			this.image = data.cover_xl
 			this.type = 'album'
 			this.link = data.link
-      this.data = data
+			this.data = data
 		}
 	},
 	mounted() {
-    socket.on('analyze_track', this.showTrack)
+		socket.on('analyze_track', this.showTrack)
 		socket.on('analyze_album', this.showAlbum)
 	}
 }).$mount('#analyzer_tab')
diff --git a/public/js/modules/search.js b/public/js/modules/search.js
index 3c37334..8024583 100644
--- a/public/js/modules/search.js
+++ b/public/js/modules/search.js
@@ -32,11 +32,11 @@ export default class Search {
 				if (e.ctrlKey) {
 					QualityModal.open(term)
 				} else {
-          if (window.main_selected  == 'analyzer_tab'){
-            analyzeLink(term)
-          }else{
-            Downloads.sendAddToQueue(term)
-          }
+					if (window.main_selected == 'analyzer_tab') {
+						analyzeLink(term)
+					} else {
+						Downloads.sendAddToQueue(term)
+					}
 				}
 			} else {
 				if (term != MainSearch.query || main_selected == 'search_tab') {
diff --git a/public/js/modules/tabs.js b/public/js/modules/tabs.js
index 39a3cdd..69f6df4 100644
--- a/public/js/modules/tabs.js
+++ b/public/js/modules/tabs.js
@@ -36,9 +36,9 @@ export function playlistView(ev) {
 }
 
 export function analyzeLink(link) {
-  console.log("Analyzing: "+link)
-  LinkAnalyzerTab.reset()
-  socket.emit('analyzeLink', link)
+	console.log('Analyzing: ' + link)
+	LinkAnalyzerTab.reset()
+	socket.emit('analyzeLink', link)
 }
 
 export class Tabs {
@@ -190,7 +190,7 @@ function showTab(type, id, back = false) {
 		tabcontent[i].style.display = 'none'
 	}
 	document.getElementById(tab).style.display = 'block'
-  stopStackedTabsPreview()
+	stopStackedTabsPreview()
 }
 
 // Uses:
@@ -213,5 +213,5 @@ function backTab() {
 		socket.emit('getTracklist', { type: tabObj.type, id: tabObj.id })
 		showTab(tabObj.type, tabObj.id, true)
 	}
-  stopStackedTabsPreview()
+	stopStackedTabsPreview()
 }
diff --git a/public/js/modules/track-preview.js b/public/js/modules/track-preview.js
index 6daa4e7..e662f5f 100644
--- a/public/js/modules/track-preview.js
+++ b/public/js/modules/track-preview.js
@@ -1,4 +1,3 @@
-
 /* ===== Globals ====== */
 window.preview_max_volume = 1
 
@@ -7,85 +6,91 @@ let preview_track = document.getElementById('preview-track')
 let preview_stopped = true
 
 // init stuff
-export function initTrackPreview(){
-  preview_track.volume = 1
-  /*preview_max_volume = parseFloat(localStorage.getItem("previewVolume"))
+export function initTrackPreview() {
+	preview_track.volume = 1
+	/*preview_max_volume = parseFloat(localStorage.getItem("previewVolume"))
   if (preview_max_volume === null){
     preview_max_volume = 0.8
     localStorage.setItem("previewVolume", preview_max_volume)
   }*/
 
-  // start playing when track loaded
-  preview_track.addEventListener('canplay', function(){
-    preview_track.play()
-    preview_stopped = false
-    $(preview_track).animate({volume: preview_max_volume}, 500)
-  })
+	// start playing when track loaded
+	preview_track.addEventListener('canplay', function () {
+		preview_track.play()
+		preview_stopped = false
+		$(preview_track).animate({ volume: preview_max_volume }, 500)
+	})
 
-  // auto fadeout when at the end of the song
-  preview_track.addEventListener('timeupdate', function(){
-    if (preview_track.currentTime > preview_track.duration-1){
-      $(preview_track).animate({volume: 0}, 800)
-      preview_stopped = true
-      $('a[playing] > .preview_controls').css({opacity:0})
-      $("*").removeAttr("playing")
-      $('.preview_controls').text("play_arrow")
-      $('.preview_playlist_controls').text("play_arrow")
-    }
-  })
+	// auto fadeout when at the end of the song
+	preview_track.addEventListener('timeupdate', function () {
+		if (preview_track.currentTime > preview_track.duration - 1) {
+			$(preview_track).animate({ volume: 0 }, 800)
+			preview_stopped = true
+			$('a[playing] > .preview_controls').css({ opacity: 0 })
+			$('*').removeAttr('playing')
+			$('.preview_controls').text('play_arrow')
+			$('.preview_playlist_controls').text('play_arrow')
+		}
+	})
 }
 
 // on modal closing
-export function stopStackedTabsPreview(){
-  if ($('.preview_playlist_controls').filter(function(){return $(this).attr("playing")}).length > 0){
-    $(preview_track).animate({volume: 0}, 800)
-    preview_stopped = true
-    $(".preview_playlist_controls").removeAttr("playing")
-    $('.preview_playlist_controls').text("play_arrow")
-  }
+export function stopStackedTabsPreview() {
+	if (
+		$('.preview_playlist_controls').filter(function () {
+			return $(this).attr('playing')
+		}).length > 0
+	) {
+		$(preview_track).animate({ volume: 0 }, 800)
+		preview_stopped = true
+		$('.preview_playlist_controls').removeAttr('playing')
+		$('.preview_playlist_controls').text('play_arrow')
+	}
 }
 
 // on hover event
-export function previewMouseEnter(e){
-  $(e.currentTarget).css({opacity: 1})
+export function previewMouseEnter(e) {
+	$(e.currentTarget).css({ opacity: 1 })
 }
-export function previewMouseLeave(e){
-  let obj = e.currentTarget
-  if (($(obj).parent().attr("playing") && preview_stopped) || !$(obj).parent().attr("playing")){
-    $(obj).css({opacity: 0}, 200)
-  }
+export function previewMouseLeave(e) {
+	let obj = e.currentTarget
+	if (($(obj).parent().attr('playing') && preview_stopped) || !$(obj).parent().attr('playing')) {
+		$(obj).css({ opacity: 0 }, 200)
+	}
 }
 
 // on click event
-export function playPausePreview(e){
-  e.preventDefault()
-  console.log("PlayPause")
-  let obj = e.currentTarget
-  var icon = (obj.tagName == "I" ? $(obj) : $(obj).children('i'))
-  if ($(obj).attr("playing")){
-    if (preview_track.paused){
-      preview_track.play()
-      preview_stopped = false
-      icon.text("pause")
-      $(preview_track).animate({volume: preview_max_volume}, 500)
-    }else{
-      preview_stopped = true
-      icon.text("play_arrow")
-      $(preview_track).animate({volume: 0}, 250, "swing", ()=>{ preview_track.pause() })
-    }
-  }else{
-    $("*").removeAttr("playing")
-    $(obj).attr("playing",true)
-    $('.preview_controls').text("play_arrow")
-    $('.preview_playlist_controls').text("play_arrow")
-    $('.preview_controls').css({opacity:0})
-    icon.text("pause")
-    icon.css({opacity: 1})
-    preview_stopped = false
-    $(preview_track).animate({volume: 0}, 250, "swing", ()=>{
-      preview_track.pause()
-      $('#preview-track_source').prop("src", $(obj).data("preview"))
-      preview_track.load()
-    })
-  }
+export function playPausePreview(e) {
+	e.preventDefault()
+	console.log('PlayPause')
+	let obj = e.currentTarget
+	var icon = obj.tagName == 'I' ? $(obj) : $(obj).children('i')
+	if ($(obj).attr('playing')) {
+		if (preview_track.paused) {
+			preview_track.play()
+			preview_stopped = false
+			icon.text('pause')
+			$(preview_track).animate({ volume: preview_max_volume }, 500)
+		} else {
+			preview_stopped = true
+			icon.text('play_arrow')
+			$(preview_track).animate({ volume: 0 }, 250, 'swing', () => {
+				preview_track.pause()
+			})
+		}
+	} else {
+		$('*').removeAttr('playing')
+		$(obj).attr('playing', true)
+		$('.preview_controls').text('play_arrow')
+		$('.preview_playlist_controls').text('play_arrow')
+		$('.preview_controls').css({ opacity: 0 })
+		icon.text('pause')
+		icon.css({ opacity: 1 })
+		preview_stopped = false
+		$(preview_track).animate({ volume: 0 }, 250, 'swing', () => {
+			preview_track.pause()
+			$('#preview-track_source').prop('src', $(obj).data('preview'))
+			preview_track.load()
+		})
+	}
 }
diff --git a/public/js/modules/utils.js b/public/js/modules/utils.js
index c59c4ec..a58bcb4 100644
--- a/public/js/modules/utils.js
+++ b/public/js/modules/utils.js
@@ -7,7 +7,7 @@ function isValidURL(text) {
 }
 
 function convertDuration(duration) {
-	//convert from seconds only to mm:ss format
+	// convert from seconds only to mm:ss format
 	let mm, ss
 	mm = Math.floor(duration / 60)
 	ss = duration - mm * 60
@@ -28,7 +28,7 @@ function convertDurationSeparated(duration) {
 }
 
 function numberWithDots(x) {
-  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.')
+	return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.')
 }
 
 // On scroll event, returns currentTarget = null
@@ -49,10 +49,263 @@ function debounce(func, wait, immediate) {
 	}
 }
 
+const COUNTRIES = {
+	AF: 'Afghanistan',
+	AX: '\u00c5land Islands',
+	AL: 'Albania',
+	DZ: 'Algeria',
+	AS: 'American Samoa',
+	AD: 'Andorra',
+	AO: 'Angola',
+	AI: 'Anguilla',
+	AQ: 'Antarctica',
+	AG: 'Antigua and Barbuda',
+	AR: 'Argentina',
+	AM: 'Armenia',
+	AW: 'Aruba',
+	AU: 'Australia',
+	AT: 'Austria',
+	AZ: 'Azerbaijan',
+	BS: 'Bahamas',
+	BH: 'Bahrain',
+	BD: 'Bangladesh',
+	BB: 'Barbados',
+	BY: 'Belarus',
+	BE: 'Belgium',
+	BZ: 'Belize',
+	BJ: 'Benin',
+	BM: 'Bermuda',
+	BT: 'Bhutan',
+	BO: 'Bolivia, Plurinational State of',
+	BQ: 'Bonaire, Sint Eustatius and Saba',
+	BA: 'Bosnia and Herzegovina',
+	BW: 'Botswana',
+	BV: 'Bouvet Island',
+	BR: 'Brazil',
+	IO: 'British Indian Ocean Territory',
+	BN: 'Brunei Darussalam',
+	BG: 'Bulgaria',
+	BF: 'Burkina Faso',
+	BI: 'Burundi',
+	KH: 'Cambodia',
+	CM: 'Cameroon',
+	CA: 'Canada',
+	CV: 'Cape Verde',
+	KY: 'Cayman Islands',
+	CF: 'Central African Republic',
+	TD: 'Chad',
+	CL: 'Chile',
+	CN: 'China',
+	CX: 'Christmas Island',
+	CC: 'Cocos (Keeling) Islands',
+	CO: 'Colombia',
+	KM: 'Comoros',
+	CG: 'Congo',
+	CD: 'Congo, the Democratic Republic of the',
+	CK: 'Cook Islands',
+	CR: 'Costa Rica',
+	CI: "C\u00f4te d'Ivoire",
+	HR: 'Croatia',
+	CU: 'Cuba',
+	CW: 'Cura\u00e7ao',
+	CY: 'Cyprus',
+	CZ: 'Czech Republic',
+	DK: 'Denmark',
+	DJ: 'Djibouti',
+	DM: 'Dominica',
+	DO: 'Dominican Republic',
+	EC: 'Ecuador',
+	EG: 'Egypt',
+	SV: 'El Salvador',
+	GQ: 'Equatorial Guinea',
+	ER: 'Eritrea',
+	EE: 'Estonia',
+	ET: 'Ethiopia',
+	FK: 'Falkland Islands (Malvinas)',
+	FO: 'Faroe Islands',
+	FJ: 'Fiji',
+	FI: 'Finland',
+	FR: 'France',
+	GF: 'French Guiana',
+	PF: 'French Polynesia',
+	TF: 'French Southern Territories',
+	GA: 'Gabon',
+	GM: 'Gambia',
+	GE: 'Georgia',
+	DE: 'Germany',
+	GH: 'Ghana',
+	GI: 'Gibraltar',
+	GR: 'Greece',
+	GL: 'Greenland',
+	GD: 'Grenada',
+	GP: 'Guadeloupe',
+	GU: 'Guam',
+	GT: 'Guatemala',
+	GG: 'Guernsey',
+	GN: 'Guinea',
+	GW: 'Guinea-Bissau',
+	GY: 'Guyana',
+	HT: 'Haiti',
+	HM: 'Heard Island and McDonald Islands',
+	VA: 'Holy See (Vatican City State)',
+	HN: 'Honduras',
+	HK: 'Hong Kong',
+	HU: 'Hungary',
+	IS: 'Iceland',
+	IN: 'India',
+	ID: 'Indonesia',
+	IR: 'Iran, Islamic Republic of',
+	IQ: 'Iraq',
+	IE: 'Ireland',
+	IM: 'Isle of Man',
+	IL: 'Israel',
+	IT: 'Italy',
+	JM: 'Jamaica',
+	JP: 'Japan',
+	JE: 'Jersey',
+	JO: 'Jordan',
+	KZ: 'Kazakhstan',
+	KE: 'Kenya',
+	KI: 'Kiribati',
+	KP: "Korea, Democratic People's Republic of",
+	KR: 'Korea, Republic of',
+	KW: 'Kuwait',
+	KG: 'Kyrgyzstan',
+	LA: "Lao People's Democratic Republic",
+	LV: 'Latvia',
+	LB: 'Lebanon',
+	LS: 'Lesotho',
+	LR: 'Liberia',
+	LY: 'Libya',
+	LI: 'Liechtenstein',
+	LT: 'Lithuania',
+	LU: 'Luxembourg',
+	MO: 'Macao',
+	MK: 'Macedonia, the Former Yugoslav Republic of',
+	MG: 'Madagascar',
+	MW: 'Malawi',
+	MY: 'Malaysia',
+	MV: 'Maldives',
+	ML: 'Mali',
+	MT: 'Malta',
+	MH: 'Marshall Islands',
+	MQ: 'Martinique',
+	MR: 'Mauritania',
+	MU: 'Mauritius',
+	YT: 'Mayotte',
+	MX: 'Mexico',
+	FM: 'Micronesia, Federated States of',
+	MD: 'Moldova, Republic of',
+	MC: 'Monaco',
+	MN: 'Mongolia',
+	ME: 'Montenegro',
+	MS: 'Montserrat',
+	MA: 'Morocco',
+	MZ: 'Mozambique',
+	MM: 'Myanmar',
+	NA: 'Namibia',
+	NR: 'Nauru',
+	NP: 'Nepal',
+	NL: 'Netherlands',
+	NC: 'New Caledonia',
+	NZ: 'New Zealand',
+	NI: 'Nicaragua',
+	NE: 'Niger',
+	NG: 'Nigeria',
+	NU: 'Niue',
+	NF: 'Norfolk Island',
+	MP: 'Northern Mariana Islands',
+	NO: 'Norway',
+	OM: 'Oman',
+	PK: 'Pakistan',
+	PW: 'Palau',
+	PS: 'Palestine, State of',
+	PA: 'Panama',
+	PG: 'Papua New Guinea',
+	PY: 'Paraguay',
+	PE: 'Peru',
+	PH: 'Philippines',
+	PN: 'Pitcairn',
+	PL: 'Poland',
+	PT: 'Portugal',
+	PR: 'Puerto Rico',
+	QA: 'Qatar',
+	RE: 'R\u00e9union',
+	RO: 'Romania',
+	RU: 'Russian Federation',
+	RW: 'Rwanda',
+	BL: 'Saint Barth\u00e9lemy',
+	SH: 'Saint Helena, Ascension and Tristan da Cunha',
+	KN: 'Saint Kitts and Nevis',
+	LC: 'Saint Lucia',
+	MF: 'Saint Martin (French part)',
+	PM: 'Saint Pierre and Miquelon',
+	VC: 'Saint Vincent and the Grenadines',
+	WS: 'Samoa',
+	SM: 'San Marino',
+	ST: 'Sao Tome and Principe',
+	SA: 'Saudi Arabia',
+	SN: 'Senegal',
+	RS: 'Serbia',
+	SC: 'Seychelles',
+	SL: 'Sierra Leone',
+	SG: 'Singapore',
+	SX: 'Sint Maarten (Dutch part)',
+	SK: 'Slovakia',
+	SI: 'Slovenia',
+	SB: 'Solomon Islands',
+	SO: 'Somalia',
+	ZA: 'South Africa',
+	GS: 'South Georgia and the South Sandwich Islands',
+	SS: 'South Sudan',
+	ES: 'Spain',
+	LK: 'Sri Lanka',
+	SD: 'Sudan',
+	SR: 'Suriname',
+	SJ: 'Svalbard and Jan Mayen',
+	SZ: 'Swaziland',
+	SE: 'Sweden',
+	CH: 'Switzerland',
+	SY: 'Syrian Arab Republic',
+	TW: 'Taiwan, Province of China',
+	TJ: 'Tajikistan',
+	TZ: 'Tanzania, United Republic of',
+	TH: 'Thailand',
+	TL: 'Timor-Leste',
+	TG: 'Togo',
+	TK: 'Tokelau',
+	TO: 'Tonga',
+	TT: 'Trinidad and Tobago',
+	TN: 'Tunisia',
+	TR: 'Turkey',
+	TM: 'Turkmenistan',
+	TC: 'Turks and Caicos Islands',
+	TV: 'Tuvalu',
+	UG: 'Uganda',
+	UA: 'Ukraine',
+	AE: 'United Arab Emirates',
+	GB: 'United Kingdom',
+	US: 'United States',
+	UM: 'United States Minor Outlying Islands',
+	UY: 'Uruguay',
+	UZ: 'Uzbekistan',
+	VU: 'Vanuatu',
+	VE: 'Venezuela, Bolivarian Republic of',
+	VN: 'Viet Nam',
+	VG: 'Virgin Islands, British',
+	VI: 'Virgin Islands, U.S.',
+	WF: 'Wallis and Futuna',
+	EH: 'Western Sahara',
+	YE: 'Yemen',
+	ZM: 'Zambia',
+	ZW: 'Zimbabwe'
+}
+
 export default {
 	isValidURL,
 	convertDuration,
 	convertDurationSeparated,
-  numberWithDots,
-	debounce
+	numberWithDots,
+	debounce,
+	COUNTRIES
 }