Initial commit

This commit is contained in:
magmaus3 2022-10-29 17:26:04 +02:00
commit 8b79863393
Signed by: magmaus3
GPG key ID: 966755D3F4A9B251
12 changed files with 1573 additions and 0 deletions

2
iwm_browser/__init__.py Normal file
View file

@ -0,0 +1,2 @@
__version__ = '0.1.0'

117
iwm_browser/main.py Normal file
View file

@ -0,0 +1,117 @@
from typing import Union
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, Form
import jinja2
import uvicorn
import httpx
import json
import os
import re
basedir = os.path.dirname(__file__)
app = FastAPI()
app.mount("/static", StaticFiles(directory=basedir + "/static"), name="static")
template_env = jinja2.Environment(
loader=jinja2.PackageLoader("iwm_browser", "templates"),
auto_reload=True
)
BASE_URL = "http://make.fangam.es"
THUMB_URL = "https://images.make.fangam.es"
# Matches level code.
# \S[A-Z0-9]{4}\-[A-Z0-9]{4} = With dash, no whitespace
# \S[A-Z0-9]{8} = without dash, no whitespace
level_code_regex = re.compile("[A-Z0-9]{4}\-[A-Z0-9]{4}|[A-Z0-9]{8}")
@app.get("/", response_class=HTMLResponse)
async def root():
template = template_env.get_template("home.html")
return template.render()
@app.get("/search", response_class=HTMLResponse)
async def search(
q: Union[str, None] = None,
p: Union[int, None] = None,
sort: Union[str, None] = None,
dir: Union[str, None] = None,
date: Union[int, None] = None
):
template = template_env.get_template("search.html")
limit = 10
if p is None:
p = 0
searchResults = None
if q is not None:
print(level_code_regex.match(q))
if level_code_regex.match(q):
levelCode = q.upper().replace("-", "")
searchValue = q
entryNumber = -1
try:
rq = httpx.get(BASE_URL + "/api/v1/map", params={
"code": levelCode,
}, timeout=10)
searchResults = rq.json()
print(rq.url, p * limit)
except httpx.ReadTimeout:
return "Server timed out"
else:
# Check sort types
if sort is None:
sort = "average_rating"
if dir is None:
dir = "desc"
if date is None:
date = -1
order = {"Dir": dir, "Name": sort}
if date == -1:
last_x_hours = {}
else:
last_x_hours = {"last_x_hours": date}
searchValue = q
try:
rq = httpx.get(BASE_URL + "/api/v1/map", params={
"start": p * limit,
"limit": limit,
"min_diff": 0.00,
"max_diff": 5.00,
"order": json.dumps([order]),
"name": q,
**last_x_hours
}, timeout=10)
searchResults = rq.json()
print(rq.url, p * limit)
except httpx.ReadTimeout:
return "Server timed out"
entryNumber=len(searchResults)
else:
searchValue = ''
entryNumber = None
return template.render(
searchValue=searchValue,
searchPage=p,
searchResults=searchResults,
THUMB_URL=THUMB_URL,
entryLimit=limit,
entryNumber=entryNumber
)
def start():
"""Launched with `poetry run start` at root level"""
uvicorn.run("iwm_browser.main:app", host="0.0.0.0", port=7775, reload=True)

View file

@ -0,0 +1,62 @@
/* .Home {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.contentBox {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
outline: 2px solid whitesmoke;
padding: 16px;
/* width: 80%; */
/* height: 80%; */
} */
.tileBox {
display: flex;
/* outline: 3px dashed pink; */
justify-content: center;
padding: 2px;
width: 100%;
margin-top: 16px;
}
.homeTile {
display: flex;
flex-direction: column;
margin: 4px;
padding: 4px;
justify-content: center;
outline: 1px solid greenyellow;
transition: all 0.5s ease-out;
}
.homeTile a {
color: inherit;
text-decoration: inherit;
transition: all 0.5s ease-out;
}
.homeTile:hover {
outline: 1px solid cyan;
box-shadow: 2px 2px 5px white;
}
.homeTile a:hover {
color: cyan;
}

View file

