Compare commits
36 Commits
d48dd04af9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 936cadfea8 | |||
| cb7ec029eb | |||
| af5116d931 | |||
| 8dae4c1b5d | |||
| 8e67e714d0 | |||
| 7c9aaa04ba | |||
| 93ecd4999f | |||
| 0d367e43a6 | |||
| 3afab4c7fa | |||
| 885d2559af | |||
| 8f1e4e60e1 | |||
| a47c2a3886 | |||
| 11d8621f9b | |||
| b683a2721a | |||
| 7a60ca468d | |||
| 1a5380e17e | |||
| 6e92e40f1c | |||
| b4e6b4c296 | |||
| 9700a5dc7f | |||
| 067f0e189d | |||
| 639a8e2fb0 | |||
| 25b03aa105 | |||
| 53cd3852aa | |||
| f67f377be1 | |||
| bfb1b8a21e | |||
| ef459d728a | |||
| 5690bcadf9 | |||
| 8b5b80f7c5 | |||
| 66806ad922 | |||
| b29a61a44b | |||
| a0562330a3 | |||
| df59a2c097 | |||
| 8209f52fa7 | |||
| b029eba456 | |||
| 1b2425a493 | |||
| e66f7e0588 |
@@ -0,0 +1,65 @@
|
||||
name: Deploy website
|
||||
run-name: ${{ gitea.actor }} is deploying update
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
|
||||
set -e
|
||||
|
||||
APP_DIR=/var/www/alfieking.dev
|
||||
RELEASES_DIR=$APP_DIR/releases
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
NEW_RELEASE=$RELEASES_DIR/$TIMESTAMP
|
||||
KEEP_RELEASES=5
|
||||
|
||||
echo "Creating release directory..."
|
||||
mkdir -p $NEW_RELEASE
|
||||
|
||||
echo "Cloning public repository..."
|
||||
git clone --depth 1 -b main \
|
||||
https://git.alfieking.dev/acetheking987/alfieking.dev.git \
|
||||
$NEW_RELEASE
|
||||
|
||||
echo "Installing dependencies..."
|
||||
source $APP_DIR/venv/bin/activate
|
||||
pip install -r $NEW_RELEASE/requirements.txt
|
||||
|
||||
echo "Switching symlink..."
|
||||
ln -sfn $NEW_RELEASE $APP_DIR/current
|
||||
|
||||
echo "Changing ownership..."
|
||||
chown -R www-data:www-data /var/www/alfieking.dev/releases/$TIMESTAMP
|
||||
chown -R www-data:www-data /var/www/alfieking.dev/current
|
||||
|
||||
echo "Restarting Gunicorn (downtime incomming XD)..."
|
||||
systemctl restart alfieking.dev.service
|
||||
|
||||
echo "Cleaning old releases..."
|
||||
CURRENT_TARGET=$(readlink -f $APP_DIR/current)
|
||||
|
||||
cd $RELEASES_DIR
|
||||
for dir in $(ls -dt */ | tail -n +$((KEEP_RELEASES+1))); do
|
||||
FULL_PATH="$RELEASES_DIR/${dir%/}"
|
||||
if [ "$FULL_PATH" != "$CURRENT_TARGET" ]; then
|
||||
rm -rf "$FULL_PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deployment successful."
|
||||
EOF
|
||||
@@ -1,5 +1,3 @@
|
||||
.venv
|
||||
.env
|
||||
db.sqlite
|
||||
flask_session
|
||||
__pycache__
|
||||
.venv
|
||||
@@ -1,25 +0,0 @@
|
||||
FROM python:alpine
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the requirements file into the container
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install the required packages
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install gunicorn
|
||||
|
||||
# Copy the rest of the application code into the container
|
||||
COPY src src
|
||||
COPY templates templates
|
||||
COPY static static
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5000
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=app.py
|
||||
|
||||
# run the application
|
||||
ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.wsgi:app" ]
|
||||
@@ -1,4 +1,7 @@
|
||||
psycopg2-binary
|
||||
python-dotenv
|
||||
flask-session
|
||||
requests
|
||||
flask
|
||||
markdown
|
||||
gunicorn
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
[ ! -f .env ] || export $(grep -v '^#' .env | xargs)
|
||||
|
||||
flask --app src.wsgi --debug run
|
||||
@@ -0,0 +1,16 @@
|
||||
from os import getenv as env
|
||||
import psycopg2, logging
|
||||
|
||||
|
||||
log = logging.getLogger("blog")
|
||||
|
||||
|
||||
log.info("connecting to database")
|
||||
conn = psycopg2.connect(
|
||||
host = env("PG_HOST"),
|
||||
port = env("PG_PORT"),
|
||||
dbname = env("PG_DBNAME"),
|
||||
user= env("PG_USER"),
|
||||
password = env("PG_PASSWORD")
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
@@ -0,0 +1,73 @@
|
||||
# Imports
|
||||
from flask import Blueprint, render_template, abort
|
||||
import os, importlib, logging
|
||||
|
||||
|
||||
# Logging
|
||||
log = logging.getLogger("__main__")
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint('dynamic', __name__)
|
||||
template_folder = "templates"
|
||||
|
||||
|
||||
# helper func
|
||||
def get_path(file) -> str:
|
||||
return os.path.join(template_folder, "pages", file)
|
||||
|
||||
|
||||
# Get all files in folder
|
||||
def ListFiles(path):
|
||||
path = get_path(path)
|
||||
files = []
|
||||
for root, dirs, files_in_dir in os.walk(path):
|
||||
for file in files_in_dir:
|
||||
files.append(os.path.relpath(os.path.join(root, file), path))
|
||||
for dir in dirs:
|
||||
files.append(os.path.relpath(os.path.join(root, dir), path) + '/')
|
||||
return files
|
||||
|
||||
|
||||
# Import handlers
|
||||
handlers_unloaded = os.listdir("src/handlers")
|
||||
handlers_unloaded = [handler for handler in handlers_unloaded if handler.endswith(".py")]
|
||||
handlers = {}
|
||||
|
||||
for handler in handlers_unloaded:
|
||||
try:
|
||||
try: module = importlib.import_module(f'src.handlers.{handler[:-3]}')
|
||||
except ImportError: module = importlib.import_module(f'handlers.{handler[:-3]}')
|
||||
handlers[handler[:-3]] = module.callback
|
||||
log.info(f"Loaded handler {handler}: {module.callback}")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Failed to load handler {handler}: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# Catch-all route for generic pages
|
||||
@bp.route('/<path:filename>')
|
||||
def catch_all(filename):
|
||||
if os.path.exists(get_path(filename)):
|
||||
if os.path.isdir(get_path(filename)):
|
||||
return render_template(
|
||||
'bases/directory.html',
|
||||
directory=filename + "/" if not filename.endswith('/') else filename,
|
||||
pages=ListFiles(filename)
|
||||
)
|
||||
|
||||
if filename.split('.')[-1] in handlers.keys():
|
||||
return handlers[filename.split('.')[-1]](filename)
|
||||
|
||||
return render_template(f'pages/{filename}')
|
||||
|
||||
elif os.path.exists(get_path(filename + '.html')):
|
||||
return render_template(f'pages/{filename}.html')
|
||||
|
||||
else:
|
||||
for ext in handlers.keys():
|
||||
if os.path.exists(get_path(filename + f".{ext}")):
|
||||
return handlers[ext](filename + f".{ext}")
|
||||
|
||||
abort(404, f"'{filename}' not found")
|
||||
@@ -0,0 +1,38 @@
|
||||
from flask import Blueprint, render_template
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
bp = Blueprint("errors", __name__)
|
||||
|
||||
|
||||
@bp.route('/500')
|
||||
@bp.app_errorhandler(500)
|
||||
def internal_server_error(error:HTTPException=None):
|
||||
return render_template('errors/500.html', error=error), 500
|
||||
|
||||
|
||||
@bp.route('/404')
|
||||
@bp.app_errorhandler(404)
|
||||
def not_found(error:HTTPException=None):
|
||||
return render_template('errors/404.html', error=error), 404
|
||||
|
||||
|
||||
@bp.route('/400')
|
||||
@bp.app_errorhandler(400)
|
||||
def bad_request(error:HTTPException=None):
|
||||
return render_template('errors/400.html', error=error), 400
|
||||
|
||||
|
||||
@bp.route('/idk')
|
||||
@bp.app_errorhandler(Exception)
|
||||
def idk(error:HTTPException=None):
|
||||
if not isinstance(error, HTTPException):
|
||||
error = HTTPException("I honestly dont know")
|
||||
error.code = 418
|
||||
|
||||
return render_template(
|
||||
'errors/error.html',
|
||||
code = error.code,
|
||||
description = error.description,
|
||||
err_name = error.name,
|
||||
), error.code
|
||||
@@ -0,0 +1,10 @@
|
||||
from flask import render_template
|
||||
import markdown, os
|
||||
|
||||
def callback(filename):
|
||||
output = markdown.markdown(open(os.path.join("templates", "pages", filename), "r").read())
|
||||
return render_template(
|
||||
f'bases/md.html',
|
||||
title = filename.split("/")[-1],
|
||||
markdown = output
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
# IMPORTS
|
||||
from flask import Flask, render_template, request
|
||||
from os import getenv as env
|
||||
import logging
|
||||
|
||||
try:
|
||||
import src.pg_log as pg_log
|
||||
except ImportError:
|
||||
import pg_log
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# LOGGING
|
||||
pg_log_handler = pg_log.PgLog(
|
||||
host = env("PG_HOST"),
|
||||
port = env("PG_PORT"),
|
||||
dbname = env("PG_DBNAME"),
|
||||
user= env("PG_USER"),
|
||||
password = env("PG_PASSWORD")
|
||||
)
|
||||
pg_log_handler.setLevel(logging.DEBUG)
|
||||
|
||||
stream_log_handler = logging.StreamHandler()
|
||||
stream_log_handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s - %(levelname)s: %(message)s")
|
||||
)
|
||||
stream_log_handler.setLevel(logging.INFO)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.addHandler(stream_log_handler)
|
||||
log.addHandler(pg_log_handler)
|
||||
|
||||
werkzeug_logger = logging.getLogger("werkzeug")
|
||||
werkzeug_logger.setLevel(logging.DEBUG)
|
||||
werkzeug_logger.addHandler(pg_log_handler)
|
||||
werkzeug_logger.addHandler(stream_log_handler)
|
||||
|
||||
log.info("Logging initialized.")
|
||||
|
||||
|
||||
try:
|
||||
import src.dynamic as dynamic
|
||||
import src.errors as errors
|
||||
import src.music_metadata as music_metadata
|
||||
except ImportError:
|
||||
import dynamic, errors, music_metadata
|
||||
|
||||
|
||||
# CREATE FLASK APP
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder="../templates",
|
||||
static_folder="../static"
|
||||
)
|
||||
log.info("Flask initialized.")
|
||||
|
||||
|
||||
# BLUEPRINTS
|
||||
app.register_blueprint(errors.bp, url_prefix="/errors")
|
||||
app.register_blueprint(music_metadata.bp, url_prefix="/music")
|
||||
app.register_blueprint(dynamic.bp, url_prefix="/")
|
||||
log.info("Blueprints registered.")
|
||||
|
||||
|
||||
# ROUTES
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.route("/toaster")
|
||||
def toaster():
|
||||
return render_template("toaster.html")
|
||||
|
||||
@app.route("/terminal")
|
||||
def terminal():
|
||||
return render_template("terminal.html")
|
||||
|
||||
|
||||
# DEBUG (DONT RUN LIKE THIS IN PROD)
|
||||
if __name__ == "__main__":
|
||||
log.warning(f"RUNNING IN DEBUG MODE DO NOT USE FOR PRODUCTION!")
|
||||
app.run(debug=True)
|
||||
@@ -0,0 +1,25 @@
|
||||
from flask import Blueprint, request, abort
|
||||
from os import getenv as env
|
||||
from urllib.parse import quote
|
||||
import requests
|
||||
|
||||
bp = Blueprint("music", __name__)
|
||||
|
||||
@bp.route("/metadata")
|
||||
def metadata():
|
||||
if not request.args.get("recording_name"):
|
||||
abort(400, "Recording name is required")
|
||||
if not request.args.get("artist_name"):
|
||||
abort(400, "Artist name is required")
|
||||
|
||||
data = requests.get(
|
||||
f"https://api.listenbrainz.org/1/metadata/lookup/?recording_name={quote(request.args.get('recording_name'))}&artist_name={quote(request.args.get('artist_name'))}&metadata=true&inc=artist+tag+release",
|
||||
headers={
|
||||
"Authorization": f"Token {env("LISTENBRAINZ_TOKEN")}"
|
||||
}
|
||||
)
|
||||
|
||||
if data.status_code == 200:
|
||||
return data.json()
|
||||
else:
|
||||
abort(500, "Failed to retrieve metadata")
|
||||
@@ -0,0 +1,35 @@
|
||||
import psycopg2, logging
|
||||
|
||||
class PgLog(logging.StreamHandler):
|
||||
def __init__(self, host, port, dbname, user, password):
|
||||
super().__init__()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.dbname = dbname
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
self.conn = psycopg2.connect(
|
||||
database = dbname,
|
||||
user = user,
|
||||
password = password,
|
||||
host = host,
|
||||
port = port
|
||||
)
|
||||
|
||||
self.cursor = self.conn.cursor()
|
||||
|
||||
def __del__(self):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
|
||||
def emit(self, record):
|
||||
self.cursor.execute(
|
||||
f"INSERT INTO logs (level, message) VALUES (%s, %s)",
|
||||
(
|
||||
record.levelname,
|
||||
record.getMessage(),
|
||||
)
|
||||
)
|
||||
self.conn.commit()
|
||||
@@ -1,48 +0,0 @@
|
||||
# Imports
|
||||
from flask import Blueprint, render_template
|
||||
from os import getenv as env
|
||||
import logging
|
||||
|
||||
import src.routes.snake as snake
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint(
|
||||
'error_handlers',
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Route for 500 error
|
||||
@bp.route('/500')
|
||||
@bp.app_errorhandler(500)
|
||||
def internal_server_error(error=None):
|
||||
if error is not None:
|
||||
log.error("Internal server error: %s", error)
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
|
||||
# Route for 404 error
|
||||
@bp.route('/404')
|
||||
@bp.app_errorhandler(404)
|
||||
def not_found(error=None):
|
||||
if error is not None:
|
||||
log.warning("Page not found: %s", error)
|
||||
scores = snake.get_leaderboard()
|
||||
token = snake.generate_start_token()
|
||||
return render_template('errors/404.html', scores=scores, token=token), 404 if error is not None else 200
|
||||
|
||||
|
||||
# Route for 400 error
|
||||
@bp.route('/400')
|
||||
@bp.app_errorhandler(400)
|
||||
def bad_request(error=None):
|
||||
if error is not None:
|
||||
log.warning("Bad request: %s", error)
|
||||
return render_template('errors/400.html', error=error), 400
|
||||
@@ -1,44 +0,0 @@
|
||||
# Imports
|
||||
from flask import Blueprint, render_template, request, abort, send_from_directory
|
||||
from os import getenv as env
|
||||
import logging
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint(
|
||||
'generic',
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Route for index page
|
||||
@bp.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
# Route for robots.txt, sitemap.xml, and favicon.ico
|
||||
@bp.route('/robots.txt')
|
||||
@bp.route('/sitemap.xml')
|
||||
@bp.route('/favicon.ico')
|
||||
def web_stuffs():
|
||||
return send_from_directory(
|
||||
env('STATIC_FOLDER', default='../static'),
|
||||
request.path.lstrip('/')
|
||||
)
|
||||
|
||||
|
||||
# catch-all route for any other static pages (only in root path)
|
||||
@bp.route('/<string:filename>')
|
||||
def static_files(filename):
|
||||
try:
|
||||
return render_template(filename if filename.endswith('.html') else filename + '.html')
|
||||
except Exception as e:
|
||||
log.error(f"Error serving static file {filename}: {e}")
|
||||
abort(404)
|
||||
@@ -1,146 +0,0 @@
|
||||
# Imports
|
||||
from flask import Blueprint, abort, request, redirect
|
||||
from os import urandom, getenv as env
|
||||
import src.utils.database as database
|
||||
import src.utils.cap as cap
|
||||
import logging, datetime, threading, time
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint(
|
||||
'snake',
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Create database instance
|
||||
db = database.Database(db_name=env('DB_NAME', default='db.sqlite'))
|
||||
db.execute('CREATE TABLE IF NOT EXISTS snake_scores (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score INTEGER)')
|
||||
db.execute('''CREATE TABLE IF NOT EXISTS snake_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ip TEXT UNIQUE NOT NULL
|
||||
)''')
|
||||
|
||||
# Input validation function
|
||||
def valid_length(value, min_length=1, max_length=100):
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
return min_length <= len(value) <= max_length
|
||||
|
||||
|
||||
def valid_score(score, game_token):
|
||||
start_time = db.execute('SELECT created_at FROM snake_tokens WHERE token = ?', (game_token,)).fetchone()
|
||||
if not start_time:
|
||||
log.error("Game token not found.")
|
||||
return False
|
||||
|
||||
start_time = datetime.datetime.fromisoformat(start_time[0])
|
||||
current_time = datetime.datetime.now()
|
||||
elapsed_time = (current_time - start_time).total_seconds()
|
||||
|
||||
if elapsed_time < score / 10 * 5 + 15: # assuming that each point takes 3 seconds to achieve and 15 seconds to start the game and do captcha
|
||||
log.error("Score is too high for the elapsed time.")
|
||||
return False
|
||||
|
||||
if score <= 0 or score > 10000: # Arbitrary upper limit for scores
|
||||
log.error("Score is out of valid range.")
|
||||
return False
|
||||
|
||||
if score % 10 != 0:
|
||||
log.error("Score is not a multiple of 10.")
|
||||
return False
|
||||
|
||||
# delete the token after score validation
|
||||
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
|
||||
log.info(f"Score {score} validated successfully for token {game_token}.")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Route for score submission
|
||||
@bp.route('/snake/submit', methods=['POST'])
|
||||
def submit_score():
|
||||
name = request.form.get('name')
|
||||
score = request.form.get('score')
|
||||
captcha_token = request.form.get('cap-token')
|
||||
game_token = request.form.get('game_token')
|
||||
|
||||
if not cap.verify_captcha(captcha_token):
|
||||
log.error("Captcha verification failed.")
|
||||
abort(400, "Captcha verification failed")
|
||||
|
||||
if not name or not score or not captcha_token or not game_token:
|
||||
log.error("Name, score, captcha token, or game token is missing.")
|
||||
abort(400, "Missing required fields")
|
||||
|
||||
if not valid_length(name, min_length=3, max_length=15):
|
||||
log.error("Invalid name length.")
|
||||
abort(400, "Name must be between 3 and 15 characters long.")
|
||||
|
||||
if not valid_score(int(score), game_token):
|
||||
log.error("Invalid score.")
|
||||
abort(400, "Score not vilid, so either you are trying to cheat the leaderboard or something is seriously wrong.")
|
||||
|
||||
try:
|
||||
db.execute('INSERT INTO snake_scores (name, score) VALUES (?, ?)', (name, int(score)))
|
||||
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
|
||||
log.info(f"Score submitted: {name} - {score}")
|
||||
return redirect('/404')
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Database error: {e}")
|
||||
abort(500, "Internal server error while submitting score.")
|
||||
|
||||
|
||||
# Generate a unique game token
|
||||
def generate_start_token():
|
||||
"""Generate a unique start token for the game."""
|
||||
token = urandom(16).hex()
|
||||
ip = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||
|
||||
ip_token = db.execute('SELECT token FROM snake_tokens WHERE ip = ?', (ip,)).fetchone()
|
||||
if ip_token:
|
||||
log.info(f"Token already exists for IP: {ip}, reusing token.")
|
||||
return ip_token[0]
|
||||
|
||||
log.info(f"Generated start token: {token}")
|
||||
db.execute('INSERT INTO snake_tokens (token, ip) VALUES (?, ?)', (token, ip))
|
||||
return token
|
||||
|
||||
|
||||
# Get leaderboard scores
|
||||
def get_leaderboard():
|
||||
"""Fetch scores from the leaderboard."""
|
||||
try:
|
||||
scores = db.execute('SELECT name, score FROM snake_scores ORDER BY score DESC').fetchall()
|
||||
leaderboard = [{'position': i + 1, 'name': score[0], 'score': score[1]} for i, score in enumerate(scores)]
|
||||
log.info("Leaderboard fetched successfully.")
|
||||
return leaderboard
|
||||
except Exception as e:
|
||||
log.error(f"Error fetching leaderboard: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Clear all tokens older than 1 hour
|
||||
def clear_old_tokens():
|
||||
while True:
|
||||
try:
|
||||
one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1)
|
||||
db.execute('DELETE FROM snake_tokens WHERE created_at < ?', (one_hour_ago,))
|
||||
log.info("Old tokens cleared.")
|
||||
except Exception as e:
|
||||
log.error(f"Error clearing old tokens: {e}")
|
||||
time.sleep(3600) # Run every hour
|
||||
|
||||
|
||||
# Start the token clearing thread
|
||||
token_thread = threading.Thread(target=clear_old_tokens, daemon=True)
|
||||
token_thread.start()
|
||||
@@ -1,42 +0,0 @@
|
||||
# Imports
|
||||
from os import getenv as env
|
||||
import requests, logging
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Function to verify CAPTCHA response
|
||||
def verify_captcha(token: str) -> bool:
|
||||
"""
|
||||
Verify the CAP response token with the CAP server.
|
||||
|
||||
Args:
|
||||
token (str): The CAP response token to verify.
|
||||
|
||||
Returns:
|
||||
bool: True if the token is valid, False otherwise.
|
||||
"""
|
||||
if not token:
|
||||
return False
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
env('CAP_VERIFY_URL', default='https://<instance_url>/<key_id>/siteverify'),
|
||||
json={
|
||||
'secret': env('CAP_SECRET', default=''),
|
||||
'response': token,
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
if response.status_code != 200:
|
||||
log.error("CAPTCHA verification failed with status code: %s", response.status_code)
|
||||
return False
|
||||
return response.json().get('success', False)
|
||||
|
||||
except Exception as e:
|
||||
log.error("Error verifying CAPTCHA: %s", e)
|
||||
return False
|
||||
@@ -1,18 +0,0 @@
|
||||
# Imports
|
||||
import sqlite3
|
||||
|
||||
# Database class
|
||||
class Database:
|
||||
def __init__(self, db_name='db.sqlite'):
|
||||
self.connection = sqlite3.connect(db_name, check_same_thread=False)
|
||||
self.cursor = self.connection.cursor()
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is None:
|
||||
params = []
|
||||
self.cursor.execute(query, params)
|
||||
self.connection.commit()
|
||||
return self.cursor
|
||||
|
||||
def close(self):
|
||||
self.connection.close()
|
||||
@@ -1,58 +0,0 @@
|
||||
# Imports
|
||||
from flask import Flask, request, render_template, send_from_directory, abort
|
||||
from flask_session import Session
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from os import getenv as env, listdir
|
||||
import logging, importlib
|
||||
|
||||
|
||||
# Load env
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Create console log handler
|
||||
console_log = logging.StreamHandler()
|
||||
console_log.setFormatter(logging.Formatter("\033[1;32m%(asctime)s\033[0m - \033[1;34m%(levelname)s\033[0m - \033[1;31m%(name)s\033[0m - %(message)s"))
|
||||
console_log.setLevel(logging.INFO)
|
||||
|
||||
# Create file log handler
|
||||
file_log = logging.FileHandler(env('LOG_FILE', default='app.log'), mode=env('LOG_MODE', default='a'))
|
||||
file_log.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s"))
|
||||
file_log.setLevel(logging.DEBUG)
|
||||
|
||||
# Add handlers to the logger
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.addHandler(console_log)
|
||||
log.addHandler(file_log)
|
||||
log.info("Logging initialized")
|
||||
|
||||
|
||||
# Create flask app
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
# Configure sessions
|
||||
app.config["SESSION_PERMANENT"] = True
|
||||
app.config["SESSION_TYPE"] = "filesystem"
|
||||
Session(app)
|
||||
|
||||
|
||||
# Load routes
|
||||
routes_dir = env('ROUTES_DIR', default='src/routes')
|
||||
for filename in listdir(routes_dir):
|
||||
if not filename.endswith('.py') and filename.startswith('__'):
|
||||
continue
|
||||
|
||||
module_name = f"{routes_dir.replace('/', '.')}.{filename[:-3]}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
if hasattr(module, 'bp'):
|
||||
app.register_blueprint(module.bp)
|
||||
log.info(f"Registered blueprint: {module_name}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to register blueprint {module_name}: {e}")
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -3,7 +3,6 @@ https://cyber.dabamos.de/88x31/anythingbut.gif
|
||||
https://cyber.dabamos.de/88x31/bestdesktop.gif
|
||||
https://kopawz.neocities.org/buttonhoard/buttonsfldr2/diagnosedwithGAY.gif
|
||||
https://kopawz.neocities.org/indexgraphics/buttondecor/ilikecomputer.png
|
||||
https://identity-crisis.carrd.co/assets/images/gallery04/ad4f8d52.jpg?v=4e55d939
|
||||
https://anlucas.neocities.org/best_viewed_with_eyes.gif
|
||||
https://anlucas.neocities.org/html_learn_it_today.gif
|
||||
https://highway.eightyeightthirty.one/badge/5d58a8f32b007d4897db6f862a895a81674fb35f5cc3947fc66595817ca174db
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 965 B |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 743 KiB After Width: | Height: | Size: 743 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 240 KiB |
@@ -1,6 +1,6 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /404.html
|
||||
Disallow: /errors
|
||||
|
||||
Sitemap: https://alfieking.dev/sitemap.xml
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.example.com/index.html</loc>
|
||||
<loc>https://alfieking.dev/</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://alfieking.dev/toaster</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 4.9 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 4.5 MiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 170 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 86 B |
|
After Width: | Height: | Size: 170 B |
|
After Width: | Height: | Size: 88 B |
|
After Width: | Height: | Size: 82 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 116 B |
|
Before Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 830 KiB |
@@ -1,155 +0,0 @@
|
||||
#snakeContainer {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
canvas#snakeCanvas {
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min-content;
|
||||
gap: 1rem;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
form input[type="text"] {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
form button[type="submit"] {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.min-width {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.max-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#snakeLeaderboardSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
max-height: 272px ;
|
||||
}
|
||||
|
||||
#snakeLeaderboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#snakeLeaderboard li {
|
||||
padding: 5px 20px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#snakeLeaderboard li:nth-child(even) {
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
padding: 20px;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
dialog button {
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
dialog h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
dialog p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
form {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.min-width {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.mobileOnly {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pcOnly {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 651px) {
|
||||
.mobileOnly {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pcOnly {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,25 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wdth,wght@125,700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||
@font-face {
|
||||
font-family:"Irken";
|
||||
src:url("/static/content/Irken-Like-AllCaps.woff") format("woff");
|
||||
src:url("/static/content/fonts/Irken-Like-AllCaps.woff") format("woff");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family:"Scratch";
|
||||
src:url("/static/content/fonts/avali-scratch.otf.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family:"Ultrafont";
|
||||
src:url("/static/content/fonts/ultrakill-font.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family:"Ultrafont2";
|
||||
src:url("/static/content/fonts/ultrakill-font-2.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@@ -17,6 +35,13 @@
|
||||
--font-family: "Space Mono", "serif";
|
||||
--title-font: 'Roboto Mono', sans-serif;
|
||||
--irken-font: 'Irken';
|
||||
--scratch-font: 'Scratch';
|
||||
--ultrafont-font: 'Ultrafont';
|
||||
--smileos2-box: url(/static/content/smileos/SmileOS_2_Box.webp) 17 3 3 fill / 51px 9px 9px;
|
||||
--smileos2-font: 'Ultrafont2';
|
||||
--smileos2-emphasis: #FF4343;
|
||||
--line-height: normal;
|
||||
--header-line-height: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -25,6 +50,7 @@ body {
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size);
|
||||
font-family: var(--font-family);
|
||||
line-height: var(--line-height);
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: 1fr;
|
||||
@@ -35,6 +61,10 @@ body {
|
||||
width: 940px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: var(--header-line-height);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
grid-area: sidebar;
|
||||
display: flex;
|
||||
@@ -220,6 +250,15 @@ main section a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.smileos {
|
||||
border-image: var(--smileos2-box);
|
||||
padding: 54px 15px 12px;
|
||||
image-rendering: pixelated;
|
||||
font-size: 1.25rem;
|
||||
font-family: var(--smileos2-font);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#furry {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -227,7 +266,7 @@ main section a {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.1;
|
||||
background-image: url('/static/content/background.png');
|
||||
background-image: url('/static/content/general_images/background.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 0;
|
||||
background-size: cover;
|
||||
@@ -244,6 +283,14 @@ main section a {
|
||||
font-family: var(--irken-font);
|
||||
}
|
||||
|
||||
.scratch {
|
||||
font-family: var(--scratch-font);
|
||||
}
|
||||
|
||||
.ultrafont {
|
||||
font-family: var(--ultrafont-font);
|
||||
}
|
||||
|
||||
#alt-nav {
|
||||
display: none;
|
||||
backdrop-filter: blur(2px) brightness(0.6);
|
||||
@@ -304,6 +351,45 @@ main section a {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
#toaster-wave {
|
||||
position: absolute;
|
||||
left: -145px;
|
||||
top: 200px;
|
||||
}
|
||||
|
||||
.webring {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.webring a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.webring a:hover {
|
||||
font-weight: 900;
|
||||
text-shadow: 0px 0px 10px var(--primary-color-but-slightly-transparent);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1240px) {
|
||||
#toaster-wave {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
@@ -0,0 +1,13 @@
|
||||
section#directory ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section#directory li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
section#directory a:hover {
|
||||
scale: 1.05;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
cap-widget {
|
||||
--cap-background: var(--secondary-background-color-but-slightly-transparent);
|
||||
--cap-border-color: var(--secondary-background-color);
|
||||
--cap-border-radius: 14px;
|
||||
--cap-widget-height: 30px;
|
||||
--cap-widget-width: 230px;
|
||||
--cap-widget-padding: 14px;
|
||||
--cap-gap: 15px;
|
||||
--cap-color: var(--text-color);
|
||||
--cap-checkbox-size: 25px;
|
||||
--cap-checkbox-border: 1px solid var(--secondary-background-color);
|
||||
--cap-checkbox-border-radius: 6px;
|
||||
--cap-checkbox-background: none;
|
||||
--cap-checkbox-margin: 2px;
|
||||
--cap-font: "Space Mono", "serif";
|
||||
--cap-spinner-color: var(--primary-color);
|
||||
--cap-spinner-background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
--cap-spinner-thickness: 5px;
|
||||
--cap-credits-font-size: 12px;
|
||||
--cap-opacity-hover: 0.8;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.gallery {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gallery .gallery-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gallery .gallery-images img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.gallery h2.gallery-date {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
transform: scale(1.1) rotate(-5deg);
|
||||
}
|
||||
|
||||
#spotify {
|
||||
#listenbrainz {
|
||||
background-image: none;
|
||||
backdrop-filter: blur(2px) brightness(0.6);
|
||||
border: var(--secondary-background-color) 2px solid;
|
||||
@@ -58,8 +58,8 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#spotify-title {
|
||||
font-size: 1.5rem;
|
||||
#listenbrainz-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -67,15 +67,29 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
#spotify-artist {
|
||||
#listenbrainz-artist {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
#listenbrainz-live {
|
||||
color: white;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: auto;
|
||||
margin-left: auto;
|
||||
background-color: gray;
|
||||
border-radius: 30px;
|
||||
padding: 2px 6px;
|
||||
font-family: var(--ultrafont-font);
|
||||
}
|
||||
|
||||
#button-collection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
@font-face {
|
||||
font-family:"Ultrafont2";
|
||||
src:url("/static/content/fonts/ultrakill-font-2.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
aspect-ratio: 4/3;
|
||||
padding: 0;
|
||||
font-family: "Ultrafont2";
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.smileos-header {
|
||||
height: 9%;
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Header.webp) 3 fill / 9px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 900;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.smileos-header img {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Content.webp) 1 3 3 fill / 3px 9px 9px;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"logo window"
|
||||
"buttons window";
|
||||
grid-template-columns: 40% 1fr;
|
||||
grid-template-rows: 25% 1fr;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
#smileos-logo {
|
||||
width: 250px;
|
||||
grid-area: logo;
|
||||
margin: auto;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
#terminal-window {
|
||||
height: 100%;
|
||||
grid-area: window;
|
||||
}
|
||||
|
||||
#window-container {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Content.webp) 1 3 3 fill / 3px 9px 9px;
|
||||
height: 91%;
|
||||
padding: 35px;
|
||||
box-sizing: border-box;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
#window-content {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_inset_panel.webp) 1 fill / 3px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ff4343;
|
||||
}
|
||||
|
||||
#terminal-buttons {
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.terminal-button {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Button_transparent.png) 2 / 9px;
|
||||
background: url(/static/content/smileos/SmileOS_2_Button_Background.png);
|
||||
background-repeat: repeat-x;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 15px;
|
||||
height: 75px;
|
||||
width: 270px;
|
||||
image-rendering: pixelated;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-family: "Ultrafont2";
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.terminal-button:hover {
|
||||
filter: contrast(110%);
|
||||
}
|
||||
|
||||
.terminal-button:active {
|
||||
filter: contrast(125%);
|
||||
}
|
||||
|
||||
#smileos-window-content img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#smileos-restart-music {
|
||||
color: #FF4343;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#smileos-restart-music:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ ul#toaster-specs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
ul#toaster-specs li {
|
||||
@@ -33,6 +34,35 @@ ul#toaster-specs li {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fur-meet-gallery-small {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.fur-meet-gallery-small img {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
}
|
||||
|
||||
.fur-meet-gallery-small img:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#fur-meets {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 740px) {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
@@ -46,3 +76,13 @@ ul#toaster-specs li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 690px) {
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
#toaster-wave {
|
||||
display: none;
|
||||
}
|
||||
@@ -72,67 +72,36 @@ typing();
|
||||
|
||||
// HIDDEN STUFF (shh don't tell anyone >:3)
|
||||
|
||||
let last5Chars = "";
|
||||
let last15Chars = "";
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
last5Chars += event.key;
|
||||
if (last5Chars == "furry") {
|
||||
last15Chars += event.key;
|
||||
if (last15Chars.includes("owo")) {
|
||||
console.log("owo, whats this?");
|
||||
document.getElementById('furry').style.display = 'block';
|
||||
last15Chars = "";
|
||||
}
|
||||
if (last5Chars == "irken") {
|
||||
console.log("doom doom doom!");
|
||||
if (last15Chars.includes("doom")) {
|
||||
console.log("Im gonna sing the doom song now");
|
||||
document.querySelector(":root").style.setProperty('--font-family', 'Irken');
|
||||
document.querySelector(":root").style.setProperty('--ultrafont-font', 'Irken');
|
||||
document.querySelector(":root").style.setProperty('--smileos2-font', 'Irken');
|
||||
document.querySelector(":root").style.setProperty('--title-font', '1.5em');
|
||||
document.querySelector(":root").style.setProperty('--line-height', 'default');
|
||||
document.querySelector(":root").style.setProperty('--header-line-height', 'default');
|
||||
last15Chars = "";
|
||||
}
|
||||
while (last5Chars.length >= 5) {
|
||||
last5Chars = last5Chars.slice(1);
|
||||
if (last15Chars.includes("kfc")) {
|
||||
console.log("space chicken");
|
||||
document.querySelector(":root").style.setProperty('--font-family', 'Scratch');
|
||||
document.querySelector(":root").style.setProperty('--title-font', '1em');
|
||||
document.querySelector(":root").style.setProperty('--line-height', '120%');
|
||||
document.querySelector(":root").style.setProperty('--header-line-height', '150%');
|
||||
document.querySelector(":root").style.setProperty('--ultrafont-font', 'Scratch');
|
||||
document.querySelector(":root").style.setProperty('--smileos2-font', 'Scratch');
|
||||
last15Chars = "";
|
||||
}
|
||||
while (last15Chars.length >= 15) {
|
||||
last15Chars = last15Chars.slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Spotify API
|
||||
|
||||
function getSpotify() {
|
||||
fetch('https://api.alfieking.dev/spotify/nowplaying/xz02oolstlvwxqu1pfcua9exz').then(response => {
|
||||
return response.json();
|
||||
}).then(data => {
|
||||
if (data.item == null) {
|
||||
document.getElementById('spotify').style.backgroundImage = "none";
|
||||
document.getElementById('spotify-title').innerHTML = "Spotify is not playing anything";
|
||||
document.getElementById('spotify-artist').innerHTML = ":(";
|
||||
document.getElementById('spotify-link').href = "https://open.spotify.com/";
|
||||
return;
|
||||
}
|
||||
document.getElementById('spotify').style.backgroundImage = "url(" + data.item.album.images[0].url + ")";
|
||||
document.getElementById('spotify-title').innerHTML = data.item.name;
|
||||
document.getElementById('spotify-artist').innerHTML = data.item.artists[0].name;
|
||||
document.getElementById('spotify-link').href = data.item.external_urls.spotify;
|
||||
});
|
||||
}
|
||||
|
||||
if (document.getElementById('spotify')) {
|
||||
getSpotify();
|
||||
setInterval(getSpotify, 15000);
|
||||
}
|
||||
|
||||
// load buttons
|
||||
|
||||
function loadButtons() {
|
||||
fetch('/static/content/buttons.txt').then(response => {
|
||||
return response.text();
|
||||
}).then(data => {
|
||||
container = document.getElementById('button-collection');
|
||||
for (let line of data.split('\n')) {
|
||||
if (line == "") {
|
||||
continue;
|
||||
}
|
||||
let img = document.createElement('img');
|
||||
img.src = line;
|
||||
container.appendChild(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.getElementById('button-collection')) {
|
||||
loadButtons();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// onionring.js is made up of four files - onionring-widget.js (this one!), onionring-index.js, onionring-variables.js and onionring.css
|
||||
// it's licensed under the cooperative non-violent license (CNPL) v4+ (https://thufie.lain.haus/NPL.html)
|
||||
// it was originally made by joey + mord of allium (蒜) house, last updated 2020-11-24
|
||||
|
||||
// === ONIONRING-WIDGET ===
|
||||
//this file contains the code which builds the widget shown on each page in the ring. ctrl+f 'EDIT THIS' if you're looking to change the actual html of the widget
|
||||
|
||||
var tag = document.getElementById(ringID); //find the widget on the page
|
||||
|
||||
thisSite = window.location.href; //get the url of the site we're currently on
|
||||
thisIndex = null;
|
||||
|
||||
// FIX
|
||||
thisSite = thisSite.replace("alfieking.dev", "proot.uk"); // fix domain
|
||||
thisSite = thisSite.replace("http:\/\/127.0.0.1:5000", "https:\/\/proot.uk"); // For debug purposes
|
||||
|
||||
// go through the site list to see if this site is on it and find its position
|
||||
for (i = 0; i < sites.length; i++) {
|
||||
if (thisSite.startsWith(sites[i])) { //we use startswith so this will match any subdirectory, users can put the widget on multiple pages
|
||||
thisIndex = i;
|
||||
break; //when we've found the site, we don't need to search any more, so stop the loop
|
||||
}
|
||||
}
|
||||
|
||||
function randomSite() {
|
||||
otherSites = sites.slice(); //create a copy of the sites list
|
||||
otherSites.splice(thisIndex, 1); //remove the current site so we don't just land on it again
|
||||
randomIndex = Math.floor(Math.random() * otherSites.length);
|
||||
location.href = otherSites[randomIndex];
|
||||
}
|
||||
|
||||
//if we didn't find the site in the list, the widget displays a warning instead
|
||||
if (thisIndex == null) {
|
||||
tag.insertAdjacentHTML('afterbegin', `
|
||||
<table>
|
||||
<tr>
|
||||
<td>This site isn't part of the ${ringName} webring yet 😿</td>
|
||||
</tr>
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
else {
|
||||
//find the 'next' and 'previous' sites in the ring. this code looks complex
|
||||
//because it's using a shorthand version of an if-else statement to make sure
|
||||
//the first and last sites in the ring join together correctly
|
||||
previousIndex = (thisIndex-1 < 0) ? sites.length-1 : thisIndex-1;
|
||||
nextIndex = (thisIndex+1 >= sites.length) ? 0 : thisIndex+1;
|
||||
|
||||
indexText = ""
|
||||
//if you've chosen to include an index, this builds the link to that
|
||||
if (useIndex) {
|
||||
indexText = `<a href='${indexPage}'>index</a> | `;
|
||||
}
|
||||
|
||||
randomText = ""
|
||||
//if you've chosen to include a random button, this builds the link that does that
|
||||
if (useRandom) {
|
||||
randomText = `<a href='javascript:void(0)' onclick='randomSite()'>random</a> | `;
|
||||
}
|
||||
|
||||
tag.classList = "webring";
|
||||
|
||||
//this is the code that displays the widget - EDIT THIS if you want to change the structure
|
||||
tag.insertAdjacentHTML('afterbegin', `
|
||||
<a href='${indexPage}'>${ringName}</a><br>
|
||||
<a href='${sites[previousIndex]}'><--</a>
|
||||
<a href='${sites[nextIndex]}'>--></a>
|
||||
`);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
function loadButtons() {
|
||||
fetch('/static/content/buttons/non_link_buttons.txt').then(response => {
|
||||
return response.text();
|
||||
}).then(data => {
|
||||
container = document.getElementById('button-collection');
|
||||
for (let line of data.split('\n')) {
|
||||
if (line == "") {
|
||||
continue;
|
||||
}
|
||||
let img = document.createElement('img');
|
||||
img.src = line;
|
||||
container.appendChild(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getAlbumArt(songName, artistName) {
|
||||
fetch("/music/metadata?recording_name=" + encodeURIComponent(songName) + "&artist_name=" + encodeURIComponent(artistName)).then(response => response.json()).then(data => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
document.getElementById('listenbrainz').style.backgroundImage = "url(https://coverartarchive.org/release/" + data.release_mbid + "/front)";
|
||||
} else {
|
||||
console.log("No album art found for the given song and artist");
|
||||
document.getElementById('listenbrainz').style.backgroundImage = "url(https://placehold.co/512x512/transparent/777?text=[Insert%20art%20here])";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getMusic() {
|
||||
fetch("https://api.listenbrainz.org/1/user/acetheking987/playing-now").then(response => response.json()).then(data => {
|
||||
if (data.payload.count != 0) {
|
||||
let song_data = data.payload.listens[0].track_metadata;
|
||||
getAlbumArt(song_data.track_name, song_data.artist_name);
|
||||
document.getElementById('listenbrainz-title').innerHTML = song_data.track_name;
|
||||
document.getElementById('listenbrainz-artist').innerHTML = song_data.artist_name;
|
||||
document.getElementById('listenbrainz-link').href = "https://listenbrainz.org/user/acetheking987/";
|
||||
document.getElementById('listenbrainz-live').style.backgroundColor = "red";
|
||||
document.getElementById('listenbrainz-live').innerHTML = "Live";
|
||||
} else {
|
||||
fetch("https://api.listenbrainz.org/1/user/acetheking987/listens?count=1").then(response => response.json()).then(data => {
|
||||
if (data.payload.count != 0) {
|
||||
let song_data = data.payload.listens[0].track_metadata;
|
||||
getAlbumArt(song_data.track_name, song_data.artist_name);
|
||||
document.getElementById('listenbrainz-title').innerHTML = song_data.track_name;
|
||||
document.getElementById('listenbrainz-artist').innerHTML = song_data.artist_name;
|
||||
document.getElementById('listenbrainz-link').href = "https://listenbrainz.org/user/acetheking987/";
|
||||
document.getElementById('listenbrainz-live').style.backgroundColor = "grey";
|
||||
document.getElementById('listenbrainz-live').innerHTML = "Not Live :<";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadButtons();
|
||||
getMusic();
|
||||
setInterval(getMusic, 15000);
|
||||
@@ -0,0 +1,115 @@
|
||||
const tips_of_the_day = [
|
||||
`The Revolver deals <span style="color:var(--smileos2-emphasis)">locational damage</span>.<br>A <span style="color:var(--smileos2-emphasis)">headshot</span> deals <span style="color:var(--smileos2-emphasis)">2x</span> damage and a <span style="color:var(--smileos2-emphasis)">limbshot</span> deals <span style="color:var(--smileos2-emphasis)">1.5x</span> damage.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Dash</span>: Fully invincible, costs stamina<br><span style="color:var(--smileos2-emphasis)">Slide</span>: Greater distance, no invincibility<br><span style="color:var(--smileos2-emphasis)">Jump</span>: Quickly out of melee range, less control`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Shotgun parries</span>: A <span style="color:var(--smileos2-emphasis)">point-blank</span> Shotgun shot to the torso right before an enemy attack lands will deal massive damage.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Attack sound cues</span> allow you to keep track of enemies who are off screen.`,
|
||||
`Some enemies make <span style="color:var(--smileos2-emphasis)">idle sounds</span> to make them easier to track.`,
|
||||
`Use the <span style="color:cyan">ATTRACTOR NAILGUN</span>'s magnets to form concentrated <span style="color:var(--smileos2-emphasis)">orbs</span> of nails that can be <span style="color:var(--smileos2-emphasis)">moved</span> around using the pull force of <span style="color:var(--smileos2-emphasis)">other magnets</span>.`,
|
||||
`Enemies <span style="color:var(--smileos2-emphasis)">can hurt</span> other enemy types. With quick thinking and positioning, powerful enemies can turn into powerful weapons.`,
|
||||
`The <span style="color:cyan">ATTRACTOR NAILGUN</span>'s magnets can be attached to enemies to make mobile targets easy to hit with nails.`,
|
||||
`Enemies <span style="color:var(--smileos2-emphasis)">scream</span> when falling from a <span style="color:var(--smileos2-emphasis)">fatal height</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">SLAM BOUNCING</span>: Jump immediately after landing from a <span style="color:var(--smileos2-emphasis)">ground slam</span> to jump higher. The longer the ground slam fall, the higher the bounce.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">RAILCANNON</span> variations all share the same cooldown. Choose wisely which variation best fits the situation.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">SLIDING</span> will retain previous momentum for a short amount of time. Chaining quick <span style="color:var(--smileos2-emphasis)">SLIDE JUMPS</span> after a <span style="color:var(--smileos2-emphasis)">DASH JUMP</span> will give you incredible sustained speed.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Environmental hazards</span> such as harmful liquids will hurt enemies as well.`,
|
||||
`If you're having trouble keeping up with a tough enemy, <span style="color:var(--smileos2-emphasis)">stand back and observe</span>. Every enemy has its <span style="color:var(--smileos2-emphasis)">tells</span> and <span style="color:var(--smileos2-emphasis)">patterns</span> and learning those can be your key to victory.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">HITSCAN</span> weapons can be used to hit the shotgun's <span style="color:cyan">CORE EJECT</span> in mid-air to increase its damage and blast radius.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">POWER-UPS</span> can be stacked.`,
|
||||
`Hitting an enemy with only the <span style="color:var(--smileos2-emphasis)">edge</span> of an <span style="color:var(--smileos2-emphasis)">explosion</span> will launch them without dealing much damage, making it a risky but effective tool against <span style="color:var(--smileos2-emphasis)">Stalkers</span>.`,
|
||||
`Airborne <span style="color:var(--smileos2-emphasis)">coins</span> can be shot with any <span style="color:var(--smileos2-emphasis)">hitscan</span> weapon.`,
|
||||
`Falling <span style="color:var(--smileos2-emphasis)">underwater</span> is slow, but a <span style="color:var(--smileos2-emphasis)">ground slam</span> allows for a quick return to the ground.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Sliding</span> onto water will cause one to <span style="color:var(--smileos2-emphasis)">skip across</span> its surface.`,
|
||||
`If an enemy is <span style="color:var(--smileos2-emphasis)">blessed</span> by an <span style="color:var(--smileos2-emphasis)">Idol</span>, a direct visible connection is formed between the two, allowing one to easily <span style="color:var(--smileos2-emphasis)">track down</span> and destroy the protector.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Explosions</span> deflect <span style="color:var(--smileos2-emphasis)">projectiles</span>.<br><br>If an explosion is caused right from where an enemy shoots a projectile, it can <span style="color:var(--smileos2-emphasis)">backfire</span> and <span style="color:var(--smileos2-emphasis)">hit them</span> instead.`,
|
||||
`The space of a <span style="color:var(--smileos2-emphasis)">large</span> arena can be used to one's advantage. Using the environment to <span style="color:var(--smileos2-emphasis)">break line-of-sight</span> with enemies allows for some breathing room and time to consider <span style="color:var(--smileos2-emphasis)">target prioritization</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">DON'T</span> WASTE STAMINA! Dashing <span style="color:var(--smileos2-emphasis)">needlessly</span> while fighting very <span style="color:var(--smileos2-emphasis)">aggressive foes</span> will quickly cause one to have none left when it is most needed.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Homing projectiles</span> may be more difficult to dodge, but their tracking and slower speed makes them much <span style="color:var(--smileos2-emphasis)">easier</span> to <span style="color:var(--smileos2-emphasis)">parry</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Mannequins</span> can be hard to hit due to their speed, but they lose air control when <span style="color:var(--smileos2-emphasis)">launched</span> or <span style="color:var(--smileos2-emphasis)">shot down</span> from a surface, making them <span style="color:var(--smileos2-emphasis)">unable to move</span> for a short moment.`,
|
||||
`The <span style="color:red">Knuckleblaster</span>'s <span style="color:var(--smileos2-emphasis)">blast wave</span> is also capable of breaking a <span style="color:var(--smileos2-emphasis)">Gutterman's shield</span>, and is much easier to land in a chaotic scenario.`,
|
||||
`A <span style="color:var(--smileos2-emphasis)">direct hit</span> from the <span style="color:red">Knuckleblaster</span> has extremely powerful <span style="color:var(--smileos2-emphasis)">knockback</span>, making it extremely powerful for launching enemies into <span style="color:var(--smileos2-emphasis)">pits</span> and other <span style="color:var(--smileos2-emphasis)">environmental hazards</span>.`,
|
||||
`Didn't expect me, huh?`,
|
||||
`<span style="color:#FF0078">Magenta</span> colored attacks can <span style="color:var(--smileos2-emphasis)">not</span> be dashed through and must be <span style="color:var(--smileos2-emphasis)">avoided entirely</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Blood Puppets</span> do not grant kills or style points, but their <span style="color:var(--smileos2-emphasis)">blood</span> can still <span style="color:var(--smileos2-emphasis)">heal</span>.`,
|
||||
`When facing down <span style="color:var(--smileos2-emphasis)">a difficult foe</span>, it may be beneficial to first get rid of the <span style="color:var(--smileos2-emphasis)">fodder</span> to reduce distractions.`,
|
||||
`Sometimes it may be more beneficial to <span style="color:var(--smileos2-emphasis)">stay at a distance</span> and wait for an opening <span style="color:var(--smileos2-emphasis)">before</span> getting close.`,
|
||||
`If you're having <span style="color:var(--smileos2-emphasis)">trouble</span> with a specific encounter, take a moment to <span style="color:var(--smileos2-emphasis)">weigh your options</span>. <br> There may be some <span style="color:var(--smileos2-emphasis)">trick</span>, <span style="color:var(--smileos2-emphasis)">tool</span> or <span style="color:var(--smileos2-emphasis)">alternative prioritization</span> that will tip the scales in your favor.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">ENEMY STEP</span>: Jump while in mid-air <span style="color:var(--smileos2-emphasis)">near an enemy</span> to jump off the enemy. This resets the amount of available walljumps without needing to land.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Parries</span> can be used as a powerful healing tool.<br><br>Parrying any enemy projectile or melee attack will <span style="color:red">fully replenish your health</span> up to the hard damage limit.`,
|
||||
`H a v e f u n .`,
|
||||
`If blown too far off the arena, <span style="color:lime">PUMP CHARGE</span>'s overcharge is a good way to get back.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">CHEATS</span> can be enabled in other levels by inputting <span style="color:var(--smileos2-emphasis)">🡡 🡡 🡣 🡣 🡠 🡢 🡠 🡢 B A</span> Enabling cheats will disable ranks`,
|
||||
`You can pick up the cut weapons on the second floor.`
|
||||
]
|
||||
|
||||
|
||||
var click = new Audio('/static/content/smileos/SmileOS2Click.ogx');
|
||||
|
||||
document.getElementById("smileos-about").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "About";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
<span class="red">SmileOS</span>: is the operating system found on most digital interfaces found throughout
|
||||
<span class="red">Hell</span>. <span class="red">SmileOS 1.0</span> is employed on pannels despite its poor user and developer experience to conserve
|
||||
blood after the war, while <span class="red">SmileOS 2.0</span> is used for terminals requireing higher blood consumption.
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("smileos-snake").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Snake";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
<img src="/static/content/smileos/KITR_Build.webp" alt="under construction">
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("smileos-leaderboard").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Leaderboard";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
<img src="/static/content/smileos/KITR_Build.webp" alt="under construction">
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("smileos-tip").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Tip of the Day";
|
||||
document.getElementById("smileos-window-content").innerHTML = tips_of_the_day[Math.floor(Math.random() * tips_of_the_day.length)];
|
||||
});
|
||||
|
||||
async function playAudio(url) {
|
||||
return new Promise((resolve) => {
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener('ended', resolve); // Resolve the promise when audio ends
|
||||
audio.volume = 0.4;
|
||||
audio.play();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
document.getElementById("smileos-window-title").innerHTML = "Tip of the Day";
|
||||
document.getElementById("smileos-window-content").innerHTML = tips_of_the_day[Math.floor(Math.random() * tips_of_the_day.length)];
|
||||
await playAudio('/static/content/smileos/SmileOS2Startup.ogx');
|
||||
|
||||
var music = new Audio('/static/content/smileos/Shopmusic.ogx');
|
||||
music.loop = true;
|
||||
music.volume = 0.2;
|
||||
music.play();
|
||||
});
|
||||
|
||||
document.getElementById("smileos-restart-music").addEventListener("click", async function() {
|
||||
await playAudio('/static/content/smileos/SmileOS2Startup.ogx');
|
||||
|
||||
var music = new Audio('/static/content/smileos/Shopmusic.ogx');
|
||||
music.loop = true;
|
||||
music.volume = 0.2;
|
||||
music.play();
|
||||
});
|
||||
|
||||
|
||||
document.getElementById("secret").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Secret :3";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
Well you found me, here's your <a class="red" href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Prize</a> :3
|
||||
`;
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
const canvas = document.getElementById('snakeCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const gridSize = 20;
|
||||
const tileSize = 100;
|
||||
const snakeSize = 60;
|
||||
const foodSize = 80;
|
||||
canvas.width = gridSize * tileSize;
|
||||
canvas.height = gridSize * tileSize;
|
||||
|
||||
let snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }];
|
||||
let direction = { x: 0, y: 0 };
|
||||
let food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
|
||||
let score = 0;
|
||||
let gameOver = false;
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// draw grid of checkerboard pattern
|
||||
for (let x = 0; x < gridSize; x++) {
|
||||
for (let y = 0; y < gridSize; y++) {
|
||||
ctx.fillStyle = (x + y) % 2 === 0 ?
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--background-color') :
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--secondary-background-color');
|
||||
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw snake
|
||||
snake.forEach(segment => {
|
||||
let nextVec = { x: 0, y: 0 };
|
||||
// if there is a segment after the current segment
|
||||
if (snake.indexOf(segment) < snake.length - 1) {
|
||||
const nextSegment = snake[snake.indexOf(segment) + 1];
|
||||
nextVec.x = nextSegment.x - segment.x;
|
||||
nextVec.y = nextSegment.y - segment.y;
|
||||
}
|
||||
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary-color');
|
||||
if (nextVec.x === 0 && nextVec.y === 0) {
|
||||
ctx.fillRect(
|
||||
segment.x * tileSize + (tileSize - snakeSize) / 2,
|
||||
segment.y * tileSize + (tileSize - snakeSize) / 2,
|
||||
snakeSize,
|
||||
snakeSize
|
||||
);
|
||||
} else if (nextVec.x > 0 || nextVec.y > 0) {
|
||||
ctx.fillRect(
|
||||
segment.x * tileSize + (tileSize - snakeSize) / 2,
|
||||
segment.y * tileSize + (tileSize - snakeSize) / 2,
|
||||
snakeSize + nextVec.x * (tileSize - snakeSize),
|
||||
snakeSize + nextVec.y * (tileSize - snakeSize)
|
||||
);
|
||||
} else {
|
||||
ctx.fillRect(
|
||||
segment.x * tileSize + (tileSize - snakeSize) / 2 + nextVec.x * (tileSize - snakeSize),
|
||||
segment.y * tileSize + (tileSize - snakeSize) / 2 + nextVec.y * (tileSize - snakeSize),
|
||||
snakeSize + Math.abs(nextVec.x) * (tileSize - snakeSize),
|
||||
snakeSize + Math.abs(nextVec.y) * (tileSize - snakeSize)
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Draw food
|
||||
ctx.fillStyle = '#ff4d4d';
|
||||
ctx.fillRect(
|
||||
food.x * tileSize + (tileSize - foodSize) / 2,
|
||||
food.y * tileSize + (tileSize - foodSize) / 2,
|
||||
foodSize,
|
||||
foodSize
|
||||
);
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (gameOver) return;
|
||||
|
||||
// Move snake
|
||||
const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
|
||||
|
||||
// Add new head
|
||||
snake.unshift(head);
|
||||
|
||||
// Check for food collision
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
score += 10; // Increase score
|
||||
placeFood();
|
||||
} else {
|
||||
snake.pop(); // Remove tail if no food eaten
|
||||
}
|
||||
|
||||
// Check for wall collision
|
||||
if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) {
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for self collision
|
||||
for (let i = 1; i < snake.length; i++) {
|
||||
if (head.x === snake[i].x && head.y === snake[i].y) {
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function placeFood() {
|
||||
do {
|
||||
food.x = Math.floor(Math.random() * gridSize);
|
||||
food.y = Math.floor(Math.random() * gridSize);
|
||||
} while (snake.some(segment => segment.x === food.x && segment.y === food.y));
|
||||
}
|
||||
|
||||
function changeDirection(event) {
|
||||
switch (event.key) {
|
||||
case 'w':
|
||||
if (direction.y === 0) direction = { x: 0, y: -1 };
|
||||
break;
|
||||
case 's':
|
||||
if (direction.y === 0) direction = { x: 0, y: 1 };
|
||||
break;
|
||||
case 'a':
|
||||
if (direction.x === 0) direction = { x: -1, y: 0 };
|
||||
break;
|
||||
case 'd':
|
||||
if (direction.x === 0) direction = { x: 1, y: 0 };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Menu to start the game
|
||||
function menu() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--background-color');
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-color');
|
||||
ctx.font = '200px Arial';
|
||||
ctx.fillText('Snake Game', canvas.width / 2, canvas.height / 2);
|
||||
ctx.font = '100px Arial';
|
||||
ctx.fillText('Press W/A/S/D to move', canvas.width / 2, canvas.height / 2 + 100);
|
||||
ctx.fillText('Click to start', canvas.width / 2, canvas.height / 2 + 200);
|
||||
|
||||
canvas.addEventListener('click', startGame);
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
if (!gameOver) {
|
||||
update();
|
||||
draw();
|
||||
setTimeout(gameLoop, 100);
|
||||
} else {
|
||||
document.removeEventListener('keydown', changeDirection);
|
||||
document.getElementById('score').value = score;
|
||||
alert(`Game Over! Your score: ${score}`);
|
||||
menu();
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }];
|
||||
direction = { x: 1, y: 0 };
|
||||
food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
|
||||
score = 0;
|
||||
gameOver = false;
|
||||
canvas.removeEventListener('click', startGame);
|
||||
document.addEventListener('keydown', changeDirection);
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
menu();
|
||||
@@ -0,0 +1,78 @@
|
||||
//YELLOW TERMINAL
|
||||
|
||||
// onionring.js is made up of four files - onionring-widget.js (this one!), onionring-index.js, onionring-variables.js and onionring.css
|
||||
// it's licensed under the cooperative non-violent license (CNPL) v4+ (https://thufie.lain.haus/NPL.html)
|
||||
// it was originally made by joey + mord of allium (蒜) house, last updated 2020-11-24
|
||||
|
||||
// === ONIONRING-WIDGET ===
|
||||
//this file contains the code which builds the widget shown on each page in the ring. ctrl+f 'EDIT THIS' if you're looking to change the actual html of the widget
|
||||
|
||||
var tag = document.getElementById(ringID); //find the widget on the page
|
||||
|
||||
thisSite = window.location.href; //get the url of the site we're currently on
|
||||
thisIndex = null;
|
||||
|
||||
// FIX
|
||||
thisSite = thisSite.replace("alfieking.dev", "proot.uk"); // fix domain
|
||||
thisSite = thisSite.replace("http:\/\/127.0.0.1:5000", "https:\/\/proot.uk"); // For debug purposes
|
||||
|
||||
// go through the site list to see if this site is on it and find its position
|
||||
for (i = 0; i < sites.length; i++) {
|
||||
if (thisSite.startsWith(sites[i])) { //we use startswith so this will match any subdirectory, users can put the widget on multiple pages
|
||||
thisIndex = i;
|
||||
break; //when we've found the site, we don't need to search any more, so stop the loop
|
||||
}
|
||||
}
|
||||
|
||||
function randomUltraRingSite() {
|
||||
otherSites = sites.slice(); //create a copy of the sites list
|
||||
otherSites.splice(thisIndex, 1); //remove the current site so we don't just land on it again
|
||||
randomIndex = Math.floor(Math.random() * otherSites.length);
|
||||
location.href = otherSites[randomIndex];
|
||||
}
|
||||
|
||||
//if we didn't find the site in the list, the widget displays a warning instead
|
||||
if (thisIndex == null) {
|
||||
tag.insertAdjacentHTML('afterbegin', `
|
||||
<table>
|
||||
<tr>
|
||||
<td>This site isn't part of <a href="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING" target='_parent'>${ringName}</a> yet!</td>
|
||||
</tr>
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
else {
|
||||
//find the 'next' and 'previous' sites in the ring. this code looks complex
|
||||
//because it's using a shorthand version of an if-else statement to make sure
|
||||
//the first and last sites in the ring join together correctly
|
||||
previousIndex = (thisIndex-1 < 0) ? sites.length-1 : thisIndex-1;
|
||||
nextIndex = (thisIndex+1 >= sites.length) ? 0 : thisIndex+1;
|
||||
|
||||
indexText = ""
|
||||
//if you've chosen to include an index, this builds the link to that
|
||||
if (useIndex) {
|
||||
indexText = `<a href='${indexPage}' class="textshadow" target='_parent'>[INDEX]</a> | `;
|
||||
}
|
||||
|
||||
randomText = ""
|
||||
//if you've chosen to include a random button, this builds the link that does that
|
||||
if (useRandom) {
|
||||
randomText = `<a href='javascript:void(0)' onclick='randomUltraRingSite()' style="color: red;" class="textshadow" target='_parent'>[RANDOM]</a>`;
|
||||
}
|
||||
|
||||
//this is the code that displays the widget - EDIT THIS if you want to change the structure
|
||||
tag.insertAdjacentHTML('afterbegin', `
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td class='webring-prev'><a href='${sites[previousIndex]}' style="color: red;" class="textshadow" title="[PREV]" target='_parent'><<</a></td>
|
||||
<td class='webring-info'><a href="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING" style="color: red;" target='_parent'><img src="https://jack-dawlia.neocities.org/image/ultraring-yellow-terminal.png" alt="This site is part of ULTRARING" title="ULTRARING" style="max-width:100%"></a></br>
|
||||
<span class='webring-links'>
|
||||
${randomText}
|
||||
${indexText}
|
||||
<td class='webring-next'><a href='${sites[nextIndex]}' style="color: red;" title="[NEXT]" target='_parent'>>></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
`);
|
||||
|
||||
}
|
||||
@@ -3,19 +3,19 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Alfie's basement{% endblock %}</title>
|
||||
<link rel="icon" href="/static/content/icon.webp">
|
||||
<link rel="stylesheet" href="/static/css/base.css">
|
||||
<title>{% block title %}Toasters's basement{% endblock %}</title>
|
||||
<link rel="icon" href="toaster/Toaster_v1.0_sticker.png">
|
||||
<link rel="stylesheet" href="/static/css/bases/base.css">
|
||||
<meta name="description" content="{% block description %}server backend survivor{% endblock %}">
|
||||
<meta name="keywords" content="Alfie King, Alfie, King, Alfieking, Alfieking.dev, dev, server">
|
||||
<meta name="keywords" content="{% block keywords %}Alfie King, Alfie, King, Alfieking, Alfieking.dev, dev, server, developer, backend, selfhost, homelab, Toaster, Toastergen, proot.uk, protogen, furry, fursona{% endblock %}">
|
||||
<meta name="author" content="Alfie King">
|
||||
<meta name="robots" content="all">
|
||||
<meta name="theme-color" content="#63de90" data-react-helmet="true">
|
||||
<meta property="og:site_name" content="Alfieking.dev">
|
||||
<meta property="og:url" content="https://alfieking.dev">
|
||||
<meta property="og:site_name" content="proot.uk">
|
||||
<meta property="og:url" content="https://proot.uk/">
|
||||
<meta property="og:title" content="{{ self.title() }}">
|
||||
<meta property="og:description" content="{{ self.description() }}">
|
||||
<meta property="og:image" content="/static/content/icon.webp">
|
||||
<meta property="og:image" content="toaster/Toaster_v1.0_sticker.png">
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
@@ -28,51 +28,87 @@
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/toaster">Toaster</a></li>
|
||||
<li><a href="/terminal" class="ultrafont">Terminal</a></li>
|
||||
<li><a href="/events">Events</a></li>
|
||||
<li><a href="https://git.alfieking.dev/acetheking987">Gitea</a></li>
|
||||
<li><a href="https://www.last.fm/user/acetheking987">LastFm</a></li>
|
||||
<li><a href="https://listenbrainz.org/user/acetheking987/">Music</a></li>
|
||||
<li><a href="https://prismic.alfieking.dev">Prismic</a></li>
|
||||
<li><a href="https://open.spotify.com/user/xz02oolstlvwxqu1pfcua9exz?si=14396e637f284e03">Spotify</a></li>
|
||||
<li><a href="https://steamcommunity.com/id/acetheking987/">Steam</a></li>
|
||||
<li><a href="https://www.youtube.com/@acetheking987">YouTube</a></li>
|
||||
<li><a href="https://acetheking987.tumblr.com/">Tumblr</a></li>
|
||||
<li><a href="https://www.reddit.com/user/acetheking987">Reddit</a></li>
|
||||
<li><a href="/404">404 >:3</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
<section>
|
||||
<h6 class="irken">heya, try typing "furry" and "irken" into this page!</h6>
|
||||
<h6 class="irken">heya, try typing "owo", "doom" or "<span class="scratch">kfc</span>" into this page!</h6>
|
||||
</section>
|
||||
<section id="buttons">
|
||||
<h1>BUTTONS</h1>
|
||||
<ul>
|
||||
<li><a href="https://dimden.dev/"><img src="https://dimden.dev/services/images/88x31.gif" alt="dimden"></a></li>
|
||||
<li><a href="https://ne0nbandit.neocities.org/"><img src="https://ne0nbandit.github.io/assets/img/btn/mine/nbbanner.png" alt="ne0nbandit"></a></li>
|
||||
<li><a href="https://thinliquid.dev"><img src="https://thinliquid.dev/thnlqd.png" alt="thinliquid"></a></li>
|
||||
<li><a href="https://nekoweb.org/"><img src="https://nekoweb.org/assets/buttons/button6.gif" alt="nekoweb"></a><!-- button by s1nez.nekoweb.org --></li>
|
||||
<li><a href="https://s1nez.nekoweb.org/"><img src="https://s1nez.nekoweb.org/BUTTON.gif" alt="s1nez"></a></li>
|
||||
<li><a href="https://beeps.website"><img src="https://beeps.website/assets/images/88x31-d.gif" alt="beeps"></a></li>
|
||||
<li><a href="https://itsnotstupid.com"><img src="https://itsnotstupid.com/pics/button1.gif" alt="itsnotstupid"></a></li>
|
||||
<li><a href='https://blinkies.cafe'><img src='https://blinkies.cafe/b/display/blinkiesCafe-badge.gif' alt='blinkies.cafe | make your own blinkies!'></a></li>
|
||||
<li><a href="https://eightyeightthirty.one"><img src="https://eightyeightthirty.one/88x31.png" alt="88x31"></a></li>
|
||||
<li><a href="https://neocities.org"><img src="https://cyber.dabamos.de/88x31/neocities-now.gif" alt="neocities"></a></li>
|
||||
<li><a href="https://tuxedodragon.art"><img src="https://tuxedodragon.art/tuxedodragon%2088x31.gif" alt="tuxedodragon"></a></li>
|
||||
<li><a herf="https://hijpixel.nekoweb.org/"><img src="/static/content/buttons/hijpixel.gif" alt="hijpixel"></a></li>
|
||||
<li><a href="https://lensdeer.neocities.org/"><img src="/static/content/buttons/lensdeer.gif" alt="lensdeer"></a></li>
|
||||
<li><a href="https://emmixis.net/"><img src="/static/content/buttons/emmixis.gif" alt="emmixis"></a></li>
|
||||
<li><a href="https://dimden.dev/"><img src="https://dimden.dev/services/images/88x31.gif" alt="dimden"></a></li><!-- hotlink on purpose -->
|
||||
<li><a href="https://ne0nbandit.neocities.org/"><img src="/static/content/buttons/ne0nbandit.png" alt="ne0nbandit"></a></li>
|
||||
<li><a href="https://thinliquid.dev"><img src="/static/content/buttons/thnlqd.png" alt="thinliquid"></a></li>
|
||||
<li><a href="https://nekoweb.org/"><img src="/static/content/buttons/nekoweb.gif" alt="nekoweb"></a><!-- button by s1nez.nekoweb.org --></li>
|
||||
<li><a href="https://s1nez.nekoweb.org/"><img src="/static/content/buttons/s1nez.gif" alt="s1nez"></a></li>
|
||||
<li><a href="https://beeps.website"><img src="/static/content/buttons/beeps.gif" alt="beeps"></a></li>
|
||||
<li><a href="https://itsnotstupid.com"><img src="/static/content/buttons/insia.gif" alt="itsnotstupid"></a></li>
|
||||
<li><a href='https://blinkies.cafe'><img src='/static/content/buttons/blinkiescafe.gif' alt='blinkies.cafe | make your own blinkies!'></a></li>
|
||||
<li><a href="https://eightyeightthirty.one"><img src="/static/content/buttons/8831.png" alt="88x31"></a></li>
|
||||
<li><a href="https://neocities.org"><img src="/static/content/buttons/neocities.gif" alt="neocities"></a></li>
|
||||
<li><a href="https://tuxedodragon.art"><img src="/static/content/buttons/tuxedodragon.gif" alt="tuxedodragon"></a></li>
|
||||
<li><a href="https://beepi.ng"><img src="https://beepi.ng/88x31.png" width="88" height="31" alt="unnick"></a></li>
|
||||
<li><a href="https://sneexy.synth.download"><img src="https://synth.download/assets/buttons/sneexy.svg" alt="Sneexy"></a></li>
|
||||
<li><a href="https://kraafter.me/"><img src="https://kraafter.me/assets/img/button.png" alt="Kraafter.me button" title="kraaftersite"></a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<div id='furnix'>
|
||||
<script type="text/javascript" src="https://tapeykatt.neocities.org/furnix/onionring-variables.js"></script>
|
||||
<script type="text/javascript" src="/static/js/furnix_widget.js"></script>
|
||||
</div>
|
||||
</section>
|
||||
<section class="webring">
|
||||
<a href="https://stellophiliac.github.io/roboring">Roboring</a><br>
|
||||
<a href="https://stellophiliac.github.io/roboring/Toaster/previous"><--</a>
|
||||
<a href="https://stellophiliac.github.io/roboring/Toaster/next">--></a>
|
||||
</section>
|
||||
<section>
|
||||
<div id='ultraring'>
|
||||
<script type="text/javascript" src="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING/onionring-variables.js"></script>
|
||||
<script type="text/javascript" src="/static/js/ultraring_widget.js"></script>
|
||||
<noscript>
|
||||
This site is part of <a href="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING">ULTRARING</a>!
|
||||
</noscript>
|
||||
</div>
|
||||
</section>
|
||||
<section class="webring">
|
||||
<a href="https://keithhacks.cyou/furryring.php">Furryring</a><br>
|
||||
<a href="https://keithhacks.cyou/furryring.php?prev=proot.uk"><--</a>
|
||||
<a href="https://keithhacks.cyou/furryring.php?next=proot.uk">--></a>
|
||||
</section>
|
||||
<iframe width="150" height="450" style="border:none" src="https://dogspit.nekoweb.org/sidelink.html" name="sidelink"></iframe>
|
||||
<section>
|
||||
<pre class="vsmoltext"> |\ _,,,---,,_<br>ZZZzz /,`.-'`' -. ;-;;,_<br> |,4- ) )-,_. ,\ ( `'-'<br> '---''(_/--' `-'\_)</pre>
|
||||
</section>
|
||||
<img src="/static/content/haj.gif" alt="haj" class="haj">
|
||||
<img src="/static/content/general_images/haj.gif" alt="haj" class="haj">
|
||||
</div>
|
||||
<main id="main">
|
||||
<header id="home">
|
||||
<div class="row">
|
||||
<img src="/static/content/icon.webp">
|
||||
<img src="/static/content/toaster/Toaster_v1.0_sticker.png">
|
||||
<div>
|
||||
<h1>Alfie King</h1>
|
||||
<h1>Toaster</h1>
|
||||
<h2 id="typing">server backend survivor</h2>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/toaster" id="toaster-wave">
|
||||
<img src="/static/content/toaster/Toaster_v1.1.png" alt="toaster">
|
||||
</a>
|
||||
</header>
|
||||
<nav id="alt-nav">
|
||||
<ul>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}/{{ directory }} - Toaster's basement{% endblock %}
|
||||
{% block description %}server backend survivor{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/bases/directory.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="directory">
|
||||
<h1>/{{ directory }}</h1>
|
||||
<ul>
|
||||
{% for page in pages %}
|
||||
<li><a href="/{{ directory }}{{ page.split('.')[0] }}">{{ page.split('.')[0] }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Gallery - Toasters's basement{% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||