Compare commits
	
		
			9 Commits
		
	
	
		
			372d151264
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| aee3052ad7 | |||
| a93598d8f6 | |||
| e165367133 | |||
| 96bbef8bbd | |||
| 95b0479600 | |||
| 6d1d53e43c | |||
| b7c2adae27 | |||
| 931e9b43e6 | |||
| dc6c8b642c | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,3 +5,4 @@ | |||||||
| # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml | ||||||
| hs_err_pid* | hs_err_pid* | ||||||
|  |  | ||||||
|  | /res/songs/ | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | # Default ignored files | ||||||
|  | /shelf/ | ||||||
|  | /workspace.xml | ||||||
|  | # Editor-based HTTP Client requests | ||||||
|  | /httpRequests/ | ||||||
|  | # Datasource local storage ignored files | ||||||
|  | /dataSources/ | ||||||
|  | /dataSources.local.xml | ||||||
							
								
								
									
										7
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | <component name="ProjectCodeStyleConfiguration"> | ||||||
|  |   <code_scheme name="Project" version="173"> | ||||||
|  |     <ScalaCodeStyleSettings> | ||||||
|  |       <option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" /> | ||||||
|  |     </ScalaCodeStyleSettings> | ||||||
|  |   </code_scheme> | ||||||
|  | </component> | ||||||
							
								
								
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <component name="ProjectCodeStyleConfiguration"> | ||||||
|  |   <state> | ||||||
|  |     <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> | ||||||
|  |   </state> | ||||||
|  | </component> | ||||||
							
								
								
									
										12
									
								
								.idea/libraries/io_methvin_directory_watcher.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.idea/libraries/io_methvin_directory_watcher.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <component name="libraryTable"> | ||||||
|  |   <library name="io.methvin.directory.watcher" type="repository"> | ||||||
|  |     <properties maven-id="io.methvin:directory-watcher:0.18.0" /> | ||||||
|  |     <CLASSES> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/io/methvin/directory-watcher/0.18.0/directory-watcher-0.18.0.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar!/" /> | ||||||
|  |     </CLASSES> | ||||||
|  |     <JAVADOC /> | ||||||
|  |     <SOURCES /> | ||||||
|  |   </library> | ||||||
|  | </component> | ||||||
							
								
								
									
										16
									
								
								.idea/libraries/lihaoyi_upickle_2_13.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.idea/libraries/lihaoyi_upickle_2_13.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <component name="libraryTable"> | ||||||
|  |   <library name="lihaoyi.upickle_2.13" type="repository"> | ||||||
|  |     <properties maven-id="com.lihaoyi:upickle_2.13:3.1.0" /> | ||||||
|  |     <CLASSES> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/com/lihaoyi/upickle_2.13/3.1.0/upickle_2.13-3.1.0.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/org/scala-lang/scala-library/2.13.10/scala-library-2.13.10.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/com/lihaoyi/ujson_2.13/3.1.0/ujson_2.13-3.1.0.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/com/lihaoyi/upickle-core_2.13/3.1.0/upickle-core_2.13-3.1.0.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/com/lihaoyi/geny_2.13/1.0.0/geny_2.13-1.0.0.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/com/lihaoyi/upack_2.13/3.1.0/upack_2.13-3.1.0.jar!/" /> | ||||||
|  |       <root url="jar://$MAVEN_REPOSITORY$/com/lihaoyi/upickle-implicits_2.13/3.1.0/upickle-implicits_2.13-3.1.0.jar!/" /> | ||||||
|  |     </CLASSES> | ||||||
|  |     <JAVADOC /> | ||||||
|  |     <SOURCES /> | ||||||
|  |   </library> | ||||||
|  | </component> | ||||||
							
								
								
									
										6
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK"> | ||||||
|  |     <output url="file://$PROJECT_DIR$/out" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/Lab18.iml" filepath="$PROJECT_DIR$/Lab18.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										7
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="VcsDirectoryMappings"> | ||||||
|  |     <mapping directory="" vcs="Git" /> | ||||||
|  |     <mapping directory="$PROJECT_DIR$" vcs="Git" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										16
									
								
								Lab18.iml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Lab18.iml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <module type="JAVA_MODULE" version="4"> | ||||||