@ -0,0 +1,28 @@
html,body {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
overflow: scroll;
}
body {
display: flex;
flex-direction: column;
background-color: #20202a;
color: #eee;
font-size: xx-large;
font-family: monospace;
}
.version {
position: absolute;
display: flex;
text-align: right;
padding: 0px;
margin: 0px;
font-size: small;
}

View file

@ -0,0 +1,87 @@
.Home, .Search {
display: flex;
justify-content: center;
align-items: center;
/* margin-top: 2px; */
width: 100vw;
height: 100vh;
}
.contentBox {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
outline: 2px solid whitesmoke;
padding: 16px;
/* width: 80%; */
min-height: 20%;
}
.tileBox {
display: flex;
/* outline: 3px dashed pink; */
justify-content: center;
padding: 2px;
width: 100%;
margin-top: 16px;
}
.homeTile {
display: flex;
flex-direction: column;
margin: 4px;
padding: 4px;
justify-content: center;
outline: 1px solid greenyellow;
transition: all 0.5s ease-out;
}
.homeTile a {
color: inherit;
text-decoration: inherit;
transition: all 0.5s ease-out;
}
.homeTile:hover {
outline: 1px solid cyan;
box-shadow: 2px 2px 5px white;
}
.homeTile a:hover {
color: cyan;
}
code {
font-family: monospace;
white-space: pre;
background-color: rgba(255, 255, 255, 0.100);
padding: 4px;
outline: 1px solid white;
}
/* Various UI components */
/* TODO: move to another file */
.splitter {
padding: 0px;
margin: 0px;
width: 100%;
height: 2px;
background-color: white;
}

View file

@ -0,0 +1,97 @@
.contentBox_ {
align-items: start;
margin: 0.2rem
}
.header {
text-align: center;
height: min-content;
}
.searchBar {
display: flex;
flex-direction: column;
margin-top: 0.2em;
/* width: 23rem; */
width: 100%;
height: max-content;
justify-content: center;
justify-self: center;
}
.searchBar input {
width: 95%;
background-color: transparent;
color: white;
border: 1px solid white;
border-right: 1px dotted white;
font-family: monospace;
font-size: 0.5em;
}
.searchBar .searchButton {
width: 5%;
background-color: transparent;
border: 1px solid white;
border-left: none;
}
.searchBar .search_entry form {
display: flex;
flex-direction: column;
width: 100%;
height: 1em;
}
.searchBar .search_entry {
display: flex;
}
.searchBar .sortType {
display: flex;
font-size: medium;
}
.searchBar summary {
font-size: small;
}
.searchBar .upload_date {
font-size: medium;
}
.searchResultsBox {
overflow: scroll;
outline: 2px solid white;
padding: 0.2em;
}
.Search {
align-items: start;
}
.contentBox_ {
display: grid;
flex-direction: column;
/* min-height: 80%; */
/*align-self: center;*/
}
.searchResultsFooter {
display: flex;
font-size: large;
text-align: center;
border-top: 0.1rem solid white;
align-items: center;
margin-bottom: 3rem;
}
.searchResultsFooter p {
margin: 0.5rem;
}
.searchResultsFooter a {
margin: 1rem;
border: 1px solid blue;
}

View file

