Initial commit
This commit is contained in:
commit
8b79863393
12 changed files with 1573 additions and 0 deletions
2
iwm_browser/__init__.py
Normal file
2
iwm_browser/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
__version__ = '0.1.0'
|
||||
|
117
iwm_browser/main.py
Normal file
117
iwm_browser/main.py
Normal 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)
|
||||
|
62
iwm_browser/static/homepage.css
Normal file
62
iwm_browser/static/homepage.css
Normal 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;
|
||||
}
|
28
iwm_browser/static/index.css
Normal file
28
iwm_browser/static/index.css
Normal 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;
|
||||
}
|
87
iwm_browser/static/main.css
Normal file
87
iwm_browser/static/main.css
Normal 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;
|
||||
|
||||
}
|
97
iwm_browser/static/search/search.css
Normal file
97
iwm_browser/static/search/search.css
Normal 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;
|
||||
}
|
126
iwm_browser/static/search/searchResults.css
Normal file
126
iwm_browser/static/search/searchResults.css
Normal 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;
|
||||
}*/
|
12
iwm_browser/templates/base.html
Normal file
12
iwm_browser/templates/base.html
Normal 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>
|
19
iwm_browser/templates/home.html
Normal file
19
iwm_browser/templates/home.html
Normal 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>
|
90
iwm_browser/templates/search.html
Normal file
90
iwm_browser/templates/search.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue