Skip to content

Commit

Permalink
preliminary loading functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ThePhar committed Jun 24, 2024
1 parent 733c833 commit 1edf9e7
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 267 deletions.
151 changes: 57 additions & 94 deletions WebHostLib/options.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
import collections.abc
import json
import os
from textwrap import dedent
from typing import Dict, Type, Union
from typing import Dict, Union

import yaml
from docutils.core import publish_parts
from flask import Response, redirect, render_template, request
from flask_wtf import FlaskForm
from wtforms import SelectField, StringField
from wtforms.fields.simple import HiddenField
from wtforms.validators import DataRequired

import Options
from Utils import get_file_safe_name, local_path
from worlds.AutoWorld import AutoWorldRegister, World
from worlds.AutoWorld import AutoWorldRegister
from . import app, cache
from .generate import get_meta


class PlayerOptionsForm(FlaskForm):
game = HiddenField()
name = StringField("Player Name", validators=[DataRequired()])


def create() -> None:
target_folder = local_path("WebHostLib", "static", "generated")
yaml_folder = os.path.join(target_folder, "configs")
Expand All @@ -47,37 +37,68 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False
for group in world.web.option_groups:
start_collapsed[group.name] = group.start_collapsed

# TODO: Testing
form = PlayerOptionsForm(prefix="meta")

return render_template(
template,
world_name=world_name,
# world=world,
# option_groups=Options.get_option_groups(world, visibility_level=visibility_flag),
# start_collapsed=start_collapsed,
# issubclass=issubclass,
# Options=Options,
form=form,
world=world,
option_groups=Options.get_option_groups(world, visibility_level=visibility_flag),
start_collapsed=start_collapsed,
issubclass=issubclass,
Options=Options,
header_theme=f"header/{get_world_theme(world_name)}Header.html",
)


# Test
@app.route("/games/<string:world_name>/test", methods=["POST"])
def test_action(world_name: str) -> Union[Response, str]:
return str(",".join([f"{k}: {v}" for k, v in request.form.items()]))


@app.route("/games/<string:world_name>/<string:option_name>/<int:start_index>")
def test_action_2(world_name: str, option_name: str, start_index: int) -> Union[Response, str]:
@app.route("/games/<string:world_name>/options")
def test_action_2(world_name: str) -> dict:
world = AutoWorldRegister.world_types[world_name]
option = world.options_dataclass.type_hints[option_name]
keys = list(option.valid_keys)
keys.sort()
options, presets = {}, {}
for option_name, option in world.options_dataclass.type_hints.items():
if issubclass(option, Options.VerifyKeys):
if issubclass(option, Options.LocationSet) and option.verify_location_name:
keys = list(sorted(option.valid_keys) if option.valid_keys else sorted(world.location_names))
elif issubclass(option, (Options.ItemSet, Options.ItemDict)) and option.verify_item_name:
keys = list(sorted(option.valid_keys) if option.valid_keys else sorted(world.item_names))
else:
keys = list(
option.valid_keys
if isinstance(option.valid_keys, collections.abc.Sequence)
else sorted(option.valid_keys)
)
options[option_name] = {
"default": list(getattr(option, "default", [])),
"valid_keys": keys,
}
elif issubclass(option, Options.NamedRange):
options[option_name] = {
"default": option.default,
"range_start": option.range_start,
"range_end": option.range_end,
"range_names": option.special_range_names,
}
elif issubclass(option, Options.Range):
options[option_name] = {
"default": option.default,
"range_start": option.range_start,
"range_end": option.range_end,
}
else:
options[option_name] = {
"default": option.name_lookup[option.default] if not isinstance(option.default, str) else option.default
}

for preset_name, preset_options in world.web.options_presets.items():
presets[preset_name] = {}
for option_name, value in preset_options.items():
option = world.options_dataclass.type_hints[option_name]
if issubclass(option, Options.VerifyKeys):
presets[preset_name][option_name] = list(value)
elif issubclass(option, Options.Range):
presets[preset_name][option_name] = value
else:
presets[preset_name][option_name] = option.name_lookup[value] if not isinstance(value, str) else value

return json.dumps(keys[start_index:start_index+100])
# return ""
return {"options": options, "presets": presets}


def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]:
Expand Down Expand Up @@ -119,68 +140,11 @@ def filter_rst_to_html(text: str) -> str:
)["body"]


@app.template_test("ordered")
@app.template_filter("ordered")
def test_ordered(obj):
return isinstance(obj, collections.abc.Sequence)


@app.route("/games/<string:game>/option-presets", methods=["GET"])
@cache.cached()
def option_presets(game: str) -> Response:
world = AutoWorldRegister.world_types[game]

presets = {}
for preset_name, preset in world.web.options_presets.items():
presets[preset_name] = {}
for preset_option_name, preset_option in preset.items():
if preset_option == "random":
presets[preset_name][preset_option_name] = preset_option
continue

option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option)
if isinstance(option, Options.NamedRange) and isinstance(preset_option, str):
assert preset_option in option.special_range_names, (
f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. "
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
)

presets[preset_name][preset_option_name] = option.value
elif isinstance(
option,
(
Options.Range,
Options.OptionSet,
Options.OptionList,
Options.ItemDict,
),
):
presets[preset_name][preset_option_name] = option.value
elif isinstance(preset_option, str):
# Ensure the option value is valid for Choice and Toggle options
assert option.name_lookup[option.value] == preset_option, (
f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. "
f"Values must not be resolved to a different option via option.from_text (or an alias)."
)
# Use the name of the option
presets[preset_name][preset_option_name] = option.current_key
else:
# Use the name of the option
presets[preset_name][preset_option_name] = option.current_key

class SetEncoder(json.JSONEncoder):
def default(self, obj):
from collections.abc import Set