@ -0,0 +1,126 @@
.searchResults {
display: grid;
align-self: start;
align-content: start;
}
.searchResultCard {
display: grid;
margin: 4px;
margin-top: 8px;
margin-bottom: 8px;
padding: 0.5em;
/*font-size: 0.8rem;*/
font-size: medium;
outline: 1px solid white;
max-width: 50rem;
grid-template-columns: 0.9fr 1fr;
column-gap: 0.5rem;
}
@media screen and (max-width: 550px) {
.searchResults {
font-size: 16px;
}
}
.searchResultCard .thumbnail {
display: grid;
justify-self: left;
background-color: transparent;
justify-content: center;
max-width: 100%;
height: fit-content
}
.searchResultCard .thumbnail img {
width: 100%;
height: auto;
}
.searchResultCard .details {
display: grid;
border-left: 1px solid white;
padding: 0.5rem;
column-gap: 1em;
justify-content: left;
text-align: left;
grid-template-columns: 1fr;
grid-template-areas: "basic";
}
.searchResultCard .basic {
align-self: left;
justify-content: left;
}
.levelRating {
justify-self: left;
margin-top: 0.5em;
}
.levelTitle {
justify-self: left;
text-align: left;
font-size: large;
font-weight: bold;
margin-bottom: 0.3rem;
}
.creatorName {
justify-self: left;
text-align: left;
/*font-size: 0.8rem;*/
}
.levelDescription {
text-align: left;
background-color: rgba(245, 222, 179, 0.128);
padding: 1em;
font-size: 1rem;
/* justify-self: right;
width: 100%;
padding: 0.5em;
outline: 1px dashed pink; */
}
.levelCode {
margin-top: 0.5em;
}
.levelCode input {
background-color: transparent;
color: white;
font-family: monospace;
width: 6rem;
text-align: center;
border: 1px solid #505050;
}
/*
.levelDifficulty {
display: flex;
justify-self: right;
/* z-index: 1; * /
}
.levelDifficulty span {
text-align: end;
justify-self: right;
font-size: 2em;
overflow: hidden;
}*/

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/index.css">
</head>
<body>
stuff
</body>
</html>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/index.css">
<link rel="stylesheet" href="/static/main.css">
</head>
<body>
<div class="Home">
<div class="contentBox">
<div class="header">IWM Browser</div>
<div class="splitter" />
<div class="tileBox"><a href="/search">Search</a></div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/index.css">
<link rel="stylesheet" href="/static/main.css">
<link rel="stylesheet" href="/static/search/search.css">
<link rel="stylesheet" href="/static/search/searchResults.css">
<!-- Optional features -->
<script>
function copy(obj) {
// copies the contents of the
// input field to clipboard, and
// adds the "copied" css class
navigator.clipboard.writeText(obj.value);
alert("Level code copied")
}
</script>
</head>
<body>
<div class="Search">
<div class="contentBox_">
<div class="header">Search</div>
<div class="splitter"></div>
<div class='searchBar'>
<form action="/search" method="get">
<div class="search_entry">
<input type='text' id="q" name="q" value='{{ searchValue }}' placeholder="Enter a level name or code"/>
<button class="searchButton" id='searchButton' type='submit'>🔍</button>
</div>
<details>
<summary>More options</summary>
<div class="sortType">
<label for="sort">Sort type</label>
<select id="sort" name="sort">
<option value="average_rating">Rating</option>
<option value="created_at">Upload date</option>
<option value="average_difficulty">Difficulty</option>
<option value="play_count">Plays</option>
</select>
<select id="dir" name="dir">
<option value="desc">Descending</option>
<option value="asc">Ascending</option>
</select>
</div>
<div class="upload_date">
<label for="date">Upload date</label>
<select id="date" name="date">
<option value="-1">All time</option>
<option value="720">Past 30 days</option>
<option value="168">Past 7 days</option>
<option value="24">Past day</option>
</select>
</div>
</details>
</form>
</div>
{% if searchResults %}
<div class="searchResults">
{% for map in searchResults %}
<div class="searchResultCard">
<div class="thumbnail"><img src="{{ THUMB_URL }}/{{ map['ID'] }}.png"></div>
<div class="details">
<div class="basic">
<div class="levelTitle">{{ map['Name'] }}</div>
<div class="levelDifficulty"><span>Difficulty: {{ '▲' * map['AverageUserDifficulty']|int }}</span></div>
<div class="creatorName">by <b>{{ map['CreatorName'] }}</b></div>
<div class="levelRating">{{ map['NumThumbsUp'] }} 👍 {{ map['NumThumbsDown'] }} 👎</div>
<div class="levelCode">Code: <input type="text" onclick="copy(this)" readonly=1 value="{{ map['MapCode'] }}" /></div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="searchResultsFooter">
<p>Page {{ searchPage+1 }}</p>
{% if not searchPage <= 0 %}<a href="/search?q={{ searchValue }}&p={{ searchPage-1 }}">Prev</a>{% endif %}
{% if not entryNumber < entryLimit %}<a href="/search?q={{ searchValue }}&p={{ searchPage+1 }}">Next</a>{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
</body>
</html>