initial
This commit is contained in:
commit
bbf28141e2
61 changed files with 2576 additions and 0 deletions
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
.kotlin
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="livingroom.db" uuid="6bcc7058-ec04-42cf-8f9a-af9d9f6c4e2d">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:livingroom.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourcePerFileMappings">
|
||||||
|
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/6bcc7058-ec04-42cf-8f9a-af9d9f6c4e2d/console.sql" value="6bcc7058-ec04-42cf-8f9a-af9d9f6c4e2d" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
16
.idea/gradle.xml
generated
Normal file
16
.idea/gradle.xml
generated
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21 (2)" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java/org/adrianvictor/livingroom/data/Database.java" dialect="GenericSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
30
build.gradle.kts
Normal file
30
build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
plugins {
|
||||||
|
java
|
||||||
|
application
|
||||||
|
id("com.gradleup.shadow") version "9.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "org.adrianvictor.livingroom"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass = "org.adrianvictor.livingroom.Main"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.51.2.0")
|
||||||
|
implementation("com.googlecode.json-simple:json-simple:1.1.1")
|
||||||
|
implementation("org.freemarker:freemarker:2.3.32")
|
||||||
|
|
||||||
|
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
1
config.json
Normal file
1
config.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"library":"library.db","port":8080,"users":{"admin":{"password":"admin","role":"admin"}}}
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#Mon Feb 09 18:15:20 BRT 2026
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
234
gradlew
vendored
Executable file
234
gradlew
vendored
Executable file
|
|
@ -0,0 +1,234 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
BIN
library.db
Normal file
BIN
library.db
Normal file
Binary file not shown.
BIN
livingroom.db
Normal file
BIN
livingroom.db
Normal file
Binary file not shown.
1
settings.gradle.kts
Normal file
1
settings.gradle.kts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = "LivingRoom"
|
||||||
25
src/main/java/org/adrianvictor/livingroom/Logger.java
Normal file
25
src/main/java/org/adrianvictor/livingroom/Logger.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.adrianvictor.livingroom;
|
||||||
|
|
||||||
|
public class Logger {
|
||||||
|
public static final String ANSI_RESET = "\u001B[0m";
|
||||||
|
public static final String ANSI_BLACK = "\u001B[30m";
|
||||||
|
public static final String ANSI_RED = "\u001B[31m";
|
||||||
|
public static final String ANSI_GREEN = "\u001B[32m";
|
||||||
|
public static final String ANSI_YELLOW = "\u001B[33m";
|
||||||
|
public static final String ANSI_BLUE = "\u001B[34m";
|
||||||
|
public static final String ANSI_PURPLE = "\u001B[35m";
|
||||||
|
public static final String ANSI_CYAN = "\u001B[36m";
|
||||||
|
public static final String ANSI_WHITE = "\u001B[37m";
|
||||||
|
|
||||||
|
public static void info(String t) {
|
||||||
|
System.out.println(ANSI_BLUE + "[Info] " + ANSI_RESET + t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void warning(String t) {
|
||||||
|
System.out.println(ANSI_YELLOW + "[Warning] " + ANSI_RESET + t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void error(String t) {
|
||||||
|
System.out.println(ANSI_RED + "[Error] " + ANSI_RESET + t);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main/java/org/adrianvictor/livingroom/Main.java
Normal file
49
src/main/java/org/adrianvictor/livingroom/Main.java
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.adrianvictor.livingroom;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.config.AppConfig;
|
||||||
|
import org.adrianvictor.livingroom.config.ConfigLoader;
|
||||||
|
import org.adrianvictor.livingroom.http.Handler;
|
||||||
|
import org.adrianvictor.livingroom.http.Handlers;
|
||||||
|
import org.adrianvictor.livingroom.http.Server;
|
||||||
|
import org.adrianvictor.livingroom.data.Indexer;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
private static String directoryPath;
|
||||||
|
private static final Indexer indexer = Indexer.getInstance();
|
||||||
|
private static AppConfig config;
|
||||||
|
private static String configPath;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
if ("--config".equals(args[i]) && i + 1 < args.length) {
|
||||||
|
configPath = args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = ConfigLoader.load("config.json");
|
||||||
|
config.hashPlaintextPasswords();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error("Error loading config: " + e.getMessage());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryPath = config.getLibrary();
|
||||||
|
Server server = Server.getInstance();
|
||||||
|
indexer.scanAsync(new File(directoryPath));
|
||||||
|
indexer.collectGarbage();
|
||||||
|
|
||||||
|
for (Map.Entry<Handler, String> e : Handlers.getAll().entrySet()) {
|
||||||
|
server.registerHandler(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppConfig getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/java/org/adrianvictor/livingroom/auth/Role.java
Normal file
6
src/main/java/org/adrianvictor/livingroom/auth/Role.java
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.adrianvictor.livingroom.auth;
|
||||||
|
|
||||||
|
public enum Role {
|
||||||
|
USER,
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
34
src/main/java/org/adrianvictor/livingroom/auth/Session.java
Normal file
34
src/main/java/org/adrianvictor/livingroom/auth/Session.java
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package org.adrianvictor.livingroom.auth;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Session {
|
||||||
|
private final String sessionId;
|
||||||
|
private final String username;
|
||||||
|
private final long createdAt;
|
||||||
|
private long lastAccessedAt;
|
||||||
|
private static final long SESSION_TIMEOUT = 86400000; // 24 hours in milliseconds
|
||||||
|
|
||||||
|
public Session(String username) {
|
||||||
|
this.sessionId = UUID.randomUUID().toString();
|
||||||
|
this.username = username;
|
||||||
|
this.createdAt = System.currentTimeMillis();
|
||||||
|
this.lastAccessedAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLastAccessed() {
|
||||||
|
this.lastAccessedAt = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return System.currentTimeMillis() - lastAccessedAt > SESSION_TIMEOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.adrianvictor.livingroom.auth;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.services.UserService;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SessionManager {
|
||||||
|
private static final SessionManager instance = new SessionManager();
|
||||||
|
private final Map<String, Session> sessions = new HashMap<>();
|
||||||
|
|
||||||
|
private SessionManager() {}
|
||||||
|
|
||||||
|
public static SessionManager getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session createSession(String username) {
|
||||||
|
Session session = new Session(username);
|
||||||
|
sessions.put(session.getSessionId(), session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session getSession(String sessionId) {
|
||||||
|
Session session = sessions.get(sessionId);
|
||||||
|
if (session != null && !session.isExpired()) {
|
||||||
|
session.updateLastAccessed();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
if (session != null) {
|
||||||
|
sessions.remove(sessionId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void destroySession(String sessionId) {
|
||||||
|
sessions.remove(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/org/adrianvictor/livingroom/auth/User.java
Normal file
9
src/main/java/org/adrianvictor/livingroom/auth/User.java
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.adrianvictor.livingroom.auth;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.utils.PasswordUtils;
|
||||||
|
|
||||||
|
public record User(String username, String hashedPassword, String salt, Role role) {
|
||||||
|
public boolean auth(String password) {
|
||||||
|
return PasswordUtils.verifyPassword(password, salt, hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package org.adrianvictor.livingroom.config;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.utils.PasswordUtils;
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class AppConfig {
|
||||||
|
private String library;
|
||||||
|
private int port;
|
||||||
|
private Map<String, UserConfig> users;
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
public AppConfig(String path) {
|
||||||
|
this.path = path;
|
||||||
|
this.users = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLibrary() { return library; }
|
||||||
|
public void setLibrary(String library) { this.library = library; }
|
||||||
|
|
||||||
|
public int getPort() { return port; }
|
||||||
|
public void setPort(int port) { this.port = port; }
|
||||||
|
|
||||||
|
public Map<String, UserConfig> getUsers() { return users; }
|
||||||
|
public void setUsers(Map<String, UserConfig> users) {
|
||||||
|
this.users = (users != null) ? users : new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hashPlaintextPasswords() {
|
||||||
|
if (users == null) return; // safety check
|
||||||
|
for (Map.Entry<String, UserConfig> entry : users.entrySet()) {
|
||||||
|
Logger.info("Hashing user password for %s.".formatted(entry.getKey()));
|
||||||
|
UserConfig user = entry.getValue();
|
||||||
|
if (user.getHash() == null && user.getPassword() != null) {
|
||||||
|
String salt = PasswordUtils.generateSalt();
|
||||||
|
String hash = PasswordUtils.hashPassword(user.getPassword(), salt);
|
||||||
|
|
||||||
|
user.setSalt(salt);
|
||||||
|
user.setHash(hash);
|
||||||
|
user.setPassword(null); // clear plaintext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void saveConfig() throws IOException {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
json.put("library", getLibrary());
|
||||||
|
json.put("port", getPort());
|
||||||
|
|
||||||
|
JSONObject usersJson = new JSONObject();
|
||||||
|
if (users != null) {
|
||||||
|
for (Map.Entry<String, UserConfig> entry : users.entrySet()) {
|
||||||
|
String username = entry.getKey();
|
||||||
|
UserConfig user = entry.getValue();
|
||||||
|
|
||||||
|
JSONObject userJson = new JSONObject();
|
||||||
|
userJson.put("role", user.getRole());
|
||||||
|
userJson.put("salt", user.getSalt());
|
||||||
|
userJson.put("hash", user.getHash());
|
||||||
|
usersJson.put(username, userJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.put("users", usersJson);
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(path)) {
|
||||||
|
writer.write(json.toJSONString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UserConfig {
|
||||||
|
private String role;
|
||||||
|
private String password;
|
||||||
|
private String salt;
|
||||||
|
private String hash;
|
||||||
|
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public void setRole(String role) { this.role = role; }
|
||||||
|
|
||||||
|
public String getSalt() { return salt; }
|
||||||
|
public void setSalt(String salt) { this.salt = salt; }
|
||||||
|
|
||||||
|
public String getHash() { return hash; }
|
||||||
|
public void setHash(String hash) { this.hash = hash; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package org.adrianvictor.livingroom.config;
|
||||||
|
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ConfigLoader {
|
||||||
|
|
||||||
|
private static AppConfig config;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static AppConfig load(String path) throws Exception {
|
||||||
|
if (config != null) return config; // singleton
|
||||||
|
|
||||||
|
File file = new File(path);
|
||||||
|
AppConfig appConfig;
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
appConfig = createDefaultConfig(path);
|
||||||
|
|
||||||
|
saveConfig(appConfig, path);
|
||||||
|
} else {
|
||||||
|
JSONParser parser = new JSONParser();
|
||||||
|
JSONObject obj = (JSONObject) parser.parse(new FileReader(path));
|
||||||
|
|
||||||
|
appConfig = new AppConfig(path);
|
||||||
|
appConfig.setLibrary((String) obj.get("library"));
|
||||||
|
|
||||||
|
// port may be a long in org.json.simple
|
||||||
|
Object portObj = obj.get("port");
|
||||||
|
if (portObj instanceof Long) {
|
||||||
|
appConfig.setPort(((Long) portObj).intValue());
|
||||||
|
} else if (portObj instanceof String) {
|
||||||
|
appConfig.setPort(Integer.parseInt((String) portObj));
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Invalid port value in config");
|
||||||
|
}
|
||||||
|
|
||||||
|
// users
|
||||||
|
JSONObject usersObj = (JSONObject) obj.get("users");
|
||||||
|
Map<String, AppConfig.UserConfig> users = new HashMap<>();
|
||||||
|
for (Object key : usersObj.keySet()) {
|
||||||
|
String username = (String) key;
|
||||||
|
JSONObject userJson = (JSONObject) usersObj.get(username);
|
||||||
|
|
||||||
|
AppConfig.UserConfig user = new AppConfig.UserConfig();
|
||||||
|
user.setRole((String) userJson.get("role"));
|
||||||
|
user.setPassword((String) userJson.get("password"));
|
||||||
|
|
||||||
|
users.put(username, user);
|
||||||
|
}
|
||||||
|
appConfig.setUsers(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(appConfig);
|
||||||
|
|
||||||
|
config = appConfig;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppConfig createDefaultConfig(String path) {
|
||||||
|
AppConfig defaultConfig = new AppConfig(path);
|
||||||
|
defaultConfig.setLibrary("/srv/games");
|
||||||
|
defaultConfig.setPort(8080);
|
||||||
|
|
||||||
|
Map<String, AppConfig.UserConfig> users = new HashMap<>();
|
||||||
|
AppConfig.UserConfig admin = new AppConfig.UserConfig();
|
||||||
|
admin.setRole("admin");
|
||||||
|
admin.setPassword("admin");
|
||||||
|
users.put("admin", admin);
|
||||||
|
|
||||||
|
defaultConfig.setUsers(users);
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void saveConfig(AppConfig appConfig, String path) throws Exception {
|
||||||
|
JSONObject obj = new JSONObject();
|
||||||
|
obj.put("library", appConfig.getLibrary());
|
||||||
|
obj.put("port", appConfig.getPort());
|
||||||
|
|
||||||
|
JSONObject usersObj = new JSONObject();
|
||||||
|
for (Map.Entry<String, AppConfig.UserConfig> entry : appConfig.getUsers().entrySet()) {
|
||||||
|
JSONObject userJson = new JSONObject();
|
||||||
|
userJson.put("role", entry.getValue().getRole());
|
||||||
|
userJson.put("password", entry.getValue().getPassword());
|
||||||
|
usersObj.put(entry.getKey(), userJson);
|
||||||
|
}
|
||||||
|
obj.put("users", usersObj);
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(path)) {
|
||||||
|
writer.write(obj.toJSONString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validate(AppConfig appConfig) {
|
||||||
|
if (appConfig.getLibrary() == null || appConfig.getLibrary().isEmpty())
|
||||||
|
throw new IllegalStateException("Library path must be set");
|
||||||
|
if (appConfig.getPort() <= 0)
|
||||||
|
throw new IllegalStateException("Port must be > 0");
|
||||||
|
if (appConfig.getUsers() == null || appConfig.getUsers().isEmpty())
|
||||||
|
throw new IllegalStateException("At least one user must be defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppConfig get() {
|
||||||
|
if (config == null) throw new IllegalStateException("Config not loaded yet");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/main/java/org/adrianvictor/livingroom/data/Database.java
Normal file
195
src/main/java/org/adrianvictor/livingroom/data/Database.java
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
package org.adrianvictor.livingroom.data;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.config.AppConfig;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
import org.json.simple.parser.ParseException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Database {
|
||||||
|
static String url;
|
||||||
|
static Database instance;
|
||||||
|
|
||||||
|
private Database() {
|
||||||
|
String library = Main.getConfig().getLibrary();
|
||||||
|
File dbFile = new File(library, "livingroom.db");
|
||||||
|
url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
|
||||||
|
Logger.info("Loading DB at %s.".formatted(url));
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Database getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new Database();
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setup() {
|
||||||
|
var sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
location TEXT NOT NULL,
|
||||||
|
properties TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
||||||
|
stmt.execute(sql);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void add(Item item) {
|
||||||
|
String sql = "INSERT INTO games(name, location, properties) VALUES(?,?,?)";
|
||||||
|
|
||||||
|
try (var conn = DriverManager.getConnection(url);
|
||||||
|
var pstmt = conn.prepareStatement(sql)) {
|
||||||
|
pstmt.setString(1, item.getPropertiesString().get("name"));
|
||||||
|
pstmt.setString(2, item.location().toString());
|
||||||
|
pstmt.setString(3, item.getPropertiesJSON().toJSONString());
|
||||||
|
pstmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(String gameID) {
|
||||||
|
String sql = "DELETE FROM games WHERE id=?";
|
||||||
|
|
||||||
|
try (var conn = DriverManager.getConnection(url);
|
||||||
|
var pstmt = conn.prepareStatement(sql)) {
|
||||||
|
pstmt.setString(1, gameID);
|
||||||
|
pstmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void nuke() {
|
||||||
|
String sql = "DROP TABLE IF EXISTS games;";
|
||||||
|
|
||||||
|
try (var conn = DriverManager.getConnection(url);
|
||||||
|
var stmt = conn.createStatement()) {
|
||||||
|
stmt.execute(sql);
|
||||||
|
setup();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean has(String name) {
|
||||||
|
String sql = "SELECT id FROM games WHERE name=?";
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(url);
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql);) {
|
||||||
|
stmt.setString(1, name);
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Item getGame(int id) {
|
||||||
|
String sql = "SELECT * FROM games WHERE id=?";
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(url);
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql);) {
|
||||||
|
stmt.setString(1, String.valueOf(id));
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
List<Item> processed = processResultSet(rs);
|
||||||
|
if (processed.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed.getFirst();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Item> getAllGames() {
|
||||||
|
String sql = "SELECT * FROM games";
|
||||||
|
JSONParser parser = new JSONParser();
|
||||||
|
List<Item> result = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(url);
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||||
|
ResultSet rs = stmt.executeQuery()) {
|
||||||
|
result.addAll(processResultSet(rs));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Item> processResultSet(ResultSet rs) throws SQLException {
|
||||||
|
List<Item> result = new ArrayList<>();
|
||||||
|
JSONParser parser = new JSONParser();
|
||||||
|
while (rs.next()) {
|
||||||
|
long id = rs.getLong("id");
|
||||||
|
String name = rs.getString("name");
|
||||||
|
String location = rs.getString("location");
|
||||||
|
String json = rs.getString("properties");
|
||||||
|
|
||||||
|
JSONObject props;
|
||||||
|
try {
|
||||||
|
props = (JSONObject) parser.parse(json);
|
||||||
|
} catch (ParseException pe) {
|
||||||
|
Logger.error("Failed to parse JSON for game id " + id + ": " + pe.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.put("name", name);
|
||||||
|
props.put("id", id);
|
||||||
|
|
||||||
|
File file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
file = new File(location);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error("Cannot find game path for id " + id + ": " + e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<Property, String> properties = new HashMap<>();
|
||||||
|
|
||||||
|
for (Object propEntry : props.entrySet()) {
|
||||||
|
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) propEntry;
|
||||||
|
|
||||||
|
String value = String.valueOf(entry.getValue());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Property prop = Property.valueOf(entry.getKey().toString().toUpperCase());
|
||||||
|
properties.put(prop, value);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
Logger.warning("Unknown property: %s".formatted(entry.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item item = new Item(file, properties);
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/main/java/org/adrianvictor/livingroom/data/Indexer.java
Normal file
106
src/main/java/org/adrianvictor/livingroom/data/Indexer.java
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
package org.adrianvictor.livingroom.data;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||||
|
import org.json.simple.JSONArray;
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
import org.json.simple.parser.ParseException;
|
||||||
|
|
||||||
|
import javax.xml.crypto.Data;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Indexer {
|
||||||
|
private static Indexer instance;
|
||||||
|
private Indexer() {};
|
||||||
|
|
||||||
|
public static synchronized Indexer getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new Indexer();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scan(File directory) {;
|
||||||
|
File[] files = directory.listFiles();
|
||||||
|
|
||||||
|
for (File f : files) {
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
File[] dir = f.listFiles();
|
||||||
|
for (File file : dir) {
|
||||||
|
if (file.isFile() && file.toPath().toString().endsWith(".json")) {
|
||||||
|
Logger.info("Found json: %s".formatted(file.toPath().getFileName()));
|
||||||
|
try (FileReader fr = new FileReader(file)) {
|
||||||
|
Object obj = new JSONParser().parse(fr);
|
||||||
|
|
||||||
|
if (obj instanceof JSONObject) {
|
||||||
|
processObject((JSONObject) obj, file);
|
||||||
|
} else if (obj instanceof JSONArray) {
|
||||||
|
for (Object element : (JSONArray) obj) {
|
||||||
|
if (element instanceof JSONObject) {
|
||||||
|
processObject((JSONObject) element, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Logger.error("File not found: %s".formatted(file.getPath()) + e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("IO Error reading file: %s".formatted(file.getPath()) + e);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Logger.error("JSON Parsing error in file: %s".formatted(file.getPath()) + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processObject(JSONObject jobj, File file) {
|
||||||
|
HashMap<Property, String> properties = new HashMap<>();
|
||||||
|
Logger.info("Adding %s to catalog.".formatted(file.getPath()));
|
||||||
|
|
||||||
|
for (Object propEntry : jobj.entrySet()) {
|
||||||
|
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) propEntry;
|
||||||
|
|
||||||
|
String value = String.valueOf(entry.getValue());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Property prop = Property.valueOf(entry.getKey().toString().toUpperCase());
|
||||||
|
properties.put(prop, value);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
Logger.warning("Unknown property: %s".formatted(entry.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Database.getInstance().has(properties.get(Property.NAME))) {
|
||||||
|
Database.getInstance().add(new Item(file, properties));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collectGarbage() {
|
||||||
|
List<Item> games = Database.getInstance().getAllGames();
|
||||||
|
for (Item game : games) {
|
||||||
|
if (!game.location().exists()) {
|
||||||
|
Logger.warning("Game %s was caught by garbage collection because it's file does not exist anymore at %s."
|
||||||
|
.formatted(game.properties().get(Property.NAME), game.location().getPath())
|
||||||
|
);
|
||||||
|
Database.getInstance().remove(game.properties().get(Property.ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scanAsync(File directory) {
|
||||||
|
Thread asyncThread = new Thread(() -> {
|
||||||
|
scan(directory);
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncThread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package org.adrianvictor.livingroom.data.catalog;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
import org.json.simple.parser.ParseException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record Item(File location, HashMap<Property, String> properties) {
|
||||||
|
public HashMap<String, String> getPropertiesString() {
|
||||||
|
HashMap<String, String> result = new HashMap<>();
|
||||||
|
properties.forEach((key, value) -> {
|
||||||
|
result.put(key.name().toLowerCase(), value);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject getPropertiesJSON() {
|
||||||
|
return new JSONObject(getPropertiesString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getVersions() {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
|
||||||
|
if (getPropertiesString().get("files") == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONParser parser = new JSONParser();
|
||||||
|
JSONObject jsonObject = (JSONObject) parser.parse(getPropertiesString().get("files"));
|
||||||
|
|
||||||
|
for (Object o : jsonObject.keySet()) {
|
||||||
|
if (o instanceof String s) {
|
||||||
|
result.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersionFile(String version) {
|
||||||
|
if (getPropertiesString().get("files") != null) {
|
||||||
|
try {
|
||||||
|
JSONParser parser = new JSONParser();
|
||||||
|
JSONObject jsonObject = (JSONObject) parser.parse(getPropertiesString().get("files"));
|
||||||
|
JSONObject versionObject = (JSONObject) jsonObject.get(version);
|
||||||
|
return (String) versionObject.get("file");
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
} catch (ClassCastException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.adrianvictor.livingroom.data.catalog;
|
||||||
|
|
||||||
|
public enum Property {
|
||||||
|
ID,
|
||||||
|
NAME,
|
||||||
|
PUBLISHER,
|
||||||
|
AUTHOR,
|
||||||
|
YEAR,
|
||||||
|
DATE,
|
||||||
|
DESCRIPTION,
|
||||||
|
MEDIUM,
|
||||||
|
ARTWORK,
|
||||||
|
FILES,
|
||||||
|
OPERATING_SYSTEM;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.adrianvictor.livingroom.http;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
|
||||||
|
public interface Handler {
|
||||||
|
HttpResponse result(String baseAddress, String path, HttpExchange exchange);
|
||||||
|
}
|
||||||
21
src/main/java/org/adrianvictor/livingroom/http/Handlers.java
Normal file
21
src/main/java/org/adrianvictor/livingroom/http/Handlers.java
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.adrianvictor.livingroom.http;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.http.handlers.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class Handlers {
|
||||||
|
private static HashMap<Handler, String> map = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
map.put(new CatalogHandler(), "/catalog");
|
||||||
|
map.put(new ImageHandler(), "/pic");
|
||||||
|
map.put(new FileHandler(), "/download");
|
||||||
|
map.put(new WebHandler(), "/web");
|
||||||
|
map.put(new StaticWebHandler(), "/static");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashMap<Handler, String> getAll() {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package org.adrianvictor.livingroom.http;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record HttpResponse(
|
||||||
|
int status,
|
||||||
|
byte[] body,
|
||||||
|
Map<String, String> headers
|
||||||
|
) {
|
||||||
|
public static HttpResponse ok(byte[] body, String contentType, Map<String, String> headers) {
|
||||||
|
return new HttpResponse(
|
||||||
|
200,
|
||||||
|
body,
|
||||||
|
Map.of("Content-Type", contentType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpResponse ok(byte[] body, String contentType) {
|
||||||
|
return ok(body, contentType, new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpResponse redirect(String location) {
|
||||||
|
return new HttpResponse(
|
||||||
|
302,
|
||||||
|
new byte[0],
|
||||||
|
Map.of("Location", location)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/main/java/org/adrianvictor/livingroom/http/Server.java
Normal file
114
src/main/java/org/adrianvictor/livingroom/http/Server.java
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
package org.adrianvictor.livingroom.http;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.http.handlers.FileHandler;
|
||||||
|
import org.adrianvictor.livingroom.http.handlers.ImageHandler;
|
||||||
|
import org.adrianvictor.livingroom.http.handlers.StaticWebHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Server {
|
||||||
|
static Server instance;
|
||||||
|
HttpServer httpServer;
|
||||||
|
|
||||||
|
private Server() {
|
||||||
|
try {
|
||||||
|
httpServer = HttpServer.create(new InetSocketAddress(Main.getConfig().getPort()), 0);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Server getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new Server();
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
httpServer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerHandler(Handler handler, String path) {
|
||||||
|
httpServer.createContext(path, new HttpHandler() {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
|
try {
|
||||||
|
String fullPath = exchange.getRequestURI().getPath();
|
||||||
|
|
||||||
|
String afterBase = "";
|
||||||
|
if (fullPath.length() > path.length()) {
|
||||||
|
afterBase = fullPath.substring(path.length() + 1);
|
||||||
|
}
|
||||||
|
String decoded = java.net.URLDecoder.decode(afterBase, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
int thirdSlash = nthIndexOf(fullPath, '/', 3);
|
||||||
|
String baseAddress;
|
||||||
|
if (thirdSlash == -1) {
|
||||||
|
baseAddress = fullPath;
|
||||||
|
} else {
|
||||||
|
baseAddress = fullPath.substring(0, thirdSlash);
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange.getResponseHeaders().add("X-Content-Type-Options", "nosniff");
|
||||||
|
exchange.getResponseHeaders().add("X-Frame-Option", "DENY");
|
||||||
|
exchange.getResponseHeaders().add("Referrer-Policy", "no-referrer");
|
||||||
|
exchange.getResponseHeaders().add("Content-Security-Policy", "");
|
||||||
|
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||||
|
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
|
||||||
|
if (handler instanceof FileHandler) {
|
||||||
|
((FileHandler) handler).handleStream(exchange, decoded);
|
||||||
|
} else if (handler instanceof ImageHandler) {
|
||||||
|
((ImageHandler) handler).handleStream(exchange, decoded);
|
||||||
|
} else if (handler instanceof StaticWebHandler) {
|
||||||
|
((StaticWebHandler) handler).handleStream(exchange, decoded);
|
||||||
|
} else {
|
||||||
|
HttpResponse result = handler.result(baseAddress, decoded, exchange);
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> header : result.headers().entrySet()) {
|
||||||
|
exchange.getResponseHeaders().set(header.getKey(), header.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange.sendResponseHeaders(result.status(), result.body().length);
|
||||||
|
|
||||||
|
OutputStream os = exchange.getResponseBody();
|
||||||
|
os.write(result.body());
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error("Handler error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
try {
|
||||||
|
exchange.sendResponseHeaders(500, 0);
|
||||||
|
exchange.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
exchange.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int nthIndexOf(String str, char c, int n) {
|
||||||
|
int pos = -1;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
pos = str.indexOf(c, pos + 1);
|
||||||
|
if (pos == -1) break;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.adrianvictor.livingroom.http.handlers;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import org.adrianvictor.livingroom.http.Handler;
|
||||||
|
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||||
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.json.simple.JSONArray;
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record CatalogHandler() implements Handler {
|
||||||
|
@Override
|
||||||
|
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||||
|
List<Item> rawCatalog = Database.getInstance().getAllGames();
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
for (Item item : rawCatalog) {
|
||||||
|
jsonArray.add(new JSONObject(item.getPropertiesString()));
|
||||||
|
}
|
||||||
|
String response = jsonArray.toJSONString();
|
||||||
|
|
||||||
|
return HttpResponse.ok(response.getBytes(), "application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package org.adrianvictor.livingroom.http.handlers;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import org.adrianvictor.livingroom.http.Handler;
|
||||||
|
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FileHandler implements Handler {
|
||||||
|
|
||||||
|
public void handleStream(HttpExchange exchange, String path) {
|
||||||
|
String[] parts = path.split("/");
|
||||||
|
|
||||||
|
if (parts.length < 2) {
|
||||||
|
send404(exchange, "Usage: .../game_id/DownloadName");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id;
|
||||||
|
try {
|
||||||
|
id = Integer.parseInt(parts[0]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
send404(exchange, "The provided game ID %s is not a valid number.".formatted(parts[0]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String platform = URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||||
|
Item game = Database.getInstance().getGame(id);
|
||||||
|
if (game == null) {
|
||||||
|
send404(exchange, "Game not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> files = game.getVersions();
|
||||||
|
if (files.isEmpty() || !files.contains(platform)) {
|
||||||
|
send404(exchange, "This game has no such download: %s.".formatted(platform));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File gameFile = new File(game.location().getParent() + '/' + game.getVersionFile(platform));
|
||||||
|
|
||||||
|
if (!gameFile.exists()) {
|
||||||
|
Logger.info(gameFile.getPath());
|
||||||
|
send404(exchange, "Could not find the file you're looking for. Sorry.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info("Providing game file: " + gameFile.getPath());
|
||||||
|
|
||||||
|
exchange.getResponseHeaders().add("Content-Type", contentType());
|
||||||
|
exchange.getResponseHeaders().add(
|
||||||
|
"Content-Disposition",
|
||||||
|
"attachment; filename=\"" + gameFile.getName() + "\""
|
||||||
|
);
|
||||||
|
|
||||||
|
try (FileInputStream fis = new FileInputStream(gameFile);
|
||||||
|
OutputStream os = exchange.getResponseBody()) {
|
||||||
|
exchange.sendResponseHeaders(200, gameFile.length());
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192]; // 8 KB chunks
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
send404(exchange);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send404(HttpExchange exchange, String message) {
|
||||||
|
byte[] notFound = message.getBytes();
|
||||||
|
try {
|
||||||
|
exchange.sendResponseHeaders(404, notFound.length);
|
||||||
|
exchange.getResponseBody().write(notFound);
|
||||||
|
exchange.getResponseBody().close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send404(HttpExchange exchange) {
|
||||||
|
send404(exchange, "Not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||||
|
return HttpResponse.ok(new byte[0], "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String contentType() {
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.adrianvictor.livingroom.http.handlers;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import org.adrianvictor.livingroom.http.Handler;
|
||||||
|
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||||
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ImageHandler implements Handler {
|
||||||
|
public void handleStream(HttpExchange exchange, String path) throws IOException {
|
||||||
|
int id = Integer.parseInt(path);
|
||||||
|
Item game = Database.getInstance().getGame(id);
|
||||||
|
|
||||||
|
if (game.properties().get(Property.ARTWORK) == null || game.properties().get(Property.ARTWORK).isEmpty()) {
|
||||||
|
byte[] notFound = "Not found".getBytes();
|
||||||
|
exchange.sendResponseHeaders(404, notFound.length);
|
||||||
|
exchange.getResponseBody().write(notFound);
|
||||||
|
exchange.getResponseBody().close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File image = new File(game.location().getParent() + '/' + game.properties().get(Property.ARTWORK));
|
||||||
|
|
||||||
|
exchange.getResponseHeaders().add("Content-Type", "image/png");
|
||||||
|
exchange.sendResponseHeaders(200, image.length());
|
||||||
|
|
||||||
|
try (FileInputStream fis = new FileInputStream(image)) {
|
||||||
|
byte[] buffer = new byte[8192]; // 8 KB chunks
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||||
|
exchange.getResponseBody().write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
exchange.getResponseBody().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||||
|
return HttpResponse.ok(new byte[0], "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String contentType() {
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.adrianvictor.livingroom.http.handlers;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import org.adrianvictor.livingroom.http.Handler;
|
||||||
|
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class StaticWebHandler implements Handler {
|
||||||
|
|
||||||
|
private static final String STATIC_ROOT = "static";
|
||||||
|
|
||||||
|
public void handleStream(HttpExchange exchange, String path) throws IOException {
|
||||||
|
if (path.contains("..")) {
|
||||||
|
exchange.sendResponseHeaders(403, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resourcePath = "static/" + path.replace("\\", "/");
|
||||||
|
InputStream resourceStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
|
||||||
|
|
||||||
|
if (resourceStream == null) {
|
||||||
|
exchange.sendResponseHeaders(404, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType;
|
||||||
|
if (path.endsWith(".css")) contentType = "text/css";
|
||||||
|
else if (path.endsWith(".js")) contentType = "application/javascript";
|
||||||
|
else if (path.endsWith(".png")) contentType = "image/png";
|
||||||
|
else if (path.endsWith(".jpg") || path.endsWith(".jpeg")) contentType = "image/jpeg";
|
||||||
|
else contentType = "application/octet-stream";
|
||||||
|
|
||||||
|
exchange.getResponseHeaders().add("Content-Type", contentType);
|
||||||
|
exchange.sendResponseHeaders(200, 0);
|
||||||
|
|
||||||
|
try (resourceStream; OutputStream os = exchange.getResponseBody()) {
|
||||||
|
resourceStream.transferTo(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||||
|
return HttpResponse.ok(new byte[0], "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String contentType() {
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package org.adrianvictor.livingroom.http.handlers;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import org.adrianvictor.livingroom.auth.User;
|
||||||
|
import org.adrianvictor.livingroom.http.Handler;
|
||||||
|
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.services.UserService;
|
||||||
|
import org.adrianvictor.livingroom.utils.net.Cookie;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
import org.adrianvictor.livingroom.web.Pages;
|
||||||
|
import org.adrianvictor.livingroom.auth.Session;
|
||||||
|
import org.adrianvictor.livingroom.auth.SessionManager;
|
||||||
|
import org.adrianvictor.livingroom.web.pages.Login;
|
||||||
|
import org.adrianvictor.livingroom.web.pages.NotFound;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class WebHandler implements Handler {
|
||||||
|
|
||||||
|
private final Configuration cfg;
|
||||||
|
|
||||||
|
public WebHandler() {
|
||||||
|
cfg = new Configuration(Configuration.VERSION_2_3_32);
|
||||||
|
cfg.setClassForTemplateLoading(
|
||||||
|
Main.class,
|
||||||
|
"/templates"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
String firstPath = path.split("/")[0];
|
||||||
|
String remainingPath = path.substring(firstPath.length());
|
||||||
|
if (remainingPath.startsWith("/")) {
|
||||||
|
remainingPath = remainingPath.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("webpref", "web");
|
||||||
|
|
||||||
|
String sessionId = Cookie.getSessionIdFromCookies(exchange);
|
||||||
|
Session session = SessionManager.getInstance().getSession(sessionId);
|
||||||
|
|
||||||
|
if (firstPath.equals("login")) {
|
||||||
|
if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
return handleLoginPage(baseAddress, remainingPath, exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page page = Pages.get(firstPath);
|
||||||
|
if (page == null) {
|
||||||
|
page = new Login();
|
||||||
|
}
|
||||||
|
return HttpResponse.ok(page.result(cfg, baseAddress, remainingPath, data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
return HttpResponse.ok(new Login().result(cfg, baseAddress, "", data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
Page page = Pages.get(firstPath);
|
||||||
|
|
||||||
|
User user = UserService.getInstance().getUser(session.getUsername());
|
||||||
|
|
||||||
|
data.put("username", session.getUsername());
|
||||||
|
data.put("userRole", user.role().toString().toLowerCase());
|
||||||
|
|
||||||
|
if (page == null) {
|
||||||
|
return HttpResponse.ok(new NotFound().result(cfg, baseAddress, path, data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.ok(page.result(cfg, baseAddress, remainingPath, data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponse handleLoginPage(String baseAddress, String path, HttpExchange exchange) {
|
||||||
|
String method = exchange.getRequestMethod();
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("webpref", "web");
|
||||||
|
|
||||||
|
if ("POST".equals(method)) {
|
||||||
|
try {
|
||||||
|
// Read POST body
|
||||||
|
String body = new String(exchange.getRequestBody().readAllBytes());
|
||||||
|
String[] params = body.split("&");
|
||||||
|
String username = null;
|
||||||
|
String password = null;
|
||||||
|
|
||||||
|
|
||||||
|
for (String param : params) {
|
||||||
|
String[] kv = param.split("=");
|
||||||
|
if (kv.length == 2) {
|
||||||
|
if ("username".equals(kv[0])) {
|
||||||
|
username = java.net.URLDecoder.decode(kv[1], java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
} else if ("password".equals(kv[0])) {
|
||||||
|
password = java.net.URLDecoder.decode(kv[1], java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UserService.getInstance().getUser(username).auth(password)) {
|
||||||
|
Session session = SessionManager.getInstance().createSession(username);
|
||||||
|
Cookie.setSessionCookie(exchange, session.getSessionId());
|
||||||
|
|
||||||
|
return HttpResponse.redirect("/web/");
|
||||||
|
} else {
|
||||||
|
return HttpResponse.ok(new Login().result(cfg, baseAddress, "", data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error("Login error: " + e.getMessage());
|
||||||
|
return HttpResponse.ok(new NotFound().result(cfg, baseAddress, path, data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.ok(new NotFound().result(cfg, baseAddress, path, data, exchange).getBytes(), "text/html");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.adrianvictor.livingroom.services;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.auth.Role;
|
||||||
|
import org.adrianvictor.livingroom.auth.User;
|
||||||
|
import org.adrianvictor.livingroom.config.AppConfig;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class UserService {
|
||||||
|
private static UserService instance;
|
||||||
|
private static Map<String, AppConfig.UserConfig> users;
|
||||||
|
|
||||||
|
private UserService() {
|
||||||
|
users = Main.getConfig().getUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserService getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new UserService();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser(String user) {
|
||||||
|
AppConfig.UserConfig raw = users.get(user);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Role role;
|
||||||
|
|
||||||
|
try {
|
||||||
|
role = Role.valueOf(raw.getRole());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
role = Role.USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new User(
|
||||||
|
user,
|
||||||
|
raw.getHash(),
|
||||||
|
raw.getSalt(),
|
||||||
|
role
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.adrianvictor.livingroom.utils;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
|
||||||
|
public class PasswordUtils {
|
||||||
|
private static final int ITERATIONS = 65536;
|
||||||
|
private static final int KEY_LENGTH = 256;
|
||||||
|
|
||||||
|
public static String generateSalt() {
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
new SecureRandom().nextBytes(salt);
|
||||||
|
return Base64.getEncoder().encodeToString(salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String hashPassword(String password, String salt) {
|
||||||
|
try {
|
||||||
|
PBEKeySpec spec = new PBEKeySpec(
|
||||||
|
password.toCharArray(),
|
||||||
|
Base64.getDecoder().decode(salt),
|
||||||
|
ITERATIONS,
|
||||||
|
KEY_LENGTH
|
||||||
|
);
|
||||||
|
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
byte[] hash = skf.generateSecret(spec).getEncoded();
|
||||||
|
return Base64.getEncoder().encodeToString(hash);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||||
|
throw new RuntimeException("Error hashing password", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifyPassword(String password, String salt, String expectedHash) {
|
||||||
|
String hash = hashPassword(password, salt);
|
||||||
|
return hash.equals(expectedHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.adrianvictor.livingroom.utils.image;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ColorExtractor {
|
||||||
|
public static String getDominantColor(File imageFile) throws Exception {
|
||||||
|
BufferedImage image = ImageIO.read(imageFile);
|
||||||
|
|
||||||
|
// Downsample
|
||||||
|
BufferedImage resized = new BufferedImage(150, 150, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g2d = resized.createGraphics();
|
||||||
|
g2d.drawImage(image, 0, 0, 150, 150, null);
|
||||||
|
g2d.dispose();
|
||||||
|
|
||||||
|
// Score by saturation, skip neutrals
|
||||||
|
Map<Integer, Double> colorScore = new HashMap<>();
|
||||||
|
for (int y = 0; y < resized.getHeight(); y++) {
|
||||||
|
for (int x = 0; x < resized.getWidth(); x++) {
|
||||||
|
int rgb = resized.getRGB(x, y);
|
||||||
|
|
||||||
|
// if (isNeutralColor(rgb)) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);
|
||||||
|
double score = colorScore.getOrDefault(rgb, 0.0) + hsb[1];
|
||||||
|
colorScore.put(rgb, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int dominantColor = colorScore.entrySet().stream()
|
||||||
|
.max(Map.Entry.comparingByValue())
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.orElse(0);
|
||||||
|
|
||||||
|
return String.format("#%06X", dominantColor & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDominantColor(Item game) throws Exception {
|
||||||
|
String image = game.properties().get(Property.ARTWORK);
|
||||||
|
File imageFile = new File(game.location().getParent() + '/' + image);
|
||||||
|
return getDominantColor(imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.adrianvictor.livingroom.utils.net;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
|
||||||
|
public class Cookie {
|
||||||
|
private static final String SESSION_COOKIE_NAME = "SESSIONID";
|
||||||
|
|
||||||
|
public static String getSessionIdFromCookies(HttpExchange exchange) {
|
||||||
|
String cookieHeader = exchange.getRequestHeaders().getFirst("Cookie");
|
||||||
|
if (cookieHeader == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] cookies = cookieHeader.split(";");
|
||||||
|
for (String cookie : cookies) {
|
||||||
|
cookie = cookie.trim();
|
||||||
|
if (cookie.startsWith(SESSION_COOKIE_NAME + "=")) {
|
||||||
|
return cookie.substring((SESSION_COOKIE_NAME + "=").length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setSessionCookie(HttpExchange exchange, String sessionId) {
|
||||||
|
String cookie = SESSION_COOKIE_NAME + "=" + sessionId + "; Path=/; HttpOnly; SameSite=Strict";
|
||||||
|
exchange.getResponseHeaders().add("Set-Cookie", cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearSessionCookie(HttpExchange exchange) {
|
||||||
|
String cookie = SESSION_COOKIE_NAME + "=; Path=/; Max-Age=0; HttpOnly";
|
||||||
|
exchange.getResponseHeaders().add("Set-Cookie", cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/java/org/adrianvictor/livingroom/web/Page.java
Normal file
10
src/main/java/org/adrianvictor/livingroom/web/Page.java
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.adrianvictor.livingroom.web;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface Page {
|
||||||
|
String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange);
|
||||||
|
}
|
||||||
22
src/main/java/org/adrianvictor/livingroom/web/Pages.java
Normal file
22
src/main/java/org/adrianvictor/livingroom/web/Pages.java
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.adrianvictor.livingroom.web;
|
||||||
|
|
||||||
|
import org.adrianvictor.livingroom.web.pages.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class Pages {
|
||||||
|
private static HashMap<String, Page> map = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
Index index = new Index();
|
||||||
|
map.put("", index);
|
||||||
|
map.put("game", new Game());
|
||||||
|
map.put("logout", new Logout());
|
||||||
|
map.put("scan", new Scan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashMap<String, Page> getAll() {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
public static Page get(String path) { return map.get(path); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.adrianvictor.livingroom.web;
|
||||||
|
|
||||||
|
public class QuickResponses {
|
||||||
|
public static String notFound() {
|
||||||
|
return "Not Found";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.Template;
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||||
|
import org.adrianvictor.livingroom.utils.image.ColorExtractor;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
import org.adrianvictor.livingroom.web.QuickResponses;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Game implements Page {
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
try {
|
||||||
|
Template template = cfg.getTemplate("game.ftl");
|
||||||
|
|
||||||
|
String[] args = path.split("/");
|
||||||
|
|
||||||
|
int id;
|
||||||
|
try {
|
||||||
|
id = Integer.parseInt(args[0]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return QuickResponses.notFound();
|
||||||
|
// send404(exchange, "The provided game ID %s is not a valid number.".formatted(args[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Item game = Database.getInstance().getGame(id);
|
||||||
|
|
||||||
|
Map<String, String> gameMap = game.getPropertiesString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String accentHEX = ColorExtractor.getDominantColor(game);
|
||||||
|
data.put("accentColor", accentHEX);
|
||||||
|
} catch (Exception e) {
|
||||||
|
data.put("accentColor", "#FFFFFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("game", gameMap);
|
||||||
|
data.put("versions", game.getVersions());
|
||||||
|
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
template.process(data, writer);
|
||||||
|
|
||||||
|
return writer.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
return "<h1>Error</h1>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.Template;
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Item;
|
||||||
|
import org.adrianvictor.livingroom.data.catalog.Property;
|
||||||
|
import org.adrianvictor.livingroom.utils.image.ColorExtractor;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Index implements Page {
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
try {
|
||||||
|
Template template = cfg.getTemplate("games.ftl");
|
||||||
|
|
||||||
|
List<Map<String, Object>> gamesList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Item item : Database.getInstance().getAllGames()) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
for (Map.Entry<Property, String> e : item.properties().entrySet()) {
|
||||||
|
map.put(e.getKey().name(), e.getValue());
|
||||||
|
}
|
||||||
|
map.put("versions", item.getVersions());
|
||||||
|
String accent = "#FFFFFF";
|
||||||
|
try {
|
||||||
|
accent = ColorExtractor.getDominantColor(item);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
map.put("accentColor", accent);
|
||||||
|
gamesList.add(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("games", gamesList);
|
||||||
|
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
template.process(data, writer);
|
||||||
|
|
||||||
|
return writer.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
return "<h1>Error</h1>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import org.adrianvictor.livingroom.Logger;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Login implements Page {
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
try {
|
||||||
|
// Check if this is a POST request
|
||||||
|
// For now, just show the login form
|
||||||
|
|
||||||
|
freemarker.template.Template template = cfg.getTemplate("login.ftl");
|
||||||
|
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
template.process(data, writer);
|
||||||
|
|
||||||
|
return writer.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error(e.getMessage());
|
||||||
|
return "<h1>Error</h1>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import org.adrianvictor.livingroom.auth.SessionManager;
|
||||||
|
import org.adrianvictor.livingroom.utils.net.Cookie;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Logout implements Page {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
String sessionId = Cookie.getSessionIdFromCookies(exchange);
|
||||||
|
SessionManager sm = SessionManager.getInstance();
|
||||||
|
sm.destroySession(sessionId);
|
||||||
|
data.remove("username");
|
||||||
|
return new Login().result(cfg, baseAddress, path, data, exchange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class NotFound implements Page {
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
return "Not found";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
|
import org.adrianvictor.livingroom.data.Indexer;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
import org.adrianvictor.livingroom.web.QuickResponses;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Remove implements Page {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
String arg = path.split("/")[0];
|
||||||
|
if (arg == null) {
|
||||||
|
return QuickResponses.notFound();
|
||||||
|
}
|
||||||
|
int id;
|
||||||
|
try {
|
||||||
|
id = Integer.parseInt(arg);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return QuickResponses.notFound();
|
||||||
|
}
|
||||||
|
Database.getInstance().remove(String.valueOf(id));
|
||||||
|
return "Success";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.adrianvictor.livingroom.web.pages;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import org.adrianvictor.livingroom.Main;
|
||||||
|
import org.adrianvictor.livingroom.data.Indexer;
|
||||||
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Scan implements Page {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
|
Indexer.getInstance().scanAsync(new File(Main.getConfig().getLibrary()));
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/resources/config.json
Normal file
9
src/main/resources/config.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"library": "/home/adrian/Desktop/testing/livingroom",
|
||||||
|
"users": {
|
||||||
|
"admin": {
|
||||||
|
"role": "admin",
|
||||||
|
"password": "test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/main/resources/static/images/dvd.png
Normal file
BIN
src/main/resources/static/images/dvd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 858 KiB |
275
src/main/resources/static/styles.css
Normal file
275
src/main/resources/static/styles.css
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
:root {
|
||||||
|
--accent-color-darker: rgba(255, 255, 255, 0.6);
|
||||||
|
--accent-color: white;
|
||||||
|
--default-box-shadow: 2px 7px 5px rgba(0,0,0,0.3), 0px -4px 10px rgba(0,0,0,0.3);
|
||||||
|
--alternative-bg: rgb(20, 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 1em;
|
||||||
|
border-bottom: thick solid var(--accent-color);
|
||||||
|
background-color: var(--alternative-bg);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
header::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--accent-color);
|
||||||
|
filter: drop-shadow(0 0 50vh var(--accent-color));
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
|
margin-bottom: .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
transition: .2s;
|
||||||
|
color: gainsboro;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
border-bottom: medium solid var(--accent-color-darker);
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-bottom-color: var(--accent-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .fakeButton {
|
||||||
|
font-size: .9em;
|
||||||
|
border: medium solid var(--accent-color);
|
||||||
|
padding: .6rem;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
transition: .2s;
|
||||||
|
box-shadow: var(--default-box-shadow);
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fakeButton {
|
||||||
|
/* border-color: var(--accent-color-darker); */
|
||||||
|
border-style: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover, button:focus {
|
||||||
|
color: black;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#games {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameCard {
|
||||||
|
width: 8em;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameCard:hover {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameCard .gameImage {
|
||||||
|
border: thin solid var(--accent-color);
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: var(--default-box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameCard .gameTitle {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameCard .versionsList {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game page */
|
||||||
|
|
||||||
|
body#gamePage {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameImage {
|
||||||
|
border: medium solid var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage h2 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .game {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameImageContainer {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameImage {
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: var(--default-box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameDisk {
|
||||||
|
position: absolute;
|
||||||
|
height: 60%;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
transition: .4s;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: drop-shadow(2px 7px 5px rgba(0,0,0,0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameInfo {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameDownloads {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: auto;
|
||||||
|
text-wrap: wrap;
|
||||||
|
transition: opacity 0.3s, visibility 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameImageContainer.showDownloads:hover .gameDisk {
|
||||||
|
transform: translateX(60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameImageContainer.showDownloads .gameDownloads {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameInfo .gameTitle .gameAuthorText {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameInfo .gameDescription {
|
||||||
|
padding: .4em;
|
||||||
|
background-color: rgb(40, 40, 40);
|
||||||
|
box-shadow: var(--default-box-shadow);
|
||||||
|
border: medium solid var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameInfo .gameDescription p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#gamePage .gameInfo .gameTitle h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
.loginForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm input {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm .formGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .4em;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm .formSubmit {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
8
src/main/resources/static/svg/disc.svg
Normal file
8
src/main/resources/static/svg/disc.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path fill="#444" d="M8 0c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM15 8c0 1.1-0.2 2.1-0.7 3l-2.7-1.2c0.2-0.6 0.4-1.2 0.4-1.8 0-2.2-1.8-4-4-4-0.5 0-0.9 0.1-1.4 0.3l-1.2-2.8c0.6-0.2 1.2-0.4 1.8-0.5l0.3 3h0.5v-3c3.9 0 7 3.1 7 7zM8 5c1.7 0 3 1.3 3 3s-1.3 3-3 3-3-1.3-3-3 1.3-3 3-3zM1 8c0-1.1 0.2-2.1 0.7-3l2.7 1.2c-0.2 0.6-0.4 1.2-0.4 1.8 0 2.2 1.8 4 4 4 0.5 0 0.9-0.1 1.4-0.3l1.2 2.8c-0.6 0.2-1.2 0.4-1.8 0.5l-0.3-3h-0.5v3c-3.9 0-7-3.1-7-7z"></path>
|
||||||
|
<path fill="#444" d="M10 8c0 1.105-0.895 2-2 2s-2-0.895-2-2c0-1.105 0.895-2 2-2s2 0.895 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 929 B |
14
src/main/resources/templates/base.ftl
Normal file
14
src/main/resources/templates/base.ftl
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
<title><#if pageTitle??>${pageTitle}<#else>LivingRoom</#if></title>
|
||||||
|
</head>
|
||||||
|
<body <#if bodyID??>id="${bodyID}"</#if><#if accentColor??> style="--accent-color: ${accentColor};"</#if>>
|
||||||
|
<#include "header.ftl">
|
||||||
|
<main>
|
||||||
|
<#if pageContent??>${pageContent}</#if>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
src/main/resources/templates/game.ftl
Normal file
54
src/main/resources/templates/game.ftl
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<#assign pageTitle = game["name"]>
|
||||||
|
<#assign bodyID = "gamePage">
|
||||||
|
<#assign pageContent>
|
||||||
|
<div class="game">
|
||||||
|
<div class="gameImageContainer">
|
||||||
|
<img class="gameImage" src="/pic/${game["id"]}">
|
||||||
|
<img class="gameDisk" src="/static/images/dvd.png">
|
||||||
|
<div class="gameDownloads">
|
||||||
|
<#if versions?has_content>
|
||||||
|
<div class="downloads">
|
||||||
|
<h3>Available Downloads</h3>
|
||||||
|
<ul>
|
||||||
|
<#list versions as version>
|
||||||
|
<li><a href="/download/${game["id"]}/${version}">${version}</a></li>
|
||||||
|
</#list>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gameInfo">
|
||||||
|
<div class="gameTitle">
|
||||||
|
<h2>${game["name"]}<#if game["author"]?has_content><span class="gameAuthorText"> by ${game["author"]}</span></#if></h2>
|
||||||
|
<#if game["year"]?has_content>
|
||||||
|
<p>${game["year"]}</p>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="downloadsButton">
|
||||||
|
<button onclick="document.querySelector('.gameImageContainer').classList.toggle('showDownloads');">Downloads</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gameProperties">
|
||||||
|
<#if game["publisher"]?has_content>
|
||||||
|
<p><b>Published by</b> ${game["publisher"]}</p>
|
||||||
|
</#if>
|
||||||
|
<#if game["date"]?has_content>
|
||||||
|
<p><b>Publishing date:</b> ${game["date"]}</p>
|
||||||
|
</#if>
|
||||||
|
<#if game["operating_system"]?has_content>
|
||||||
|
<p><b>Made for</b> ${game["operating_system"]}</p>
|
||||||
|
</#if>
|
||||||
|
<#if game["description"]?has_content>
|
||||||
|
<p><b>Description: </b></p>
|
||||||
|
<div class="gameDescription">
|
||||||
|
<p>${game["description"]}</p>
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
<#if userRole == "admin">Admin actions: <a href="/${webpref}/remove/${game["id"]}">deindex</a>, <a href="/${webpref}/scan">trigger new scan</a></#if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</#assign>
|
||||||
|
<#include "base.ftl">
|
||||||
15
src/main/resources/templates/games.ftl
Normal file
15
src/main/resources/templates/games.ftl
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<#assign pageTitle = "Games">
|
||||||
|
<#assign pageContent>
|
||||||
|
<h2>Games</h2>
|
||||||
|
<div id="games">
|
||||||
|
<#list games as game>
|
||||||
|
<div class="gameCard" style="--accent-color: ${game["accentColor"]};">
|
||||||
|
<a href="/${webpref}/game/${game["ID"]}">
|
||||||
|
<img class="gameImage" src="/pic/${game["ID"]}">
|
||||||
|
<p>${game["NAME"]}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</#list>
|
||||||
|
</div>
|
||||||
|
</#assign>
|
||||||
|
<#include "base.ftl">
|
||||||
4
src/main/resources/templates/header.ftl
Normal file
4
src/main/resources/templates/header.ftl
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<header>
|
||||||
|
<h1><a href="/${webpref}/">LivingRoom</a></h1>
|
||||||
|
<p><#if username??>${username} (<a href="/${webpref}/logout">logout</a>)</#if></p>
|
||||||
|
</header>
|
||||||
20
src/main/resources/templates/login.ftl
Normal file
20
src/main/resources/templates/login.ftl
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<#assign pageTitle = "Login">
|
||||||
|
<#assign pageContent>
|
||||||
|
<div class="loginContainer">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form class="loginForm" method="POST" action="/${webpref}/login">
|
||||||
|
<div class="formGroup username">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="formGroup password">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="formSubmit">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</#assign>
|
||||||
|
<#include "base.ftl">
|
||||||
Loading…
Add table
Add a link
Reference in a new issue