|  |   <component name="NewModuleRootManager" inherit-compiler-output="true"> | ||||||
|  |     <exclude-output /> | ||||||
|  |     <content url="file://$MODULE_DIR$"> | ||||||
|  |       <sourceFolder url="file://$MODULE_DIR$/res" type="java-resource" /> | ||||||
|  |       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/res/songs" /> | ||||||
|  |     </content> | ||||||
|  |     <orderEntry type="inheritedJdk" /> | ||||||
|  |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|  |     <orderEntry type="library" name="scala-sdk-2.13.12" level="application" /> | ||||||
|  |     <orderEntry type="library" name="io.methvin.directory.watcher" level="project" /> | ||||||
|  |     <orderEntry type="library" name="lihaoyi.upickle_2.13" level="project" /> | ||||||
|  |   </component> | ||||||
|  | </module> | ||||||
							
								
								
									
										3
									
								
								res/template/album.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								res/template/album.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | td:first-child { | ||||||
|  | 	text-align: right; | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								res/template/album.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								res/template/album.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="utf-8"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 	<title>Slopify V2 - Listen to the music you hate</title> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="base.css"> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="album.css"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 	<header id="navbar"> | ||||||
|  | 		<a class="back" id="back-btn" href="{{artist.page}}"></a> | ||||||
|  | 		<h1 class="title">Slopify</h1> | ||||||
|  | 	</header> | ||||||
|  | 	<div id="body"> | ||||||
|  | 		<h2>Album</h2> | ||||||
|  | 		<div id="crumbs"> | ||||||
|  | 			<a href="{{artist.page}}">{{artist.name}}</a> | ||||||
|  | 			<span class="sep"></span> | ||||||
|  | 			<a href="./">{{album.name}}</a> | ||||||
|  | 		</div> | ||||||
|  | 		<table id="songs"> | ||||||
|  | 			<thead> | ||||||
|  | 				<tr> | ||||||
|  | 					<th>No</th> | ||||||
|  | 					<th>Name</th> | ||||||
|  | 				</tr> | ||||||
|  | 			</thead> | ||||||
|  | 			<tbody> | ||||||
|  | 				{{songs}} | ||||||
|  | 			</tbody> | ||||||
|  | 		</table> | ||||||
|  | 	</div> | ||||||
|  | 	<script type="text/javascript" src="base.js"></script> | ||||||
|  | 	<script> | ||||||
|  | 		initTable(document.getElementById("songs")) | ||||||
|  | 	</script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										0
									
								
								res/template/artist.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								res/template/artist.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										36
									
								
								res/template/artist.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								res/template/artist.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="utf-8"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 	<title>Slopify V2 - Listen to the music you hate</title> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="base.css"> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="artist.css"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 	<header id="navbar"> | ||||||
|  | 		<a class="back" id="back-btn" href="index.html"></a> | ||||||
|  | 		<h1 class="title">Slopify</h1> | ||||||
|  | 	</header> | ||||||
|  | 	<div id="body"> | ||||||
|  | 		<h2>Artist</h2> | ||||||
|  | 		<div id="crumbs"> | ||||||
|  | 			<a href="./">{{artist.name}}</a> | ||||||
|  | 		</div> | ||||||
|  | 		<table id="albums"> | ||||||
|  | 			<thead> | ||||||
|  | 				<tr> | ||||||
|  | 					<th>Name</th> | ||||||
|  | 				</tr> | ||||||
|  | 			</thead> | ||||||
|  | 			<tbody> | ||||||
|  | 				{{albums}} | ||||||
|  | 			</tbody> | ||||||
|  | 		</table> | ||||||
|  | 	</div> | ||||||
|  | 	<script type="text/javascript" src="base.js"></script> | ||||||
|  | 	<script> | ||||||
|  | 		initTable(document.getElementById("albums")) | ||||||
|  | 	</script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										126
									
								
								res/template/base.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								res/template/base.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | |||||||
|  | * { | ||||||
|  | 	margin: 0; | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | html, body { | ||||||
|  | 	width: 100%; | ||||||
|  | 	height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body { | ||||||
|  | 	background-color: #0d130d; | ||||||
|  | 	font-family: Ubuntu; | ||||||
|  | 	font-size: 16pt; | ||||||
|  | 	color: white; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #navbar { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: auto 1fr auto; | ||||||
|  | 	grid-template-areas: "back title space"; | ||||||
|  | 	place-items: center; | ||||||
|  | 	text-align: center; | ||||||
|  | 	border-bottom: solid #3e5b3e 1px; | ||||||
|  | 	padding: 0.4em 0.8em; | ||||||
|  | 	gap: 0.8em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #navbar .back { | ||||||
|  | 	grid-area: back; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #back-btn { | ||||||
|  | 	width: 2em; | ||||||
|  | 	height: 2em; | ||||||
|  | 	position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #back-btn::before { | ||||||
|  | 	content: ""; | ||||||
|  | 	border: solid white 4px; | ||||||
|  | 	border-style: solid none none solid; | ||||||
|  | 	position: absolute; | ||||||
|  | 	inset: 0; | ||||||
|  | 	transform: rotate(-45deg); | ||||||
|  | 	width: 0.5em; | ||||||
|  | 	height: 0.5em; | ||||||
|  | 	margin: auto; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #navbar .title { | ||||||
|  | 	grid-area: title; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #body { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  | 	padding: 2em; | ||||||
|  | 	gap: 1em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #crumbs { | ||||||
|  | 	display: flex; | ||||||
|  | 	gap: 1em; | ||||||
|  | 	align-items: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #crumbs .sep { | ||||||
|  | 	position: relative; | ||||||
|  | 	width: 0.5em; | ||||||
|  | 	height: 0.5em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #crumbs .sep::before { | ||||||
|  | 	content: ""; | ||||||
|  | 	position: absolute; | ||||||
|  | 	top: 50%; | ||||||
|  | 	left: 50%; | ||||||
|  | 	width: 100%; | ||||||
|  | 	height: 100%; | ||||||
|  | 	border: white 0.2em; | ||||||
|  | 	border-style: solid solid none none; | ||||||
|  | 	transform: translate(-50%, -50%) rotate(45deg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a { | ||||||
|  | 	color: #00bf00; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | table { | ||||||
|  | 	border-collapse: collapse; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | th, td { | ||||||
|  | 	border: solid white 1px; | ||||||
|  | 	padding: 0.5em 1em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tr:nth-child(even) { | ||||||
|  | 	background-color: #aeaeae21; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | th { | ||||||
|  | 	cursor: pointer; | ||||||
|  | 	position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | th.sort-asc::before { | ||||||
|  | 	content: ""; | ||||||
|  | 	position: absolute; | ||||||
|  | 	top: 50%; | ||||||
|  | 	left: 0.5em; | ||||||
|  | 	transform: translate(-50%, -25%); | ||||||
|  | 	border: solid transparent 0.2em; | ||||||
|  | 	border-top-color: white; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | th.sort-desc::before { | ||||||
|  | 	content: ""; | ||||||
|  | 	position: absolute; | ||||||
|  | 	top: 50%; | ||||||
|  | 	left: 0.5em; | ||||||
|  | 	transform: translate(-50%, -55%); | ||||||
|  | 	border: solid transparent 0.2em; | ||||||
|  | 	border-bottom-color: white; | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								res/template/base.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								res/template/base.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | function initTable(table) { | ||||||
|  | 	table.tHead.querySelectorAll("th").forEach((header, i) => { | ||||||
|  | 		header.addEventListener("click", () => { | ||||||
|  | 			toggleSort(table, header, i) | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function toggleSort(table, header, colI) { | ||||||
|  | 	if (header.classList.contains("sort-asc")) { | ||||||
|  | 		header.classList.remove("sort-asc") | ||||||
|  | 		header.classList.add("sort-desc") | ||||||
|  | 		doSort(table, colI, true) | ||||||
|  | 	} else if (header.classList.contains("sort-desc")) { | ||||||
|  | 		header.classList.remove("sort-desc") | ||||||
|  | 		header.classList.add("sort-asc") | ||||||
|  | 		doSort(table, colI, false) | ||||||
|  | 	} else { | ||||||
|  | 		table.tHead.querySelectorAll("th.sort-asc,th.sort-desc").forEach(th => { | ||||||
|  | 			th.classList.remove("sort-asc") | ||||||
|  | 			th.classList.remove("sort-desc") | ||||||
|  | 		}) | ||||||
|  | 		header.classList.add("sort-asc") | ||||||
|  | 		doSort(table, colI, false) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function doSort(table, colI, desc = false) { | ||||||
|  | 	let swapped = true | ||||||
|  | 	while (swapped) { | ||||||
|  | 		let rows = Array.from(table.rows) | ||||||
|  | 		swapped = false | ||||||
|  | 		let rowA, rowB | ||||||
|  | 		for (let i=1; i<rows.length-1; i++) { | ||||||
|  | 			rowA = rows[i] | ||||||
|  | 			rowB = rows[i+1] | ||||||
|  | 			if (desc == compareRows(rowA, rowB, colI)) { | ||||||
|  | 				swapped = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (swapped) { | ||||||
|  | 			rowA.parentNode.insertBefore(rowB, rowA) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function compareRows(rowA, rowB, colI) { | ||||||
|  | 	let valA = rowA.querySelectorAll("td")[colI].innerText | ||||||
|  | 	let valB = rowB.querySelectorAll("td")[colI].innerText | ||||||
|  |  | ||||||
|  | 	if (!Number.isNaN(+valA) && !Number.isNaN(+valB)) { | ||||||
|  | 		valA = +valA | ||||||
|  | 		valB = +valB | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return valB > valA | ||||||
|  | } | ||||||
							
								
								
									
										0
									
								
								res/template/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								res/template/index.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								res/template/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								res/template/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="utf-8"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 	<title>Slopify V2 - Listen to the music you hate</title> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="base.css"> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="index.css"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 	<header id="navbar"> | ||||||
|  | 		<h1 class="title">Slopify</h1> | ||||||
|  | 	</header> | ||||||
|  | 	<div id="body"> | ||||||
|  | 		<h2>Artists</h2> | ||||||
|  | 		<div id="crumbs"></div> | ||||||
|  | 		<table id="artists"> | ||||||
|  | 			<thead> | ||||||
|  | 				<tr> | ||||||
|  | 					<th>Name</th> | ||||||
|  | 				</tr> | ||||||
|  | 			</thead> | ||||||
|  | 			<tbody> | ||||||
|  | 				{{artists}} | ||||||
|  | 			</tbody> | ||||||
|  | 		</table> | ||||||
|  | 	</div> | ||||||
|  | 	<script type="text/javascript" src="base.js"></script> | ||||||
|  | 	<script> | ||||||
|  | 		initTable(document.getElementById("artists")) | ||||||
|  | 	</script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										61
									
								
								res/template/song.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								res/template/song.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | @keyframes shadow-anim { | ||||||
|  | 	0% { | ||||||
|  | 		box-shadow: 0.0em 0.2em 0.6em #344c34a0; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	25% { | ||||||
|  | 		box-shadow: 0.1em 0.3em 0.8em #344c34a0; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	50% { | ||||||
|  | 		box-shadow: 0.2em 0.4em 1.2em #344c34a0; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	75% { | ||||||
|  | 		box-shadow: 0.1em 0.3em 0.8em #344c34a0; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	100% { | ||||||
|  | 		box-shadow: 0.0em 0.2em 0.6em #344c34a0; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #song { | ||||||
|  | 	display: grid; | ||||||
|  | 	animation: shadow-anim 10s infinite alternate; | ||||||
|  | 	border-radius: 0.8em; | ||||||
|  | 	padding: 0.8em; | ||||||
|  | 	grid-template-columns: auto 1fr auto; | ||||||
|  | 	grid-template-rows: auto 1fr; | ||||||
|  | 	grid-template-areas: | ||||||
|  | 		"num title duration" | ||||||
|  | 		"cover cover cover"; | ||||||
|  | 	max-height: 20em; | ||||||
|  | 	min-width: 50%; | ||||||
|  | 	max-width: 30em; | ||||||
|  | 	margin: auto; | ||||||
|  | 	gap: 0.4em; | ||||||
|  | 	align-items: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #song .number { | ||||||
|  | 	grid-area: num; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #song .title { | ||||||
|  | 	grid-area: title; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #song .duration { | ||||||
|  | 	grid-area: duration; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #song .cover { | ||||||
|  | 	grid-area: cover; | ||||||
|  | 	object-fit: contain; | ||||||
|  |  | ||||||
|  | 	background-color: #393939; | ||||||
|  | 	width: 8em; | ||||||
|  | 	height: 8em; | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								res/template/song.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								res/template/song.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="utf-8"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|  | 	<title>Slopify V2 - Listen to the music you hate</title> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="base.css"> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="song.css"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 	<header id="navbar"> | ||||||
|  | 		<a class="back" id="back-btn" href="{{album.page}}"></a> | ||||||
|  | 		<h1 class="title">Slopify</h1> | ||||||
|  | 	</header> | ||||||
|  | 	<div id="body"> | ||||||
|  | 		<h2>Song</h2> | ||||||
|  | 		<div id="crumbs"> | ||||||
|  | 			<a href="{{artist.page}}">{{artist.name}}</a> | ||||||
|  | 			<span class="sep"></span> | ||||||
|  | 			<a href="{{album.page}}">{{album.name}}</a> | ||||||
|  | 			<span class="sep"></span> | ||||||
|  | 			<a href="./">{{song.title}}</a> | ||||||
|  | 		</div> | ||||||
|  | 		<div id="song"> | ||||||
|  | 			<h4 class="number">{{song.number}}</h4> | ||||||
|  | 			<h3 class="title">{{song.title}}</h3> | ||||||
|  | 			<div class="cover"></div> | ||||||
|  | 			<!-- <img src="{{album.image}}" alt="{{album.name}} cover image"> --> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	<script type="text/javascript" src="base.js"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										21
									
								
								src/ch/hevs/isc/slopify_v2/Album.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/ch/hevs/isc/slopify_v2/Album.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | package ch.hevs.isc.slopify_v2 | ||||||
|  |  | ||||||
|  | import scala.collection.mutable.ArrayBuffer | ||||||
|  |  | ||||||
|  | class Album(val name: String) extends Serializable { | ||||||
|  |   private var _songs: ArrayBuffer[Song] = new ArrayBuffer() | ||||||
|  |   def addSong(song: Song): Unit = _songs.addOne(song) | ||||||
|  |   def containsSong(song: Song): Boolean = _songs.exists(s => s.number == song.number && s.title == song.title) | ||||||
|  |   def getSongs(): Array[Song] = _songs.toArray | ||||||
|  |  | ||||||
|  |   def getSongByTitle(title: String): Option[Song] = { | ||||||
|  |     for (song: Song <- _songs) { | ||||||
|  |       if (song.title == title) { | ||||||
|  |         return Some(song) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return None | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   override def toString: String = s"<Album '$name': ${_songs.length} song(s)>" | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								src/ch/hevs/isc/slopify_v2/Artist.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/ch/hevs/isc/slopify_v2/Artist.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | package ch.hevs.isc.slopify_v2 | ||||||
|  |  | ||||||
|  | import scala.collection.mutable.ArrayBuffer | ||||||
|  |  | ||||||
|  | class Artist(val name: String) extends Serializable { | ||||||
|  |   private var _albums: ArrayBuffer[Album] = new ArrayBuffer() | ||||||
|  |   def addAlbum(album: Album): Unit = _albums.addOne(album) | ||||||
|  |  | ||||||
|  |   def hasAlbum(album: Album): Boolean = _albums.exists(_.name == album.name) | ||||||
|  |   def getAlbums(): Array[Album] = _albums.toArray | ||||||
|  |  | ||||||
|  |   def getAlbumByName(name: String): Option[Album] = { | ||||||
|  |     for (album: Album <- _albums) { | ||||||
|  |       if (album.name == name) { | ||||||
|  |         return Some(album) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return None | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   override def toString: String = s"<Artist '$name': ${_albums.length} album(s)>" | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/ch/hevs/isc/slopify_v2/DataBase.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/ch/hevs/isc/slopify_v2/DataBase.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | package ch.hevs.isc.slopify_v2 | ||||||
|  |  | ||||||
|  | import scala.collection.mutable.ArrayBuffer | ||||||
|  |  | ||||||
|  | class DataBase extends Serializable { | ||||||
|  |   private var _artists: ArrayBuffer[Artist] = new ArrayBuffer() | ||||||
|  |   def addArtist(artist: Artist): Unit = _artists.addOne(artist) | ||||||
|  |   def containsArtist(artist: Artist): Boolean = _artists.exists(_.name == artist.name) | ||||||
|  |   def getArtists(): Array[Artist] = _artists.toArray | ||||||
|  |  | ||||||
|  |   def getArtistByName(name: String): Option[Artist] = { | ||||||
|  |     for (artist: Artist <- _artists) { | ||||||
|  |       if (artist.name == name) { | ||||||
|  |         return Some(artist) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return None | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   override def toString: String = s"<Database: ${_artists.length} artist(s)>" | ||||||
|  | } | ||||||
							
								
								
									
										166
									
								
								src/ch/hevs/isc/slopify_v2/DataBaseHelper.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/ch/hevs/isc/slopify_v2/DataBaseHelper.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | |||||||
|  | package ch.hevs.isc.slopify_v2 | ||||||
|  |  | ||||||
|  | import java.io._ | ||||||
|  | import scala.collection.immutable.HashMap | ||||||
|  | import scala.io.{BufferedSource, Source} | ||||||
|  |  | ||||||
|  | object DataBaseHelper { | ||||||
|  |   def create(directory:String) : DataBase = { | ||||||
|  |     val db = new DataBase() | ||||||
|  |     for (a <- new File(directory).listFiles() if a.isDirectory) { | ||||||
|  |       val artistName = a.getName | ||||||
|  |       //println(s"found new artist : $artistName") | ||||||
|  |       val artist: Artist = new Artist(artistName) | ||||||
|  |  | ||||||
|  |       for (b <- a.listFiles() if b.isDirectory ) { | ||||||
|  |         val albumName = b.getName | ||||||
|  |         //println(s"found new album $albumName for artist : $artistName") | ||||||
|  |         val album: Album = new Album(albumName) | ||||||
|  |  | ||||||
|  |         for (c <- b.listFiles() if c.isFile if c.getName.toLowerCase().endsWith(".mp3") ) { | ||||||
|  |           val fileName = c.getName.substring(0, c.getName.length - 4) | ||||||
|  |           val format1 = """(\d*) (.*)""".r | ||||||
|  |           val format2 = """(\d*)-(\d*) (.*)""".r | ||||||
|  |           val format3 = """(\d*)-(.*)""".r | ||||||
|  |           var songName: String = "" | ||||||
|  |           var songNumber: Int = 0 | ||||||
|  |  | ||||||
|  |           fileName match { | ||||||
|  |             case format1(nr, name) => { | ||||||
|  |               //println(s"found song nr #$nr name:'$name' in album '$albumName' for artist : '${a.getName}'") | ||||||
|  |               songName = name | ||||||
|  |               songNumber = Integer.parseInt(nr) | ||||||
|  |             } | ||||||
|  |             case format2(cd, nr, name) => { | ||||||
|  |               //println(s"found song nr #$nr on cd#$cd name:'$name' in album '$albumName' for artist : '${a.getName}'") | ||||||
|  |               songName = name | ||||||
|  |               songNumber = Integer.parseInt(nr) | ||||||
|  |             } | ||||||
|  |             case format3(nr, name) => { | ||||||
|  |               //println(s"found song nr #$nr name:'$name' in album '$albumName' for artist : '$artistName'") | ||||||
|  |               songName = name | ||||||
|  |               songNumber = Integer.parseInt(nr) | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           val song: Song = new Song(songName, songNumber) | ||||||
|  |           album.addSong(song) | ||||||
|  |         } | ||||||
|  |         artist.addAlbum(album) | ||||||
|  |       } | ||||||
|  |       db.addArtist(artist) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return db | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def save(fileName: String, db: DataBase) : Unit = { | ||||||
|  |     val fos: FileOutputStream = new FileOutputStream(fileName) | ||||||
|  |     val oos: ObjectOutputStream = new ObjectOutputStream(fos) | ||||||
|  |     oos.writeObject(db) | ||||||
|  |     oos.close() | ||||||
|  |   } | ||||||
|  |   def load(fileName:String) : DataBase = { | ||||||
|  |     val fis: FileInputStream = new FileInputStream(fileName) | ||||||
|  |     val ois: ObjectInputStream = new ObjectInputStream(fis) | ||||||
|  |     val db: DataBase = ois.readObject().asInstanceOf[DataBase] | ||||||
|  |     ois.close() | ||||||
|  |     return db | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def exportHTML(directory: String, dataBase: DataBase): Unit = { | ||||||
|  |     var artists: Array[Artist] = dataBase.getArtists().sortBy(_.name) | ||||||
|  |  | ||||||
|  |     var artistRows: String = "" | ||||||
|  |     for (artist: Artist <- artists) { | ||||||
|  |       val artistFileName: String = formatName(artist.name) | ||||||
|  |       val artistUrl: String = s"./$artistFileName.html" | ||||||
|  |       artistRows += s"<tr><td><a href='$artistUrl'>${artist.name}</a></td></tr>\n" | ||||||
|  |  | ||||||
|  |       val albums: Array[Album] = artist.getAlbums().sortBy(_.name) | ||||||
|  |       var albumRows: String = "" | ||||||
|  |       for (album: Album <- albums) { | ||||||
|  |         val albumFileName: String = artistFileName + "_" + formatName(album.name) | ||||||
|  |         val albumUrl: String = s"./$albumFileName.html" | ||||||
|  |         albumRows += s"<tr><td><a href='$albumUrl'>${album.name}</a></td></tr>\n" | ||||||
|  |  | ||||||
|  |         val songs: Array[Song] = album.getSongs().sortBy(_.number) | ||||||
|  |         var songRows: String = "" | ||||||
|  |         for (song: Song <- songs) { | ||||||
|  |           val songFileName: String = albumFileName + "_" + formatName(song.number + "-" + song.title) | ||||||
|  |           val songUrl: String = s"./$songFileName.html" | ||||||
|  |           songRows += s"<tr><td>${song.number}</td><td><a href='$songUrl'>${song.title}</a></td></tr>\n" | ||||||
|  |  | ||||||
|  |           formatTemplate("res/template/song.html", HashMap( | ||||||
|  |             "artist.name" -> artist.name, | ||||||
|  |             "artist.page" -> artistUrl, | ||||||
|  |             "album.name" -> album.name, | ||||||
|  |             "album.page" -> albumUrl, | ||||||
|  |             "song.title" -> song.title, | ||||||
|  |             "song.number" -> song.number.toString | ||||||
|  |           ), directory + "/" + songFileName + ".html") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         formatTemplate("res/template/album.html", HashMap( | ||||||
|  |           "songs" -> songRows, | ||||||
|  |           "artist.name" -> artist.name, | ||||||
|  |           "artist.page" -> artistUrl, | ||||||
|  |           "album.name" -> album.name | ||||||
|  |         ), directory + "/" + albumFileName + ".html") | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       formatTemplate("res/template/artist.html", HashMap( | ||||||
|  |         "albums" -> albumRows, | ||||||
|  |         "artist.name" -> artist.name | ||||||
|  |       ), directory + "/" + artistFileName + ".html") | ||||||
|  |     } | ||||||
|  |     formatTemplate("res/template/index.html", HashMap( | ||||||
|  |       "artists" -> artistRows | ||||||
|  |     ), directory + "/" + "index.html") | ||||||
|  |  | ||||||
|  |     copyFile("res/template/base.js", directory + "/base.js") | ||||||
|  |     copyFile("res/template/base.css", directory + "/base.css") | ||||||
|  |     copyFile("res/template/index.css", directory + "/index.css") | ||||||
|  |     copyFile("res/template/artist.css", directory + "/artist.css") | ||||||
|  |     copyFile("res/template/album.css", directory + "/album.css") | ||||||
|  |     copyFile("res/template/song.css", directory + "/song.css") | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def copyFile(from: String, to: String): Unit = { | ||||||
|  |     val fis: FileInputStream = new FileInputStream(from) | ||||||
|  |     val fos: FileOutputStream = new FileOutputStream(to) | ||||||
|  |  | ||||||
|  |     fos.write(fis.readAllBytes()) | ||||||
|  |  | ||||||
|  |     fis.close() | ||||||
|  |     fos.close() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def formatName(name: String): String = { | ||||||
|  |     var formatted: String = name.toLowerCase | ||||||
|  |     formatted = formatted.replace(" ", "_") | ||||||
|  |     formatted = formatted.replace("'", "-") | ||||||
|  |     formatted = formatted.replaceAll("[^a-zA-Z_0-9\\-().]", "") | ||||||
|  |     return formatted | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def formatTemplate(templateName: String, variables: Map[String, String], outPath: String): Unit = { | ||||||
|  |     val source: BufferedSource = Source.fromFile(templateName) | ||||||
|  |     var template: String = source.getLines().mkString("\n") | ||||||
|  |     source.close() | ||||||
|  |  | ||||||
|  |     for ((key: String, value: String) <- variables) { | ||||||
|  |       template = template.replace(s"{{$key}}", value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val fos: FileOutputStream = new FileOutputStream(outPath) | ||||||
|  |     fos.write(template.getBytes) | ||||||
|  |     fos.close() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def main(args: Array[String]): Unit = { | ||||||
|  |     val db: DataBase = create("res/songs") | ||||||
|  |  | ||||||
|  |     exportHTML("/tmp/html", db) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										236
									
								
								src/ch/hevs/isc/slopify_v2/GUI.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								src/ch/hevs/isc/slopify_v2/GUI.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | |||||||
|  | package ch.hevs.isc.slopify_v2 | ||||||
|  |  | ||||||
|  | import io.methvin.watcher.{DirectoryChangeEvent, DirectoryChangeListener, DirectoryWatcher} | ||||||
|  |  | ||||||
|  | import java.awt.event.{KeyEvent, KeyListener} | ||||||
|  | import java.awt.{GridBagConstraints, GridBagLayout, LayoutManager} | ||||||
|  | import java.nio.file.Paths | ||||||
|  | import java.util | ||||||
|  | import javax.swing._ | ||||||
|  | import javax.swing.table.{DefaultTableModel, TableModel, TableRowSorter} | ||||||
|  |  | ||||||
|  | class GUI extends JFrame { | ||||||
|  |   val DIRECTORY: String = "res/songs/" | ||||||
|  |   val DB_PATH: String = "res/songs_db.bin" | ||||||
|  |  | ||||||
|  |   setSize(800, 600) | ||||||
|  |   setTitle("Slopify V2") | ||||||
|  |  | ||||||
|  |   // Artists | ||||||
|  |   val artistsListModel: DefaultListModel[String] = new DefaultListModel[String]() | ||||||
|  |   val artistsList: JList[String] = new JList(artistsListModel) | ||||||
|  |   artistsList.setLayoutOrientation(JList.VERTICAL) | ||||||
|  |   val col1: JPanel = new JPanel() | ||||||
|  |   val col1Layout: GridBagLayout = new GridBagLayout() | ||||||
|  |   col1.setLayout(col1Layout) | ||||||
|  |  | ||||||
|  |   val artistsPane: JScrollPane = new JScrollPane(artistsList) | ||||||
|  |   artistsList.addListSelectionListener(_ => { | ||||||
|  |     val artistName: String = artistsList.getSelectedValue | ||||||
|  |     val artist: Option[Artist] = db.getArtistByName(artistName) | ||||||
|  |     if (artist.isDefined) { | ||||||
|  |       selectArtist(artist.get) | ||||||
|  |     } else { | ||||||
|  |       clearAlbums() | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   val artistsSearchBar: JTextField = new JTextField() | ||||||
|  |   artistsSearchBar.addKeyListener(new KeyListener { | ||||||
|  |     override def keyTyped(keyEvent: KeyEvent): Unit = {} | ||||||
|  |     override def keyPressed(keyEvent: KeyEvent): Unit = {} | ||||||
|  |     override def keyReleased(keyEvent: KeyEvent): Unit = updateArtistsSearch() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   val col1Constraints: GridBagConstraints = new GridBagConstraints() | ||||||
|  |   col1Constraints.fill = GridBagConstraints.BOTH | ||||||
|  |   col1Constraints.gridx = 0 | ||||||
|  |   col1Constraints.gridy = 0 | ||||||
|  |   col1Constraints.weightx = 1 | ||||||
|  |   col1Constraints.weighty = 1 | ||||||
|  |   col1.add(artistsPane, col1Constraints) | ||||||
|  |  | ||||||
|  |   col1Constraints.fill = GridBagConstraints.HORIZONTAL | ||||||
|  |   col1Constraints.gridx = 0 | ||||||
|  |   col1Constraints.gridy = 1 | ||||||
|  |   col1Constraints.weighty = 0 | ||||||
|  |   col1.add(artistsSearchBar, col1Constraints) | ||||||
|  |  | ||||||
|  |   // Albums | ||||||
|  |   val albumsListModel: DefaultListModel[String] = new DefaultListModel[String]() | ||||||
|  |   val albumsList: JList[String] = new JList(albumsListModel) | ||||||
|  |   val col2: JScrollPane = new JScrollPane(albumsList) | ||||||
|  |   albumsList.addListSelectionListener(_ => { | ||||||
|  |     if (curArtist.isDefined) { | ||||||
|  |       val albumName: String = albumsList.getSelectedValue | ||||||
|  |       val album: Option[Album] = curArtist.get.getAlbumByName(albumName) | ||||||
|  |       if (album.isDefined) { | ||||||
|  |         selectAlbum(album.get) | ||||||
|  |       } else { | ||||||
|  |         clearSongs() | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // Songs | ||||||
|  |   val songsTableHeaders: Array[Object] = Array("Track no", "Title") | ||||||
|  |   val songsTable: JTable = new JTable(Array.empty[Array[Object]], songsTableHeaders) | ||||||
|  |   val songsTableModel: DefaultTableModel = new DefaultTableModel() | ||||||
|  |   songsTableModel.addColumn("Track no") | ||||||
|  |   songsTableModel.addColumn("Title") | ||||||
|  |   songsTable.setModel(songsTableModel) | ||||||
|  |   songsTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN) | ||||||
|  |  | ||||||
|  |   val tableSorter: TableRowSorter[TableModel] = new TableRowSorter(songsTable.getModel) | ||||||
|  |   val songsSortKeys: util.List[RowSorter.SortKey] = new util.ArrayList[RowSorter.SortKey]() | ||||||
|  |   songsSortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING)) | ||||||
|  |   tableSorter.setSortKeys(songsSortKeys) | ||||||
|  |   songsTable.setRowSorter(tableSorter) | ||||||
|  |  | ||||||
|  |   val col3: JScrollPane = new JScrollPane(songsTable) | ||||||
|  |   val songsSelectionModel: ListSelectionModel = songsTable.getSelectionModel | ||||||
|  |   songsSelectionModel.addListSelectionListener(e => { | ||||||
|  |     val row: Int = e.getFirstIndex | ||||||
|  |     try { | ||||||
|  |       val songName: String = songsTableModel.getValueAt(row, 0).asInstanceOf[String] | ||||||
|  |       val song: Option[Song] = curAlbum.get.getSongByTitle(songName) | ||||||
|  |       if (song.isDefined) { | ||||||
|  |         selectSong(song.get) | ||||||
|  |       } | ||||||
|  |     } catch { | ||||||
|  |       case e: ArrayIndexOutOfBoundsException => {} | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   val split1: JSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, col1, col2) | ||||||
|  |   val split2: JSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, split1, col3) | ||||||
|  |   split1.setResizeWeight(0.5) | ||||||
|  |   split2.setResizeWeight(0.66) | ||||||
|  |  | ||||||
|  |   val myMenuBar: JMenuBar = new JMenuBar() | ||||||
|  |   val fileMenu: JMenu = new JMenu("File") | ||||||
|  |   val refreshDbBtn: JMenuItem = new JMenuItem("Refresh Database") | ||||||
|  |   refreshDbBtn.addActionListener(_ => refreshDatabase()) | ||||||
|  |  | ||||||
|  |   fileMenu.add(refreshDbBtn) | ||||||
|  |   myMenuBar.add(fileMenu) | ||||||
|  |   setJMenuBar(myMenuBar) | ||||||
|  |   getContentPane.add(split2) | ||||||
|  |   setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) | ||||||
|  |  | ||||||
|  |   var db: DataBase = _ | ||||||
|  |   var curArtist: Option[Artist] = None | ||||||
|  |   var curAlbum: Option[Album] = None | ||||||
|  |   var curSong: Option[Song] = None | ||||||
|  |  | ||||||
|  |   DirectoryWatcher.builder() | ||||||
|  |     .path(Paths.get(DIRECTORY)) | ||||||
|  |     .listener(e => onDirectoryChange(e)) | ||||||
|  |     .build().watchAsync() | ||||||
|  |  | ||||||
|  |   refreshDatabase() | ||||||
|  |   setVisible(true) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   def clearArtists(): Unit = { | ||||||
|  |     artistsListModel.clear() | ||||||
|  |     curArtist = None | ||||||
|  |   } | ||||||
|  |   def clearAlbums(): Unit = { | ||||||
|  |     albumsListModel.clear() | ||||||
|  |     curAlbum = None | ||||||
|  |   } | ||||||
|  |   def clearSongs(): Unit = { | ||||||
|  |     songsTableModel.setRowCount(0) | ||||||
|  |     curSong = None | ||||||
|  |   } | ||||||
|  |   def addArtist(artist: Artist): Unit = { | ||||||
|  |     //println(s"Adding $artist") | ||||||
|  |     artistsListModel.addElement(artist.name) | ||||||
|  |   } | ||||||
|  |   def addAlbum(album: Album): Unit = { | ||||||
|  |     //println(s"Adding $album") | ||||||
|  |     albumsListModel.addElement(album.name) | ||||||
|  |   } | ||||||
|  |   def addSong(song: Song): Unit = { | ||||||
|  |     //println(s"Adding $song") | ||||||
|  |     val row: Array[String] = Array( | ||||||
|  |       song.number.toString.reverse.padTo(2, '0').reverse, | ||||||
|  |       song.title | ||||||
|  |     ) | ||||||
|  |     songsTableModel.addRow(row.asInstanceOf[Array[AnyRef]]) | ||||||
|  |   } | ||||||
|  |   def refreshDatabase(): Unit = { | ||||||
|  |     db = DataBaseHelper.create(DIRECTORY) | ||||||
|  |     val oldArtist: Option[Artist] = curArtist | ||||||
|  |     val oldAlbum: Option[Album] = curAlbum | ||||||
|  |     val oldSong: Option[Song] = curSong | ||||||
|  |  | ||||||
|  |     clearArtists() | ||||||
|  |     clearAlbums() | ||||||
|  |     clearSongs() | ||||||
|  |     val artists: Array[Artist] = db.getArtists().sortBy(_.name) | ||||||
|  |     for (artist: Artist <- artists) { | ||||||
|  |       addArtist(artist) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (oldArtist.isDefined && db.containsArtist(oldArtist.get)) { | ||||||
|  |       artistsList.setSelectedIndex(artistsListModel.indexOf(oldArtist.get.name)) | ||||||
|  |  | ||||||
|  |       if (oldAlbum.isDefined && curArtist.get.hasAlbum(oldAlbum.get)) { | ||||||
|  |         albumsList.setSelectedIndex(albumsListModel.indexOf(oldAlbum.get.name)) | ||||||
|  |  | ||||||
|  |         if (oldSong.isDefined && curAlbum.get.containsSong(oldSong.get)) { | ||||||
|  |           selectSong(oldSong.get) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     DataBaseHelper.save(DB_PATH, db) | ||||||
|  |   } | ||||||
|  |   def selectArtist(artist: Artist): Unit = { | ||||||
|  |     curArtist = Some(artist) | ||||||
|  |     clearAlbums() | ||||||
|  |     val albums: Array[Album] = artist.getAlbums().sortBy(_.name) | ||||||
|  |     for (album: Album <- albums) { | ||||||
|  |       addAlbum(album) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   def selectAlbum(album: Album): Unit = { | ||||||
|  |     curAlbum = Some(album) | ||||||
|  |     clearSongs() | ||||||
|  |     for (song: Song <- album.getSongs()) { | ||||||
|  |       addSong(song) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   def selectSong(song: Song): Unit = { | ||||||
|  |     curSong = Some(song) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def updateArtistsSearch(): Unit = { | ||||||
|  |     val search: String = artistsSearchBar.getText.toLowerCase | ||||||
|  |  | ||||||
|  |     clearArtists() | ||||||
|  |     clearAlbums() | ||||||
|  |     clearSongs() | ||||||
|  |     var artists: Array[Artist] = db.getArtists().sortBy(_.name) | ||||||
|  |     if (search.nonEmpty) { | ||||||
|  |       artists = artists.filter(_.name.toLowerCase.contains(search)) | ||||||
|  |     } | ||||||
|  |     for (artist: Artist <- artists) { | ||||||
|  |       addArtist(artist) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   def onDirectoryChange(event: DirectoryChangeEvent): Unit = { | ||||||
|  |     println("Changed " + event) | ||||||
|  |     SwingUtilities.invokeLater(() => { | ||||||
|  |       refreshDatabase() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | object GUI { | ||||||
|  |   def main(args: Array[String]): Unit = { | ||||||
|  |     val gui: GUI = new GUI() | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								src/ch/hevs/isc/slopify_v2/Song.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/ch/hevs/isc/slopify_v2/Song.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | package ch.hevs.isc.slopify_v2 | ||||||
|  |  | ||||||
|  | class Song(val title: String, val number: Int) extends Serializable { | ||||||
|  |   override def toString: String = s"<Song n° $number '$title'>" | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user