if isinstance(obj, Set):
return list(obj)
return json.JSONEncoder.default(self, obj)

json_data = json.dumps(presets, cls=SetEncoder)
response = Response(json_data)
response.headers["Content-Type"] = "application/json"
return response


@app.route("/weighted-options")
def weighted_options_old():
return redirect("games", 301)
Expand Down Expand Up @@ -244,8 +208,7 @@ def generate_weighted_yaml(game: str):
@app.route("/games/<string:game>/player-options")
@cache.cached()
def player_options(game: str):
print("DEBUG: " + str(app.debug))
return render_options_page("options/player.html", game, is_complex=False)
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)


# YAML generator for player-options
Expand Down
1 change: 0 additions & 1 deletion WebHostLib/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ waitress>=3.0.0
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.7.0
Flask-WTF>=1.2.1
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.1; python_version >= '3.9'
markupsafe>=2.1.5
46 changes: 29 additions & 17 deletions WebHostLib/static/assets/options/player.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
let game = '';
let presets = {};
let options = {};

window.addEventListener('load', async () => {
game = document.getElementById('player-options').getAttribute('data-game')
Expand All @@ -9,7 +10,7 @@ window.addEventListener('load', async () => {

// Fetch presets, if available.
try {
await fetchPresets();
await fetchOptions();
} catch (error) {
console.error('Failed to fetch presets.', error);
}
Expand Down Expand Up @@ -205,24 +206,35 @@ const loadSettings = () => {
*
* @returns {Promise<void>}
*/
const fetchPresets = async () => {
const response = await fetch('option-presets');
presets = await response.json();
const presetSelect = document.getElementById('game-options-preset');

const game = document.getElementById('player-options').getAttribute('data-game');
const presetToApply = localStorage.getItem(`${game}-preset`);
const playerName = localStorage.getItem(`${game}-player`);
if (presetToApply) {
localStorage.removeItem(`${game}-preset`);
presetSelect.value = presetToApply;
applyPreset(presetToApply);
const fetchOptions = async () => {
const response = await fetch('options');
const data = await response.json();
options = data.options;
presets = data.presets;
console.log(data);

for (const optionListElement of document.querySelectorAll(".option-list")) {
const option = optionListElement.id.substring(0, optionListElement.id.indexOf("-container"));
options[option].loaded = 0;

loadItems(option, optionListElement, 50);
createListObserver(optionListElement);
}

if (playerName) {
document.getElementById('player-name').value = playerName;
localStorage.removeItem(`${game}-player`);
}
// const presetSelect = document.getElementById('game-options-preset');
// const game = document.getElementById('player-options').getAttribute('data-game');
// const presetToApply = localStorage.getItem(`${game}-preset`);
// const playerName = localStorage.getItem(`${game}-player`);
// // if (presetToApply) {
// // localStorage.removeItem(`${game}-preset`);
// // presetSelect.value = presetToApply;
// // applyPreset(presetToApply);
// // }
//
// if (playerName) {
// document.getElementById('player-name').value = playerName;
// localStorage.removeItem(`${game}-player`);
// }
};

/**
Expand Down
75 changes: 75 additions & 0 deletions WebHostLib/static/assets/options/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Creates an event handler for scroll events to lazy load list elements and save them to memory.
* @param element {HTMLElement}
*/
function createListObserver(element) {
const option = element.id.substring(0, element.id.indexOf("-container"));
const observer = new IntersectionObserver((entries) => {
console.log("Called observer", option)
if (entries[0].intersectionRatio <= 0) {
return;
}

observer.unobserve(document.getElementById(`${option}-eol`));
loadItems(option, element, 50);

const eol = document.getElementById(`${option}-eol`);
if (eol) {
observer.observe(eol);
}
});

const eol = document.getElementById(`${option}-eol`);
if (eol) {
observer.observe(eol);
}
}

function loadItems(option, element, number) {
console.log(option, number, element);
/** @type {number} */
const loaded = options[option]["loaded"];
/** @type {number} */
const length = options[option]["valid_keys"].length;

// All items are already loaded, return.
if (loaded >= length) {
return;
}

// Remove the "load-more" element.
if (element.children.length !== 0) {
element.children[element.children.length - 1].remove();
}

const max = Math.min(loaded + 50, length);

/** @type {string[]} */
const keys = options[option]["valid_keys"].slice(loaded, max);

options[option]["loaded"] = max;
// TODO: If you see this in version control for the PR, reject it and dunk on me to do this properly. I only used
// this for testing and I better not have left it in. -Phar
let html = "";
for (const value of keys) {
html += `
<div class="option-entry">
<input
type="checkbox"
id="${option}-${value}"
name="${option}"
value="${value}"
${options[option]["default"].includes(value) ? "checked" : ""}
>
<label for="${option}-${value}">${value}</label>
</div>
`;
}

if (options[option]["loaded"] < length) {
html += `<div id="${option}-eol">Loading...</div>`;
}

element.innerHTML += html;
console.log(`Loaded: ${options[option]["loaded"]}`, `Children: ${element.children.length}`);
}
6 changes: 6 additions & 0 deletions WebHostLib/static/styles/options/player.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion WebHostLib/static/styles/options/player.css.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions WebHostLib/static/styles/options/shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
}
}


#player-options, #weighted-options {
.alert {
width: calc(100% - 1rem);
Expand Down Expand Up @@ -162,6 +163,13 @@
overflow-y: auto;
resize: vertical;

.loading {
padding: 1rem;
font-size: 1rem;
text-align: center;
align-self: center;
}

.option-divider {
width: 100%;
height: 2px;
Expand Down
Loading

0 comments on commit 1edf9e7

Please sign in to comment.