From 4063deba42519f0a6b12c279d17a90ab361f0b37 Mon Sep 17 00:00:00 2001 From: Unstable Kitsune Date: Wed, 3 Sep 2025 05:23:30 -0400 Subject: [PATCH] feat: implement kList webapp with Flask framework Adds complete Flask-based web application for server listing service, including routing, database integration, Discord OAuth, templates, static assets, and configuration. Handles user authentication, server browsing, and admin dashboard functionality. BROKEN --- src/ver/0.0.3/ecosystem.config.js | 41 + src/ver/0.0.3/requirements.txt | 11 + src/ver/0.0.3/run.py | 6 + src/ver/0.0.3/webapp/__init__.py | 41 + src/ver/0.0.3/webapp/config.py | 15 + src/ver/0.0.3/webapp/database.py | 48 + src/ver/0.0.3/webapp/helper.py | 0 src/ver/0.0.3/webapp/routes.py | 177 ++ src/ver/0.0.3/webapp/static/favicon.ico | Bin 0 -> 16958 bytes src/ver/0.0.3/webapp/static/js/app.js | 133 ++ src/ver/0.0.3/webapp/static/js/particles.js | 1541 +++++++++++++++++ src/ver/0.0.3/webapp/static/styles/base.css | 96 + src/ver/0.0.3/webapp/static/styles/browse.css | 15 + src/ver/0.0.3/webapp/static/styles/error.css | 8 + src/ver/0.0.3/webapp/static/styles/index.css | 35 + src/ver/0.0.3/webapp/static/styles/navbar.css | 65 + .../webapp/static/styles/view_server.css | 48 + src/ver/0.0.3/webapp/templates/3xx.html | 23 + src/ver/0.0.3/webapp/templates/4xx.html | 31 + src/ver/0.0.3/webapp/templates/5xx.html | 31 + src/ver/0.0.3/webapp/templates/admin.html | 28 + src/ver/0.0.3/webapp/templates/base.html | 81 + src/ver/0.0.3/webapp/templates/browse.html | 91 + src/ver/0.0.3/webapp/templates/dashboard.html | 41 + src/ver/0.0.3/webapp/templates/index.html | 94 + .../0.0.3/webapp/templates/view_server.html | 67 + 26 files changed, 2767 insertions(+) create mode 100644 src/ver/0.0.3/ecosystem.config.js create mode 100644 src/ver/0.0.3/requirements.txt create mode 100644 src/ver/0.0.3/run.py create mode 100644 src/ver/0.0.3/webapp/__init__.py create mode 100644 src/ver/0.0.3/webapp/config.py create mode 100644 src/ver/0.0.3/webapp/database.py create mode 100644 src/ver/0.0.3/webapp/helper.py create mode 100644 src/ver/0.0.3/webapp/routes.py create mode 100644 src/ver/0.0.3/webapp/static/favicon.ico create mode 100644 src/ver/0.0.3/webapp/static/js/app.js create mode 100644 src/ver/0.0.3/webapp/static/js/particles.js create mode 100644 src/ver/0.0.3/webapp/static/styles/base.css create mode 100644 src/ver/0.0.3/webapp/static/styles/browse.css create mode 100644 src/ver/0.0.3/webapp/static/styles/error.css create mode 100644 src/ver/0.0.3/webapp/static/styles/index.css create mode 100644 src/ver/0.0.3/webapp/static/styles/navbar.css create mode 100644 src/ver/0.0.3/webapp/static/styles/view_server.css create mode 100644 src/ver/0.0.3/webapp/templates/3xx.html create mode 100644 src/ver/0.0.3/webapp/templates/4xx.html create mode 100644 src/ver/0.0.3/webapp/templates/5xx.html create mode 100644 src/ver/0.0.3/webapp/templates/admin.html create mode 100644 src/ver/0.0.3/webapp/templates/base.html create mode 100644 src/ver/0.0.3/webapp/templates/browse.html create mode 100644 src/ver/0.0.3/webapp/templates/dashboard.html create mode 100644 src/ver/0.0.3/webapp/templates/index.html create mode 100644 src/ver/0.0.3/webapp/templates/view_server.html diff --git a/src/ver/0.0.3/ecosystem.config.js b/src/ver/0.0.3/ecosystem.config.js new file mode 100644 index 0000000..839004a --- /dev/null +++ b/src/ver/0.0.3/ecosystem.config.js @@ -0,0 +1,41 @@ +module.exports = { + apps : [{ + name : "kList", + namespace: "kServers", + version: "0.0.3", + interpreter: "/opt/klist/bin/python", + script: "run.py", + cwd: "/src/kList-v2/", + interpreter_args: "-u", + watch : false, + error_file: "~/.config/kList/log/kList.error", + out_file: "~/.config/kList/log/kList.output", + log_file: "~/.config/kList/log/kList.log", + time: true, + log_date_format : "[ MMM|DD|YYYY - hh:mm a ]", + min_uptime : 300, + listen_timeout : 8000, + kill_timeout : 1600, + wait_ready : true, + post_update: ["echo kList is lanuching!"], + env_production: { + APP_ENV: "production", + POSTGRES_URI: "postgres://USER:PASSWORD@HOST:POST/DATABASE", + BOT_TOKEN: "", + CLIENT_ID: "", + CLIENT_SECRET: "", + SITE_NAME: "kServers", + SECRET_KEY: "", + ADMIN_USER_IDS: [] + }, env_development: { + APP_ENV: "development", + POSTGRES_URI: "postgres://USER:PASSWORD@HOST:POST/DATABASE", + BOT_TOKEN: "", + CLIENT_ID: "", + CLIENT_SECRET: "", + SITE_NAME: "kServers", + SECRET_KEY: "", + ADMIN_USER_IDS: [] + } + }] + } \ No newline at end of file diff --git a/src/ver/0.0.3/requirements.txt b/src/ver/0.0.3/requirements.txt new file mode 100644 index 0000000..12adffb --- /dev/null +++ b/src/ver/0.0.3/requirements.txt @@ -0,0 +1,11 @@ +humanfriendly +Python.js +discord +discord.py +colorama +psycopg2-binary +dnspython +PyYAML +requests +flask +flask_dance \ No newline at end of file diff --git a/src/ver/0.0.3/run.py b/src/ver/0.0.3/run.py new file mode 100644 index 0000000..094bb19 --- /dev/null +++ b/src/ver/0.0.3/run.py @@ -0,0 +1,6 @@ +from webapp import create_app + +app = create_app() + +if __name__ == "__main__": + app.run() \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/__init__.py b/src/ver/0.0.3/webapp/__init__.py new file mode 100644 index 0000000..2edd601 --- /dev/null +++ b/src/ver/0.0.3/webapp/__init__.py @@ -0,0 +1,41 @@ +# kList-v2/webapp/__init__.py +import os +import logging +from rich.logging import RichHandler +from flask import Flask +from flask_dance.contrib.discord import make_discord_blueprint +from werkzeug.middleware.proxy_fix import ProxyFix + +def create_app(): + """Create and configure an instance of the Flask application.""" + app = Flask('kList') + app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) + + from . import config + app.config.from_object(config) + app.secret_key = app.config.get("SECRET_KEY") + + logging.basicConfig(level="INFO", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True, show_path=False)]) + log = logging.getLogger("werkzeug") + log.setLevel(logging.ERROR) + + if app.config.get("DEBUG_MODE"): + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" + + # --- SIMPLIFIED: Import and register the single routes blueprint --- + from . import routes + app.register_blueprint(routes.bp) + + discord_bp = make_discord_blueprint( + client_id=app.config.get("CLIENT_ID"), + client_secret=app.config.get("CLIENT_SECRET"), + redirect_url="/login/discord/authorized", + scope=["identify"] + ) + app.register_blueprint(discord_bp, url_prefix="/login") + + from . import database + with app.app_context(): + database.init_db() + + return app \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/config.py b/src/ver/0.0.3/webapp/config.py new file mode 100644 index 0000000..bc96fac --- /dev/null +++ b/src/ver/0.0.3/webapp/config.py @@ -0,0 +1,15 @@ +# kList-v2/webapp/config.py +import os +import logging + +APP_ENV = os.environ.get('production', 'development') +DEBUG_MODE = (APP_ENV == 'development') +POSTGRES_URI = os.environ.get("POSTGRES_URI") +SECRET_KEY = os.environ.get("SECRET_KEY") +CLIENT_ID = os.environ.get("CLIENT_ID") +CLIENT_SECRET = os.environ.get("CLIENT_SECRET") +BOT_TOKEN = os.environ.get("BOT_TOKEN") +SITE_NAME = os.environ.get("SITE_NAME", "Server List") +ADMIN_USER_IDS_STR = os.environ.get("ADMIN_USER_IDS", "") +ADMIN_USER_IDS = [int(uid.strip()) for uid in ADMIN_USER_IDS_STR.split(',') if uid.strip()] +RED_CONFIG_TABLE = '"kBump.663348082717122527510486617053803107995"."GUILD"' \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/database.py b/src/ver/0.0.3/webapp/database.py new file mode 100644 index 0000000..45460ce --- /dev/null +++ b/src/ver/0.0.3/webapp/database.py @@ -0,0 +1,48 @@ +# kList-v2/webapp/database.py + +import os +import psycopg2 +import psycopg2.extras +from psycopg2 import pool +import logging + +# Get a logger for this module +log = logging.getLogger(__name__) + +# --- CONFIGURATION --- +POSTGRES_URI = os.environ.get("POSTGRES_URI") +if not POSTGRES_URI: + raise ValueError("POSTGRES_URI environment variable not set.") + +db_pool = None + +def init_db(): + """Initializes the database connection pool.""" + global db_pool + # This check prevents re-initializing if it's already done + if db_pool is None: + try: + db_pool = psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=10, dsn=POSTGRES_URI) + log.info("Successfully initialized PostgreSQL connection pool.") + except Exception as e: + log.error(f"FATAL: Could not initialize database pool. Error: {e}") + raise e + +def query_db(query, params=None, fetch="all"): + """A helper function to manage connections and cursors.""" + if db_pool is None: + raise ConnectionError("Database pool is not initialized. Check startup logs for errors.") + + conn = None + try: + conn = db_pool.getconn() + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor: + cursor.execute(query, params) + if fetch == "one": + return cursor.fetchone() + if fetch == "all": + return cursor.fetchall() + conn.commit() + finally: + if conn: + db_pool.putconn(conn) \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/helper.py b/src/ver/0.0.3/webapp/helper.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ver/0.0.3/webapp/routes.py b/src/ver/0.0.3/webapp/routes.py new file mode 100644 index 0000000..34b8e41 --- /dev/null +++ b/src/ver/0.0.3/webapp/routes.py @@ -0,0 +1,177 @@ +# kList-v2/webapp/routes.py +import json +import requests +import logging +from flask import Blueprint, render_template, abort, session, redirect, url_for, request, jsonify +from flask_dance.contrib.discord import discord + +# Use relative imports because this file is part of the 'webapp' package +from .database import query_db +from .config import RED_CONFIG_TABLE, ADMIN_USER_IDS, BOT_TOKEN + +# A single Blueprint for all application routes +bp = Blueprint('main', __name__) +log = logging.getLogger(__name__) + +# --- HELPER FUNCTION --- +DISCORD_API_URL = "https://discord.com/api/v10" + +def get_discord_guild_data(server_id): + """Fetches guild (server) information from the Discord API.""" + headers = {"Authorization": f"Bot {BOT_TOKEN}"} + url = f"{DISCORD_API_URL}/guilds/{server_id}?with_counts=true" + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + response.json() + json_data = response.json() + log.info(f"OUT: {response}") + return json_data + except requests.exceptions.RequestException: + log.error(f"Discord API request failed for server ID {server_id}. Error: {e}") + return None + +# --- API ROUTE --- +@bp.route("/api/v1/server/") +def get_server_info(server_id): + sql_query = f"SELECT json_data FROM {RED_CONFIG_TABLE} WHERE primary_key_1 = %s;" + log.info(f"QUERY: {sql_query}") + raw_data = query_db(sql_query, (server_id,), fetch="one") + if not raw_data: + log.info(f"RAW DATA: {raw_data}") + return jsonify({"error": "Server not found"}), 404 + return jsonify(raw_data['json_data']) + +# --- ERROR HANDLER ROUTES --- +@bp.errorhandler(404) +def page_not_found(err): + return render_template('404.html'), 404 + +@bp.errorhandler(500) +def internal_server_error(err): + return render_template('500.html'), 500 + +@bp.errorhandler(502) +def service_error(err): + return render_template('502.html'), 502 + +@bp.errorhandler(503) +def service_unavailable(err): + return render_template('503.html'), 503 + +# --- WEB PAGE ROUTES --- + +@bp.route("/") +def index(): + sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete';" + servers_from_db = query_db(sql_query) + log.info(f"QUERY: {sql_query}") + servers_for_template = [] + for row in servers_from_db: + server_info = row['json_data'] + server_info['_id'] = row['primary_key_1'] + servers_for_template.append(server_info) + log.info(f"INFO: {server_info}") + return render_template('index.html', servers=servers_for_template) + +@bp.route("/server/") +def view_server(server_name): + original_server_name = server_name.replace('-', ' ') + log.info(f"ORIGINAL NAME: {original_server_name}") + sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'server_name') = %s;" + log.info(f"QUERY: {sql_query}") + raw_data = query_db(sql_query, (original_server_name,), fetch="one") + log.info(f"QUERY: {raw_data}") + if not raw_data: + abort(404) + server_db_data = raw_data['json_data'] + log.info(f"SERVER DATA: {server_db_data}") + server_id = raw_data['primary_key_1'] + server_db_data['server_id'] = server_id + log.info(f"SERVER ID: {server_id}") + discord_guild_data = get_discord_guild_data(server_id) + log.info(f"GUILD DATA: {get_discord_guild_data}") + if not discord_guild_data: + log.info(f"GUILD DATA: {discord_guild_data}") + abort(503) + if server_db_data.get('vanity_code'): + discord_guild_data["invite"] = f"https://discord.gg/{server_db_data.get('vanity_code')}" + log.info(f"GUILD DATA: {discord_guild_data}") + elif server_db_data.get('invite_code'): + discord_guild_data["invite"] = f"https://discord.gg/{server_db_data.get('invite_code')}" + log.info(f"GUILD DATA: {discord_guild_data}") + else: + discord_guild_data["invite"] = None + log.info(f"TEMPLATE: {render_template}") + return render_template('view_server.html', server=server_db_data, discord=discord_guild_data) + +@bp.route('/browse/', defaults={'tag_name': None}) +@bp.route('/browse/') +def browse(tag_name): + """Renders a page for browsing servers by tag.""" + log.info(f"Browse page requested for tag: {tag_name or 'All'}") + + # 1. Get all unique tags from the database + tag_query = f"SELECT json_data->>'tags' AS tags FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete';" + all_servers_tags = query_db(tag_query) + all_tags = set() + for server in all_servers_tags: + tags_string = server.get('tags') + if isinstance(tags_string, str): + tags_list = [tag.strip() for tag in tags_string.split(',') if tag.strip()] + all_tags.update(tags_list) + + sorted_tags = sorted(list(all_tags), key=str.lower) + log.info(f"Found {len(sorted_tags)} unique tags to display.") + + # 2. Filter the servers based on the selected tag + if tag_name: + log.info(f"Filtering servers by tag: '{tag_name}'") + sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete' AND (json_data->>'tags') ILIKE %s;" + filtered_servers_raw = query_db(sql_query, (f'%{tag_name}%',)) + else: + log.info("No tag selected, fetching all servers.") + sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete';" + filtered_servers_raw = query_db(sql_query) + + filtered_servers = [] + for row in filtered_servers_raw: + server_info = row['json_data'] + server_info['_id'] = row['primary_key_1'] + filtered_servers.append(server_info) + + log.info(f"Found {len(filtered_servers)} servers to display.") + return render_template('browse.html', all_tags=sorted_tags, servers=filtered_servers, current_tag=tag_name) + +# --- AUTH & ADMIN ROUTES --- +@bp.route("/auth") +def auth(): + if not discord.authorized: + return redirect(url_for(".index")) # Use dot prefix for Blueprints + user_info = discord.get("/api/users/@me").json() + user_id = int(user_info.get("id")) + if user_id in ADMIN_USER_IDS: + session["is_admin"] = True + session["user_id"] = user_id + session["username"] = user_info.get("username") + else: + session.clear() + return "

Access Denied

You are not authorized to view this page.

", 403 + return redirect(url_for(".dashboard")) + +@bp.route('/admin') +def admin(): + if session.get("is_admin"): + return redirect(url_for(".dashboard")) + return render_template('admin.html') + +@bp.route('/dashboard') +def dashboard(): + if not session.get("is_admin"): + return redirect(url_for(".admin")) + return render_template('dashboard.html') + +@bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('.index')) \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/favicon.ico b/src/ver/0.0.3/webapp/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63f84ecfcf6625848361dd5257342fbd49709a05 GIT binary patch literal 16958 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 literal 0 HcmV?d00001 diff --git a/src/ver/0.0.3/webapp/static/js/app.js b/src/ver/0.0.3/webapp/static/js/app.js new file mode 100644 index 0000000..3e0618c --- /dev/null +++ b/src/ver/0.0.3/webapp/static/js/app.js @@ -0,0 +1,133 @@ +/* ----------------------------------------------- +/* How to use? : Check the GitHub README +/* ----------------------------------------------- */ + +/* To load a config file (particles.json) you need to host this demo (MAMP/WAMP/local)... */ +/* +particlesJS.load('particles-js', 'particles.json', function() { + console.log('particles.js loaded - callback'); +}); +*/ + +/* Otherwise just put the config content (json): */ + +particlesJS('particles-js', + + { + "particles": { + "number": { + "value": 80, + "density": { + "enable": true, + "value_area": 2000 + } + }, + "color": { + "value": "random" + }, + "shape": { + "type": "circle", + "stroke": { + "width": 0, + "color": "#000000" + }, + "polygon": { + "nb_sides": 5 + }, + "image": { + "src": "img/github.svg", + "width": 100, + "height": 100 + } + }, + "opacity": { + "value": 0.5, + "random": false, + "anim": { + "enable": false, + "speed": 1, + "opacity_min": 0.1, + "sync": false + } + }, + "size": { + "value": 5, + "random": true, + "anim": { + "enable": false, + "speed": 40, + "size_min": 0.1, + "sync": false + } + }, + "line_linked": { + "enable": true, + "distance": 150, + "color": "#ffffff", + "opacity": 0.4, + "width": 1 + }, + "move": { + "enable": true, + "speed": 6, + "direction": "none", + "random": false, + "straight": false, + "out_mode": "out", + "attract": { + "enable": false, + "rotateX": 600, + "rotateY": 1200 + } + } + }, + "interactivity": { + "detect_on": "canvas", + "events": { + "onhover": { + "enable": true, + "mode": "grab" + }, + "onclick": { + "enable": true, + "mode": "push" + }, + "resize": true + }, + "modes": { + "grab": { + "distance": 400, + "line_linked": { + "opacity": 1 + } + }, + "bubble": { + "distance": 400, + "size": 40, + "duration": 2, + "opacity": 8, + "speed": 3 + }, + "repulse": { + "distance": 200 + }, + "push": { + "particles_nb": 4 + }, + "remove": { + "particles_nb": 2 + } + } + }, + "retina_detect": true, + "config_demo": { + "hide_card": false, + "background_color": "#b61924", + "background_image": "", + "background_position": "50% 50%", + "background_repeat": "no-repeat", + "background_size": "cover" + } + } + +); \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/js/particles.js b/src/ver/0.0.3/webapp/static/js/particles.js new file mode 100644 index 0000000..325d834 --- /dev/null +++ b/src/ver/0.0.3/webapp/static/js/particles.js @@ -0,0 +1,1541 @@ +/* ----------------------------------------------- +/* Author : Vincent Garreau - vincentgarreau.com +/* MIT license: http://opensource.org/licenses/MIT +/* Demo / Generator : vincentgarreau.com/particles.js +/* GitHub : github.com/VincentGarreau/particles.js +/* How to use? : Check the GitHub README +/* v2.0.0 +/* ----------------------------------------------- */ + +var pJS = function(tag_id, params){ + + var canvas_el = document.querySelector('#'+tag_id+' > .particles-js-canvas-el'); + + /* particles.js variables with default values */ + this.pJS = { + canvas: { + el: canvas_el, + w: canvas_el.offsetWidth, + h: canvas_el.offsetHeight + }, + particles: { + number: { + value: 400, + density: { + enable: true, + value_area: 800 + } + }, + color: { + value: '#fff' + }, + shape: { + type: 'circle', + stroke: { + width: 0, + color: '#ff0000' + }, + polygon: { + nb_sides: 5 + }, + image: { + src: '', + width: 100, + height: 100 + } + }, + opacity: { + value: 1, + random: false, + anim: { + enable: false, + speed: 2, + opacity_min: 0, + sync: false + } + }, + size: { + value: 20, + random: false, + anim: { + enable: false, + speed: 20, + size_min: 0, + sync: false + } + }, + line_linked: { + enable: true, + distance: 100, + color: '#fff', + opacity: 1, + width: 1 + }, + move: { + enable: true, + speed: 2, + direction: 'none', + random: false, + straight: false, + out_mode: 'out', + bounce: false, + attract: { + enable: false, + rotateX: 3000, + rotateY: 3000 + } + }, + array: [] + }, + interactivity: { + detect_on: 'canvas', + events: { + onhover: { + enable: true, + mode: 'grab' + }, + onclick: { + enable: true, + mode: 'push' + }, + resize: true + }, + modes: { + grab:{ + distance: 100, + line_linked:{ + opacity: 1 + } + }, + bubble:{ + distance: 200, + size: 80, + duration: 0.4 + }, + repulse:{ + distance: 200, + duration: 0.4 + }, + push:{ + particles_nb: 4 + }, + remove:{ + particles_nb: 2 + } + }, + mouse:{} + }, + retina_detect: false, + fn: { + interact: {}, + modes: {}, + vendors:{} + }, + tmp: {} + }; + + var pJS = this.pJS; + + /* params settings */ + if(params){ + Object.deepExtend(pJS, params); + } + + pJS.tmp.obj = { + size_value: pJS.particles.size.value, + size_anim_speed: pJS.particles.size.anim.speed, + move_speed: pJS.particles.move.speed, + line_linked_distance: pJS.particles.line_linked.distance, + line_linked_width: pJS.particles.line_linked.width, + mode_grab_distance: pJS.interactivity.modes.grab.distance, + mode_bubble_distance: pJS.interactivity.modes.bubble.distance, + mode_bubble_size: pJS.interactivity.modes.bubble.size, + mode_repulse_distance: pJS.interactivity.modes.repulse.distance + }; + + + pJS.fn.retinaInit = function(){ + + if(pJS.retina_detect && window.devicePixelRatio > 1){ + pJS.canvas.pxratio = window.devicePixelRatio; + pJS.tmp.retina = true; + } + else{ + pJS.canvas.pxratio = 1; + pJS.tmp.retina = false; + } + + pJS.canvas.w = pJS.canvas.el.offsetWidth * pJS.canvas.pxratio; + pJS.canvas.h = pJS.canvas.el.offsetHeight * pJS.canvas.pxratio; + + pJS.particles.size.value = pJS.tmp.obj.size_value * pJS.canvas.pxratio; + pJS.particles.size.anim.speed = pJS.tmp.obj.size_anim_speed * pJS.canvas.pxratio; + pJS.particles.move.speed = pJS.tmp.obj.move_speed * pJS.canvas.pxratio; + pJS.particles.line_linked.distance = pJS.tmp.obj.line_linked_distance * pJS.canvas.pxratio; + pJS.interactivity.modes.grab.distance = pJS.tmp.obj.mode_grab_distance * pJS.canvas.pxratio; + pJS.interactivity.modes.bubble.distance = pJS.tmp.obj.mode_bubble_distance * pJS.canvas.pxratio; + pJS.particles.line_linked.width = pJS.tmp.obj.line_linked_width * pJS.canvas.pxratio; + pJS.interactivity.modes.bubble.size = pJS.tmp.obj.mode_bubble_size * pJS.canvas.pxratio; + pJS.interactivity.modes.repulse.distance = pJS.tmp.obj.mode_repulse_distance * pJS.canvas.pxratio; + + }; + + + + /* ---------- pJS functions - canvas ------------ */ + + pJS.fn.canvasInit = function(){ + pJS.canvas.ctx = pJS.canvas.el.getContext('2d'); + }; + + pJS.fn.canvasSize = function(){ + + pJS.canvas.el.width = pJS.canvas.w; + pJS.canvas.el.height = pJS.canvas.h; + + if(pJS && pJS.interactivity.events.resize){ + + window.addEventListener('resize', function(){ + + pJS.canvas.w = pJS.canvas.el.offsetWidth; + pJS.canvas.h = pJS.canvas.el.offsetHeight; + + /* resize canvas */ + if(pJS.tmp.retina){ + pJS.canvas.w *= pJS.canvas.pxratio; + pJS.canvas.h *= pJS.canvas.pxratio; + } + + pJS.canvas.el.width = pJS.canvas.w; + pJS.canvas.el.height = pJS.canvas.h; + + /* repaint canvas on anim disabled */ + if(!pJS.particles.move.enable){ + pJS.fn.particlesEmpty(); + pJS.fn.particlesCreate(); + pJS.fn.particlesDraw(); + pJS.fn.vendors.densityAutoParticles(); + } + + /* density particles enabled */ + pJS.fn.vendors.densityAutoParticles(); + + }); + + } + + }; + + + pJS.fn.canvasPaint = function(){ + pJS.canvas.ctx.fillRect(0, 0, pJS.canvas.w, pJS.canvas.h); + }; + + pJS.fn.canvasClear = function(){ + pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h); + }; + + + /* --------- pJS functions - particles ----------- */ + + pJS.fn.particle = function(color, opacity, position){ + + /* size */ + this.radius = (pJS.particles.size.random ? Math.random() : 1) * pJS.particles.size.value; + if(pJS.particles.size.anim.enable){ + this.size_status = false; + this.vs = pJS.particles.size.anim.speed / 100; + if(!pJS.particles.size.anim.sync){ + this.vs = this.vs * Math.random(); + } + } + + /* position */ + this.x = position ? position.x : Math.random() * pJS.canvas.w; + this.y = position ? position.y : Math.random() * pJS.canvas.h; + + /* check position - into the canvas */ + if(this.x > pJS.canvas.w - this.radius*2) this.x = this.x - this.radius; + else if(this.x < this.radius*2) this.x = this.x + this.radius; + if(this.y > pJS.canvas.h - this.radius*2) this.y = this.y - this.radius; + else if(this.y < this.radius*2) this.y = this.y + this.radius; + + /* check position - avoid overlap */ + if(pJS.particles.move.bounce){ + pJS.fn.vendors.checkOverlap(this, position); + } + + /* color */ + this.color = {}; + if(typeof(color.value) == 'object'){ + + if(color.value instanceof Array){ + var color_selected = color.value[Math.floor(Math.random() * pJS.particles.color.value.length)]; + this.color.rgb = hexToRgb(color_selected); + }else{ + if(color.value.r != undefined && color.value.g != undefined && color.value.b != undefined){ + this.color.rgb = { + r: color.value.r, + g: color.value.g, + b: color.value.b + } + } + if(color.value.h != undefined && color.value.s != undefined && color.value.l != undefined){ + this.color.hsl = { + h: color.value.h, + s: color.value.s, + l: color.value.l + } + } + } + + } + else if(color.value == 'random'){ + this.color.rgb = { + r: (Math.floor(Math.random() * (255 - 0 + 1)) + 0), + g: (Math.floor(Math.random() * (255 - 0 + 1)) + 0), + b: (Math.floor(Math.random() * (255 - 0 + 1)) + 0) + } + } + else if(typeof(color.value) == 'string'){ + this.color = color; + this.color.rgb = hexToRgb(this.color.value); + } + + /* opacity */ + this.opacity = (pJS.particles.opacity.random ? Math.random() : 1) * pJS.particles.opacity.value; + if(pJS.particles.opacity.anim.enable){ + this.opacity_status = false; + this.vo = pJS.particles.opacity.anim.speed / 100; + if(!pJS.particles.opacity.anim.sync){ + this.vo = this.vo * Math.random(); + } + } + + /* animation - velocity for speed */ + var velbase = {} + switch(pJS.particles.move.direction){ + case 'top': + velbase = { x:0, y:-1 }; + break; + case 'top-right': + velbase = { x:0.5, y:-0.5 }; + break; + case 'right': + velbase = { x:1, y:-0 }; + break; + case 'bottom-right': + velbase = { x:0.5, y:0.5 }; + break; + case 'bottom': + velbase = { x:0, y:1 }; + break; + case 'bottom-left': + velbase = { x:-0.5, y:1 }; + break; + case 'left': + velbase = { x:-1, y:0 }; + break; + case 'top-left': + velbase = { x:-0.5, y:-0.5 }; + break; + default: + velbase = { x:0, y:0 }; + break; + } + + if(pJS.particles.move.straight){ + this.vx = velbase.x; + this.vy = velbase.y; + if(pJS.particles.move.random){ + this.vx = this.vx * (Math.random()); + this.vy = this.vy * (Math.random()); + } + }else{ + this.vx = velbase.x + Math.random()-0.5; + this.vy = velbase.y + Math.random()-0.5; + } + + // var theta = 2.0 * Math.PI * Math.random(); + // this.vx = Math.cos(theta); + // this.vy = Math.sin(theta); + + this.vx_i = this.vx; + this.vy_i = this.vy; + + + + /* if shape is image */ + + var shape_type = pJS.particles.shape.type; + if(typeof(shape_type) == 'object'){ + if(shape_type instanceof Array){ + var shape_selected = shape_type[Math.floor(Math.random() * shape_type.length)]; + this.shape = shape_selected; + } + }else{ + this.shape = shape_type; + } + + if(this.shape == 'image'){ + var sh = pJS.particles.shape; + this.img = { + src: sh.image.src, + ratio: sh.image.width / sh.image.height + } + if(!this.img.ratio) this.img.ratio = 1; + if(pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg != undefined){ + pJS.fn.vendors.createSvgImg(this); + if(pJS.tmp.pushing){ + this.img.loaded = false; + } + } + } + + + + }; + + + pJS.fn.particle.prototype.draw = function() { + + var p = this; + + if(p.radius_bubble != undefined){ + var radius = p.radius_bubble; + }else{ + var radius = p.radius; + } + + if(p.opacity_bubble != undefined){ + var opacity = p.opacity_bubble; + }else{ + var opacity = p.opacity; + } + + if(p.color.rgb){ + var color_value = 'rgba('+p.color.rgb.r+','+p.color.rgb.g+','+p.color.rgb.b+','+opacity+')'; + }else{ + var color_value = 'hsla('+p.color.hsl.h+','+p.color.hsl.s+'%,'+p.color.hsl.l+'%,'+opacity+')'; + } + + pJS.canvas.ctx.fillStyle = color_value; + pJS.canvas.ctx.beginPath(); + + switch(p.shape){ + + case 'circle': + pJS.canvas.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2, false); + break; + + case 'edge': + pJS.canvas.ctx.rect(p.x-radius, p.y-radius, radius*2, radius*2); + break; + + case 'triangle': + pJS.fn.vendors.drawShape(pJS.canvas.ctx, p.x-radius, p.y+radius / 1.66, radius*2, 3, 2); + break; + + case 'polygon': + pJS.fn.vendors.drawShape( + pJS.canvas.ctx, + p.x - radius / (pJS.particles.shape.polygon.nb_sides/3.5), // startX + p.y - radius / (2.66/3.5), // startY + radius*2.66 / (pJS.particles.shape.polygon.nb_sides/3), // sideLength + pJS.particles.shape.polygon.nb_sides, // sideCountNumerator + 1 // sideCountDenominator + ); + break; + + case 'star': + pJS.fn.vendors.drawShape( + pJS.canvas.ctx, + p.x - radius*2 / (pJS.particles.shape.polygon.nb_sides/4), // startX + p.y - radius / (2*2.66/3.5), // startY + radius*2*2.66 / (pJS.particles.shape.polygon.nb_sides/3), // sideLength + pJS.particles.shape.polygon.nb_sides, // sideCountNumerator + 2 // sideCountDenominator + ); + break; + + case 'image': + + function draw(){ + pJS.canvas.ctx.drawImage( + img_obj, + p.x-radius, + p.y-radius, + radius*2, + radius*2 / p.img.ratio + ); + } + + if(pJS.tmp.img_type == 'svg'){ + var img_obj = p.img.obj; + }else{ + var img_obj = pJS.tmp.img_obj; + } + + if(img_obj){ + draw(); + } + + break; + + } + + pJS.canvas.ctx.closePath(); + + if(pJS.particles.shape.stroke.width > 0){ + pJS.canvas.ctx.strokeStyle = pJS.particles.shape.stroke.color; + pJS.canvas.ctx.lineWidth = pJS.particles.shape.stroke.width; + pJS.canvas.ctx.stroke(); + } + + pJS.canvas.ctx.fill(); + + }; + + + pJS.fn.particlesCreate = function(){ + for(var i = 0; i < pJS.particles.number.value; i++) { + pJS.particles.array.push(new pJS.fn.particle(pJS.particles.color, pJS.particles.opacity.value)); + } + }; + + pJS.fn.particlesUpdate = function(){ + + for(var i = 0; i < pJS.particles.array.length; i++){ + + /* the particle */ + var p = pJS.particles.array[i]; + + // var d = ( dx = pJS.interactivity.mouse.click_pos_x - p.x ) * dx + ( dy = pJS.interactivity.mouse.click_pos_y - p.y ) * dy; + // var f = -BANG_SIZE / d; + // if ( d < BANG_SIZE ) { + // var t = Math.atan2( dy, dx ); + // p.vx = f * Math.cos(t); + // p.vy = f * Math.sin(t); + // } + + /* move the particle */ + if(pJS.particles.move.enable){ + var ms = pJS.particles.move.speed/2; + p.x += p.vx * ms; + p.y += p.vy * ms; + } + + /* change opacity status */ + if(pJS.particles.opacity.anim.enable) { + if(p.opacity_status == true) { + if(p.opacity >= pJS.particles.opacity.value) p.opacity_status = false; + p.opacity += p.vo; + }else { + if(p.opacity <= pJS.particles.opacity.anim.opacity_min) p.opacity_status = true; + p.opacity -= p.vo; + } + if(p.opacity < 0) p.opacity = 0; + } + + /* change size */ + if(pJS.particles.size.anim.enable){ + if(p.size_status == true){ + if(p.radius >= pJS.particles.size.value) p.size_status = false; + p.radius += p.vs; + }else{ + if(p.radius <= pJS.particles.size.anim.size_min) p.size_status = true; + p.radius -= p.vs; + } + if(p.radius < 0) p.radius = 0; + } + + /* change particle position if it is out of canvas */ + if(pJS.particles.move.out_mode == 'bounce'){ + var new_pos = { + x_left: p.radius, + x_right: pJS.canvas.w, + y_top: p.radius, + y_bottom: pJS.canvas.h + } + }else{ + var new_pos = { + x_left: -p.radius, + x_right: pJS.canvas.w + p.radius, + y_top: -p.radius, + y_bottom: pJS.canvas.h + p.radius + } + } + + if(p.x - p.radius > pJS.canvas.w){ + p.x = new_pos.x_left; + p.y = Math.random() * pJS.canvas.h; + } + else if(p.x + p.radius < 0){ + p.x = new_pos.x_right; + p.y = Math.random() * pJS.canvas.h; + } + if(p.y - p.radius > pJS.canvas.h){ + p.y = new_pos.y_top; + p.x = Math.random() * pJS.canvas.w; + } + else if(p.y + p.radius < 0){ + p.y = new_pos.y_bottom; + p.x = Math.random() * pJS.canvas.w; + } + + /* out of canvas modes */ + switch(pJS.particles.move.out_mode){ + case 'bounce': + if (p.x + p.radius > pJS.canvas.w) p.vx = -p.vx; + else if (p.x - p.radius < 0) p.vx = -p.vx; + if (p.y + p.radius > pJS.canvas.h) p.vy = -p.vy; + else if (p.y - p.radius < 0) p.vy = -p.vy; + break; + } + + /* events */ + if(isInArray('grab', pJS.interactivity.events.onhover.mode)){ + pJS.fn.modes.grabParticle(p); + } + + if(isInArray('bubble', pJS.interactivity.events.onhover.mode) || isInArray('bubble', pJS.interactivity.events.onclick.mode)){ + pJS.fn.modes.bubbleParticle(p); + } + + if(isInArray('repulse', pJS.interactivity.events.onhover.mode) || isInArray('repulse', pJS.interactivity.events.onclick.mode)){ + pJS.fn.modes.repulseParticle(p); + } + + /* interaction auto between particles */ + if(pJS.particles.line_linked.enable || pJS.particles.move.attract.enable){ + for(var j = i + 1; j < pJS.particles.array.length; j++){ + var p2 = pJS.particles.array[j]; + + /* link particles */ + if(pJS.particles.line_linked.enable){ + pJS.fn.interact.linkParticles(p,p2); + } + + /* attract particles */ + if(pJS.particles.move.attract.enable){ + pJS.fn.interact.attractParticles(p,p2); + } + + /* bounce particles */ + if(pJS.particles.move.bounce){ + pJS.fn.interact.bounceParticles(p,p2); + } + + } + } + + + } + + }; + + pJS.fn.particlesDraw = function(){ + + /* clear canvas */ + pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h); + + /* update each particles param */ + pJS.fn.particlesUpdate(); + + /* draw each particle */ + for(var i = 0; i < pJS.particles.array.length; i++){ + var p = pJS.particles.array[i]; + p.draw(); + } + + }; + + pJS.fn.particlesEmpty = function(){ + pJS.particles.array = []; + }; + + pJS.fn.particlesRefresh = function(){ + + /* init all */ + cancelRequestAnimFrame(pJS.fn.checkAnimFrame); + cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + pJS.tmp.source_svg = undefined; + pJS.tmp.img_obj = undefined; + pJS.tmp.count_svg = 0; + pJS.fn.particlesEmpty(); + pJS.fn.canvasClear(); + + /* restart */ + pJS.fn.vendors.start(); + + }; + + + /* ---------- pJS functions - particles interaction ------------ */ + + pJS.fn.interact.linkParticles = function(p1, p2){ + + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx*dx + dy*dy); + + /* draw a line between p1 and p2 if the distance between them is under the config distance */ + if(dist <= pJS.particles.line_linked.distance){ + + var opacity_line = pJS.particles.line_linked.opacity - (dist / (1/pJS.particles.line_linked.opacity)) / pJS.particles.line_linked.distance; + + if(opacity_line > 0){ + + /* style */ + var color_line = pJS.particles.line_linked.color_rgb_line; + pJS.canvas.ctx.strokeStyle = 'rgba('+color_line.r+','+color_line.g+','+color_line.b+','+opacity_line+')'; + pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width; + //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */ + + /* path */ + pJS.canvas.ctx.beginPath(); + pJS.canvas.ctx.moveTo(p1.x, p1.y); + pJS.canvas.ctx.lineTo(p2.x, p2.y); + pJS.canvas.ctx.stroke(); + pJS.canvas.ctx.closePath(); + + } + + } + + }; + + + pJS.fn.interact.attractParticles = function(p1, p2){ + + /* condensed particles */ + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx*dx + dy*dy); + + if(dist <= pJS.particles.line_linked.distance){ + + var ax = dx/(pJS.particles.move.attract.rotateX*1000), + ay = dy/(pJS.particles.move.attract.rotateY*1000); + + p1.vx -= ax; + p1.vy -= ay; + + p2.vx += ax; + p2.vy += ay; + + } + + + } + + + pJS.fn.interact.bounceParticles = function(p1, p2){ + + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx*dx + dy*dy), + dist_p = p1.radius+p2.radius; + + if(dist <= dist_p){ + p1.vx = -p1.vx; + p1.vy = -p1.vy; + + p2.vx = -p2.vx; + p2.vy = -p2.vy; + } + + } + + + /* ---------- pJS functions - modes events ------------ */ + + pJS.fn.modes.pushParticles = function(nb, pos){ + + pJS.tmp.pushing = true; + + for(var i = 0; i < nb; i++){ + pJS.particles.array.push( + new pJS.fn.particle( + pJS.particles.color, + pJS.particles.opacity.value, + { + 'x': pos ? pos.pos_x : Math.random() * pJS.canvas.w, + 'y': pos ? pos.pos_y : Math.random() * pJS.canvas.h + } + ) + ) + if(i == nb-1){ + if(!pJS.particles.move.enable){ + pJS.fn.particlesDraw(); + } + pJS.tmp.pushing = false; + } + } + + }; + + + pJS.fn.modes.removeParticles = function(nb){ + + pJS.particles.array.splice(0, nb); + if(!pJS.particles.move.enable){ + pJS.fn.particlesDraw(); + } + + }; + + + pJS.fn.modes.bubbleParticle = function(p){ + + /* on hover event */ + if(pJS.interactivity.events.onhover.enable && isInArray('bubble', pJS.interactivity.events.onhover.mode)){ + + var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.pos_y, + dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse), + ratio = 1 - dist_mouse / pJS.interactivity.modes.bubble.distance; + + function init(){ + p.opacity_bubble = p.opacity; + p.radius_bubble = p.radius; + } + + /* mousemove - check ratio */ + if(dist_mouse <= pJS.interactivity.modes.bubble.distance){ + + if(ratio >= 0 && pJS.interactivity.status == 'mousemove'){ + + /* size */ + if(pJS.interactivity.modes.bubble.size != pJS.particles.size.value){ + + if(pJS.interactivity.modes.bubble.size > pJS.particles.size.value){ + var size = p.radius + (pJS.interactivity.modes.bubble.size*ratio); + if(size >= 0){ + p.radius_bubble = size; + } + }else{ + var dif = p.radius - pJS.interactivity.modes.bubble.size, + size = p.radius - (dif*ratio); + if(size > 0){ + p.radius_bubble = size; + }else{ + p.radius_bubble = 0; + } + } + + } + + /* opacity */ + if(pJS.interactivity.modes.bubble.opacity != pJS.particles.opacity.value){ + + if(pJS.interactivity.modes.bubble.opacity > pJS.particles.opacity.value){ + var opacity = pJS.interactivity.modes.bubble.opacity*ratio; + if(opacity > p.opacity && opacity <= pJS.interactivity.modes.bubble.opacity){ + p.opacity_bubble = opacity; + } + }else{ + var opacity = p.opacity - (pJS.particles.opacity.value-pJS.interactivity.modes.bubble.opacity)*ratio; + if(opacity < p.opacity && opacity >= pJS.interactivity.modes.bubble.opacity){ + p.opacity_bubble = opacity; + } + } + + } + + } + + }else{ + init(); + } + + + /* mouseleave */ + if(pJS.interactivity.status == 'mouseleave'){ + init(); + } + + } + + /* on click event */ + else if(pJS.interactivity.events.onclick.enable && isInArray('bubble', pJS.interactivity.events.onclick.mode)){ + + + if(pJS.tmp.bubble_clicking){ + var dx_mouse = p.x - pJS.interactivity.mouse.click_pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.click_pos_y, + dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse), + time_spent = (new Date().getTime() - pJS.interactivity.mouse.click_time)/1000; + + if(time_spent > pJS.interactivity.modes.bubble.duration){ + pJS.tmp.bubble_duration_end = true; + } + + if(time_spent > pJS.interactivity.modes.bubble.duration*2){ + pJS.tmp.bubble_clicking = false; + pJS.tmp.bubble_duration_end = false; + } + } + + + function process(bubble_param, particles_param, p_obj_bubble, p_obj, id){ + + if(bubble_param != particles_param){ + + if(!pJS.tmp.bubble_duration_end){ + if(dist_mouse <= pJS.interactivity.modes.bubble.distance){ + if(p_obj_bubble != undefined) var obj = p_obj_bubble; + else var obj = p_obj; + if(obj != bubble_param){ + var value = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration); + if(id == 'size') p.radius_bubble = value; + if(id == 'opacity') p.opacity_bubble = value; + } + }else{ + if(id == 'size') p.radius_bubble = undefined; + if(id == 'opacity') p.opacity_bubble = undefined; + } + }else{ + if(p_obj_bubble != undefined){ + var value_tmp = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration), + dif = bubble_param - value_tmp; + value = bubble_param + dif; + if(id == 'size') p.radius_bubble = value; + if(id == 'opacity') p.opacity_bubble = value; + } + } + + } + + } + + if(pJS.tmp.bubble_clicking){ + /* size */ + process(pJS.interactivity.modes.bubble.size, pJS.particles.size.value, p.radius_bubble, p.radius, 'size'); + /* opacity */ + process(pJS.interactivity.modes.bubble.opacity, pJS.particles.opacity.value, p.opacity_bubble, p.opacity, 'opacity'); + } + + } + + }; + + + pJS.fn.modes.repulseParticle = function(p){ + + if(pJS.interactivity.events.onhover.enable && isInArray('repulse', pJS.interactivity.events.onhover.mode) && pJS.interactivity.status == 'mousemove') { + + var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.pos_y, + dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse); + + var normVec = {x: dx_mouse/dist_mouse, y: dy_mouse/dist_mouse}, + repulseRadius = pJS.interactivity.modes.repulse.distance, + velocity = 100, + repulseFactor = clamp((1/repulseRadius)*(-1*Math.pow(dist_mouse/repulseRadius,2)+1)*repulseRadius*velocity, 0, 50); + + var pos = { + x: p.x + normVec.x * repulseFactor, + y: p.y + normVec.y * repulseFactor + } + + if(pJS.particles.move.out_mode == 'bounce'){ + if(pos.x - p.radius > 0 && pos.x + p.radius < pJS.canvas.w) p.x = pos.x; + if(pos.y - p.radius > 0 && pos.y + p.radius < pJS.canvas.h) p.y = pos.y; + }else{ + p.x = pos.x; + p.y = pos.y; + } + + } + + + else if(pJS.interactivity.events.onclick.enable && isInArray('repulse', pJS.interactivity.events.onclick.mode)) { + + if(!pJS.tmp.repulse_finish){ + pJS.tmp.repulse_count++; + if(pJS.tmp.repulse_count == pJS.particles.array.length){ + pJS.tmp.repulse_finish = true; + } + } + + if(pJS.tmp.repulse_clicking){ + + var repulseRadius = Math.pow(pJS.interactivity.modes.repulse.distance/6, 3); + + var dx = pJS.interactivity.mouse.click_pos_x - p.x, + dy = pJS.interactivity.mouse.click_pos_y - p.y, + d = dx*dx + dy*dy; + + var force = -repulseRadius / d * 1; + + function process(){ + + var f = Math.atan2(dy,dx); + p.vx = force * Math.cos(f); + p.vy = force * Math.sin(f); + + if(pJS.particles.move.out_mode == 'bounce'){ + var pos = { + x: p.x + p.vx, + y: p.y + p.vy + } + if (pos.x + p.radius > pJS.canvas.w) p.vx = -p.vx; + else if (pos.x - p.radius < 0) p.vx = -p.vx; + if (pos.y + p.radius > pJS.canvas.h) p.vy = -p.vy; + else if (pos.y - p.radius < 0) p.vy = -p.vy; + } + + } + + // default + if(d <= repulseRadius){ + process(); + } + + // bang - slow motion mode + // if(!pJS.tmp.repulse_finish){ + // if(d <= repulseRadius){ + // process(); + // } + // }else{ + // process(); + // } + + + }else{ + + if(pJS.tmp.repulse_clicking == false){ + + p.vx = p.vx_i; + p.vy = p.vy_i; + + } + + } + + } + + } + + + pJS.fn.modes.grabParticle = function(p){ + + if(pJS.interactivity.events.onhover.enable && pJS.interactivity.status == 'mousemove'){ + + var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.pos_y, + dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse); + + /* draw a line between the cursor and the particle if the distance between them is under the config distance */ + if(dist_mouse <= pJS.interactivity.modes.grab.distance){ + + var opacity_line = pJS.interactivity.modes.grab.line_linked.opacity - (dist_mouse / (1/pJS.interactivity.modes.grab.line_linked.opacity)) / pJS.interactivity.modes.grab.distance; + + if(opacity_line > 0){ + + /* style */ + var color_line = pJS.particles.line_linked.color_rgb_line; + pJS.canvas.ctx.strokeStyle = 'rgba('+color_line.r+','+color_line.g+','+color_line.b+','+opacity_line+')'; + pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width; + //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */ + + /* path */ + pJS.canvas.ctx.beginPath(); + pJS.canvas.ctx.moveTo(p.x, p.y); + pJS.canvas.ctx.lineTo(pJS.interactivity.mouse.pos_x, pJS.interactivity.mouse.pos_y); + pJS.canvas.ctx.stroke(); + pJS.canvas.ctx.closePath(); + + } + + } + + } + + }; + + + + /* ---------- pJS functions - vendors ------------ */ + + pJS.fn.vendors.eventsListeners = function(){ + + /* events target element */ + if(pJS.interactivity.detect_on == 'window'){ + pJS.interactivity.el = window; + }else{ + pJS.interactivity.el = pJS.canvas.el; + } + + + /* detect mouse pos - on hover / click event */ + if(pJS.interactivity.events.onhover.enable || pJS.interactivity.events.onclick.enable){ + + /* el on mousemove */ + pJS.interactivity.el.addEventListener('mousemove', function(e){ + + if(pJS.interactivity.el == window){ + var pos_x = e.clientX, + pos_y = e.clientY; + } + else{ + var pos_x = e.offsetX || e.clientX, + pos_y = e.offsetY || e.clientY; + } + + pJS.interactivity.mouse.pos_x = pos_x; + pJS.interactivity.mouse.pos_y = pos_y; + + if(pJS.tmp.retina){ + pJS.interactivity.mouse.pos_x *= pJS.canvas.pxratio; + pJS.interactivity.mouse.pos_y *= pJS.canvas.pxratio; + } + + pJS.interactivity.status = 'mousemove'; + + }); + + /* el on onmouseleave */ + pJS.interactivity.el.addEventListener('mouseleave', function(e){ + + pJS.interactivity.mouse.pos_x = null; + pJS.interactivity.mouse.pos_y = null; + pJS.interactivity.status = 'mouseleave'; + + }); + + } + + /* on click event */ + if(pJS.interactivity.events.onclick.enable){ + + pJS.interactivity.el.addEventListener('click', function(){ + + pJS.interactivity.mouse.click_pos_x = pJS.interactivity.mouse.pos_x; + pJS.interactivity.mouse.click_pos_y = pJS.interactivity.mouse.pos_y; + pJS.interactivity.mouse.click_time = new Date().getTime(); + + if(pJS.interactivity.events.onclick.enable){ + + switch(pJS.interactivity.events.onclick.mode){ + + case 'push': + if(pJS.particles.move.enable){ + pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse); + }else{ + if(pJS.interactivity.modes.push.particles_nb == 1){ + pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse); + } + else if(pJS.interactivity.modes.push.particles_nb > 1){ + pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb); + } + } + break; + + case 'remove': + pJS.fn.modes.removeParticles(pJS.interactivity.modes.remove.particles_nb); + break; + + case 'bubble': + pJS.tmp.bubble_clicking = true; + break; + + case 'repulse': + pJS.tmp.repulse_clicking = true; + pJS.tmp.repulse_count = 0; + pJS.tmp.repulse_finish = false; + setTimeout(function(){ + pJS.tmp.repulse_clicking = false; + }, pJS.interactivity.modes.repulse.duration*1000) + break; + + } + + } + + }); + + } + + + }; + + pJS.fn.vendors.densityAutoParticles = function(){ + + if(pJS.particles.number.density.enable){ + + /* calc area */ + var area = pJS.canvas.el.width * pJS.canvas.el.height / 1000; + if(pJS.tmp.retina){ + area = area/(pJS.canvas.pxratio*2); + } + + /* calc number of particles based on density area */ + var nb_particles = area * pJS.particles.number.value / pJS.particles.number.density.value_area; + + /* add or remove X particles */ + var missing_particles = pJS.particles.array.length - nb_particles; + if(missing_particles < 0) pJS.fn.modes.pushParticles(Math.abs(missing_particles)); + else pJS.fn.modes.removeParticles(missing_particles); + + } + + }; + + + pJS.fn.vendors.checkOverlap = function(p1, position){ + for(var i = 0; i < pJS.particles.array.length; i++){ + var p2 = pJS.particles.array[i]; + + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx*dx + dy*dy); + + if(dist <= p1.radius + p2.radius){ + p1.x = position ? position.x : Math.random() * pJS.canvas.w; + p1.y = position ? position.y : Math.random() * pJS.canvas.h; + pJS.fn.vendors.checkOverlap(p1); + } + } + }; + + + pJS.fn.vendors.createSvgImg = function(p){ + + /* set color to svg element */ + var svgXml = pJS.tmp.source_svg, + rgbHex = /#([0-9A-F]{3,6})/gi, + coloredSvgXml = svgXml.replace(rgbHex, function (m, r, g, b) { + if(p.color.rgb){ + var color_value = 'rgba('+p.color.rgb.r+','+p.color.rgb.g+','+p.color.rgb.b+','+p.opacity+')'; + }else{ + var color_value = 'hsla('+p.color.hsl.h+','+p.color.hsl.s+'%,'+p.color.hsl.l+'%,'+p.opacity+')'; + } + return color_value; + }); + + /* prepare to create img with colored svg */ + var svg = new Blob([coloredSvgXml], {type: 'image/svg+xml;charset=utf-8'}), + DOMURL = window.URL || window.webkitURL || window, + url = DOMURL.createObjectURL(svg); + + /* create particle img obj */ + var img = new Image(); + img.addEventListener('load', function(){ + p.img.obj = img; + p.img.loaded = true; + DOMURL.revokeObjectURL(url); + pJS.tmp.count_svg++; + }); + img.src = url; + + }; + + + pJS.fn.vendors.destroypJS = function(){ + cancelAnimationFrame(pJS.fn.drawAnimFrame); + canvas_el.remove(); + pJSDom = null; + }; + + + pJS.fn.vendors.drawShape = function(c, startX, startY, sideLength, sideCountNumerator, sideCountDenominator){ + + // By Programming Thomas - https://programmingthomas.wordpress.com/2013/04/03/n-sided-shapes/ + var sideCount = sideCountNumerator * sideCountDenominator; + var decimalSides = sideCountNumerator / sideCountDenominator; + var interiorAngleDegrees = (180 * (decimalSides - 2)) / decimalSides; + var interiorAngle = Math.PI - Math.PI * interiorAngleDegrees / 180; // convert to radians + c.save(); + c.beginPath(); + c.translate(startX, startY); + c.moveTo(0,0); + for (var i = 0; i < sideCount; i++) { + c.lineTo(sideLength,0); + c.translate(sideLength,0); + c.rotate(interiorAngle); + } + //c.stroke(); + c.fill(); + c.restore(); + + }; + + pJS.fn.vendors.exportImg = function(){ + window.open(pJS.canvas.el.toDataURL('image/png'), '_blank'); + }; + + + pJS.fn.vendors.loadImg = function(type){ + + pJS.tmp.img_error = undefined; + + if(pJS.particles.shape.image.src != ''){ + + if(type == 'svg'){ + + var xhr = new XMLHttpRequest(); + xhr.open('GET', pJS.particles.shape.image.src); + xhr.onreadystatechange = function (data) { + if(xhr.readyState == 4){ + if(xhr.status == 200){ + pJS.tmp.source_svg = data.currentTarget.response; + pJS.fn.vendors.checkBeforeDraw(); + }else{ + console.log('Error pJS - Image not found'); + pJS.tmp.img_error = true; + } + } + } + xhr.send(); + + }else{ + + var img = new Image(); + img.addEventListener('load', function(){ + pJS.tmp.img_obj = img; + pJS.fn.vendors.checkBeforeDraw(); + }); + img.src = pJS.particles.shape.image.src; + + } + + }else{ + console.log('Error pJS - No image.src'); + pJS.tmp.img_error = true; + } + + }; + + + pJS.fn.vendors.draw = function(){ + + if(pJS.particles.shape.type == 'image'){ + + if(pJS.tmp.img_type == 'svg'){ + + if(pJS.tmp.count_svg >= pJS.particles.number.value){ + pJS.fn.particlesDraw(); + if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + }else{ + //console.log('still loading...'); + if(!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } + + }else{ + + if(pJS.tmp.img_obj != undefined){ + pJS.fn.particlesDraw(); + if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + }else{ + if(!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } + + } + + }else{ + pJS.fn.particlesDraw(); + if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } + + }; + + + pJS.fn.vendors.checkBeforeDraw = function(){ + + // if shape is image + if(pJS.particles.shape.type == 'image'){ + + if(pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg == undefined){ + pJS.tmp.checkAnimFrame = requestAnimFrame(check); + }else{ + //console.log('images loaded! cancel check'); + cancelRequestAnimFrame(pJS.tmp.checkAnimFrame); + if(!pJS.tmp.img_error){ + pJS.fn.vendors.init(); + pJS.fn.vendors.draw(); + } + + } + + }else{ + pJS.fn.vendors.init(); + pJS.fn.vendors.draw(); + } + + }; + + + pJS.fn.vendors.init = function(){ + + /* init canvas + particles */ + pJS.fn.retinaInit(); + pJS.fn.canvasInit(); + pJS.fn.canvasSize(); + pJS.fn.canvasPaint(); + pJS.fn.particlesCreate(); + pJS.fn.vendors.densityAutoParticles(); + + /* particles.line_linked - convert hex colors to rgb */ + pJS.particles.line_linked.color_rgb_line = hexToRgb(pJS.particles.line_linked.color); + + }; + + + pJS.fn.vendors.start = function(){ + + if(isInArray('image', pJS.particles.shape.type)){ + pJS.tmp.img_type = pJS.particles.shape.image.src.substr(pJS.particles.shape.image.src.length - 3); + pJS.fn.vendors.loadImg(pJS.tmp.img_type); + }else{ + pJS.fn.vendors.checkBeforeDraw(); + } + + }; + + + + + /* ---------- pJS - start ------------ */ + + + pJS.fn.vendors.eventsListeners(); + + pJS.fn.vendors.start(); + + + +}; + +/* ---------- global functions - vendors ------------ */ + +Object.deepExtend = function(destination, source) { + for (var property in source) { + if (source[property] && source[property].constructor && + source[property].constructor === Object) { + destination[property] = destination[property] || {}; + arguments.callee(destination[property], source[property]); + } else { + destination[property] = source[property]; + } + } + return destination; +}; + +window.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback){ + window.setTimeout(callback, 1000 / 60); + }; +})(); + +window.cancelRequestAnimFrame = ( function() { + return window.cancelAnimationFrame || + window.webkitCancelRequestAnimationFrame || + window.mozCancelRequestAnimationFrame || + window.oCancelRequestAnimationFrame || + window.msCancelRequestAnimationFrame || + clearTimeout +} )(); + +function hexToRgb(hex){ + // By Tim Down - http://stackoverflow.com/a/5624139/3493650 + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function(m, r, g, b) { + return r + r + g + g + b + b; + }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +}; + +function clamp(number, min, max) { + return Math.min(Math.max(number, min), max); +}; + +function isInArray(value, array) { + return array.indexOf(value) > -1; +} + + +/* ---------- particles.js functions - start ------------ */ + +window.pJSDom = []; + +window.particlesJS = function(tag_id, params){ + + //console.log(params); + + /* no string id? so it's object params, and set the id with default id */ + if(typeof(tag_id) != 'string'){ + params = tag_id; + tag_id = 'particles-js'; + } + + /* no id? set the id to default id */ + if(!tag_id){ + tag_id = 'particles-js'; + } + + /* pJS elements */ + var pJS_tag = document.getElementById(tag_id), + pJS_canvas_class = 'particles-js-canvas-el', + exist_canvas = pJS_tag.getElementsByClassName(pJS_canvas_class); + + /* remove canvas if exists into the pJS target tag */ + if(exist_canvas.length){ + while(exist_canvas.length > 0){ + pJS_tag.removeChild(exist_canvas[0]); + } + } + + /* create canvas element */ + var canvas_el = document.createElement('canvas'); + canvas_el.className = pJS_canvas_class; + + /* set size canvas */ + canvas_el.style.width = "100%"; + canvas_el.style.height = "100%"; + + /* append canvas */ + var canvas = document.getElementById(tag_id).appendChild(canvas_el); + + /* launch particle.js */ + if(canvas != null){ + pJSDom.push(new pJS(tag_id, params)); + } + +}; + +window.particlesJS.load = function(tag_id, path_config_json, callback){ + + /* load json config */ + var xhr = new XMLHttpRequest(); + xhr.open('GET', path_config_json); + xhr.onreadystatechange = function (data) { + if(xhr.readyState == 4){ + if(xhr.status == 200){ + var params = JSON.parse(data.currentTarget.response); + window.particlesJS(tag_id, params); + if(callback) callback(); + }else{ + console.log('Error pJS - XMLHttpRequest status: '+xhr.status); + console.log('Error pJS - File config not found'); + } + } + }; + xhr.send(); + +}; \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/styles/base.css b/src/ver/0.0.3/webapp/static/styles/base.css new file mode 100644 index 0000000..baaaead --- /dev/null +++ b/src/ver/0.0.3/webapp/static/styles/base.css @@ -0,0 +1,96 @@ +html, body { + overflow-y: scroll; + margin: 0; /* This removes the extra space */ +} + +body { + background-color: #000; + margin: 0; + padding: 0; + height: 100%; + width: 100%; +} + +code { + margin: 0; /* This removes the extra space */ +} + +/* This is the updated rule */ +pre { + white-space: pre-wrap; + word-break: break-all; +} + +#particles-js { + z-index: -1; + position: fixed; + height: 100%; + width: 100%; + top: 0; +} + +.invite-button { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +.list-group-item.active { + font-weight: bold; +} + +.list-group-item.active::after { + content: ' «'; + font-size: 1.2em; +} + +@media (max-width: 767px) { + .col-md-3 { + margin-bottom: 2rem; + } +} + +#hidden-admin-button { + position: fixed; + bottom: 10px; + right: 15px; + z-index: 9999; + font-size: 18px; /* Adjust size as needed */ + opacity: 0.0; /* Make it semi-transparent */ + text-decoration: none; + transition: opacity 0.2s ease-in-out; +} + +#hidden-admin-button:hover { + opacity: 1.0; /* Make it fully visible on hover */ + color: rgb(255, 125, 125); +} + +.access-toast { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + padding: 12px 24px; + border-radius: 8px; + background-color: #28a745; /* Bootstrap success green */ + color: white; + font-weight: bold; + z-index: 10000; + opacity: 0; + transition: opacity 0.4s ease-in-out, bottom 0.4s ease-in-out; +} + +.access-toast.show { + opacity: 1; + bottom: 40px; +} + +.center-container { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + text-align: center; + } \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/styles/browse.css b/src/ver/0.0.3/webapp/static/styles/browse.css new file mode 100644 index 0000000..da7d2eb --- /dev/null +++ b/src/ver/0.0.3/webapp/static/styles/browse.css @@ -0,0 +1,15 @@ +.list-group-item.active { + font-weight: bold; +} + +.list-group-item.active::after { + content: ' «'; + font-size: 1.2em; +} + +/* Example: Add a bit more space above the tag list when on mobile devices */ +@media (max-width: 767px) { + .col-md-3 { + margin-bottom: 2rem; + } +} \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/styles/error.css b/src/ver/0.0.3/webapp/static/styles/error.css new file mode 100644 index 0000000..dadf502 --- /dev/null +++ b/src/ver/0.0.3/webapp/static/styles/error.css @@ -0,0 +1,8 @@ +{% extends "base.css" %} +.center-container { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + text-align: center; + } \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/styles/index.css b/src/ver/0.0.3/webapp/static/styles/index.css new file mode 100644 index 0000000..dba0a62 --- /dev/null +++ b/src/ver/0.0.3/webapp/static/styles/index.css @@ -0,0 +1,35 @@ +.servers { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; /* This keeps the cards centered */ + opacity: 0%; + padding: 10px; /* This adds some nice spacing around the cards */ +} + +.card { + background-color: rgba(211, 211, 211, 0.55) !important; +} + +.invite-button { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +.list-group-item.active { + font-weight: bold; +} + +.list-group-item.active::after { + content: ' «'; + font-size: 1.2em; +} + +/* Example: Add a bit more space above the tag list when on mobile devices */ +@media (max-width: 767px) { + .col-md-3 { + margin-bottom: 2rem; + } +} \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/styles/navbar.css b/src/ver/0.0.3/webapp/static/styles/navbar.css new file mode 100644 index 0000000..347d169 --- /dev/null +++ b/src/ver/0.0.3/webapp/static/styles/navbar.css @@ -0,0 +1,65 @@ +.center-container { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + text-align: center; +} +body { + padding-bottom: 20px; +} + +.navbar { + margin-bottom: 20px; +} + + .bd-placeholder-img { + font-size: 1.125rem; + text-anchor: middle; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } + + @media (min-width: 768px) { + .bd-placeholder-img-lg { + font-size: 3.5rem; + } + } + + .b-example-divider { + height: 3rem; + background-color: rgba(0, 0, 0, .1); + border: solid rgba(0, 0, 0, .15); + border-width: 1px 0; + box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); + } + + .b-example-vr { + flex-shrink: 0; + width: 1.5rem; + height: 100vh; + } + + .bi { + vertical-align: -.125em; + fill: currentColor; + } + + .nav-scroller { + position: relative; + z-index: 2; + height: 2.75rem; + overflow-y: hidden; + } + + .nav-scroller .nav { + display: flex; + flex-wrap: nowrap; + padding-bottom: 1rem; + margin-top: -1px; + overflow-x: auto; + text-align: center; + white-space: nowrap; + -webkit-overflow-scrolling: touch; + } \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/static/styles/view_server.css b/src/ver/0.0.3/webapp/static/styles/view_server.css new file mode 100644 index 0000000..9d7f48f --- /dev/null +++ b/src/ver/0.0.3/webapp/static/styles/view_server.css @@ -0,0 +1,48 @@ +.container { + margin-top : 10px; + background: rgba(211, 211, 211, 0.55); +} + +#serverName { + text-align: center; + font-size: 20px; + font-weight: 600; + margin: 10px; +} + +.info { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.info-column { + display: flex; + flex-direction: column; + flex-wrap: wrap; + height: 100%; + margin-left: 30px; + width: 80%; + margin-top: 20px; +} + +.info > img { + height: 200px; + margin-bottom: 10px; +} + +.info-column > a { + height: 100%; +} + +.info-column > .card-body { + margin-top: 10px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + visibility: 55%; +} + +.card-body > a { + margin-right: 10px; +} \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/3xx.html b/src/ver/0.0.3/webapp/templates/3xx.html new file mode 100644 index 0000000..3f18dda --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/3xx.html @@ -0,0 +1,23 @@ + + + + + + + + + {{ error.code }} | {{site_name}} + + +
+
+
+

{{ error.code }}

+ Go Home +
+
+
+ + + + \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/4xx.html b/src/ver/0.0.3/webapp/templates/4xx.html new file mode 100644 index 0000000..b5daaa1 --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/4xx.html @@ -0,0 +1,31 @@ + + + + + + + + + {{ error.code }} | {{site_name}} + + +
+
+
+

{{ error.code }}

+

Page Not Found

+

Sorry, the page you are looking for does not exist.

+ Go Home + +
+

+ Note for server owners: If you're seeing this page for your server, its name may have recently changed on Discord. You can fix the link by running the `{p}sync` command in your server. +

+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/5xx.html b/src/ver/0.0.3/webapp/templates/5xx.html new file mode 100644 index 0000000..b5daaa1 --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/5xx.html @@ -0,0 +1,31 @@ + + + + + + + + + {{ error.code }} | {{site_name}} + + +
+
+
+

{{ error.code }}

+

Page Not Found

+

Sorry, the page you are looking for does not exist.

+ Go Home + +
+

+ Note for server owners: If you're seeing this page for your server, its name may have recently changed on Discord. You can fix the link by running the `{p}sync` command in your server. +

+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/admin.html b/src/ver/0.0.3/webapp/templates/admin.html new file mode 100644 index 0000000..0e01f1b --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/admin.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block title %}{{ site_name }} | Admin Login{% endblock %} + + + + + + + +{% block content %} +
+
+
+
+

Admin Access Required

+

Please log in with an authorized Discord account to continue.

+
+ + Login with Discord + +
+
+
+
+ + +{% endblock %} diff --git a/src/ver/0.0.3/webapp/templates/base.html b/src/ver/0.0.3/webapp/templates/base.html new file mode 100644 index 0000000..da22820 --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/base.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + {% block links %}{% endblock %} + {% block title %}{{ site_name }}{% endblock %} + + + + {% block content %}{% endblock %} + +
+ + + + + + {% block scripts %} + 👀 + + + {% endblock %} + + + \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/browse.html b/src/ver/0.0.3/webapp/templates/browse.html new file mode 100644 index 0000000..771e873 --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/browse.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block title %}Browse Servers | {{ site_name }}{% endblock %} + +{% block links %} + + + + + + + + +{% endblock %} + +{% block content %} +
+
+ ← Back to Home +

Browse Servers

+
+
+
+ + +
+
+
+

Tags

+
+ + All Servers + + {% for tag in all_tags %} + + {{ tag }} + + {% endfor %} +
+
+ +
+
+ {% if servers %} + {% for server in servers %} +
+ +
+
+ {{server['server_name']}} +
+

{{ server['description'] }}

+

+ Tags: {{ server['tags'] }} +

+ View Server +
+
+ {% endfor %} + {% else %} +
+

No servers found with the tag "{{ current_tag }}".

+
+ {% endif %} +
+
+
+
+{% endblock %} + + +{% block scripts %} + + + +{% endblock %} diff --git a/src/ver/0.0.3/webapp/templates/dashboard.html b/src/ver/0.0.3/webapp/templates/dashboard.html new file mode 100644 index 0000000..3fb2794 --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/dashboard.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block title %}Dashboard | {{ site_name }}{% endblock %} + +{% block content %} + + + +
+
+

Admin Dashboard

+
+ Welcome, {{ session.username }}! + Logout +
+
+
+ +
+
+

Overview

+

You are successfully logged in. This is the main content area for your dashboard.

+
+ +
+
+
+ Menu +
+ +
+
+
+
+{% endblock %} + + \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/index.html b/src/ver/0.0.3/webapp/templates/index.html new file mode 100644 index 0000000..dd604e7 --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/index.html @@ -0,0 +1,94 @@ +{% extends base.html %} + + + + + + + + + + + Home | kServers + + + + + +
+ {% for server in servers %} +
+ +
+
+ {{server['server_name']}} +
+

{{ server['description'] }}

+

+ Tags: {{ server['tags'] }} +

+ View Server +
+
+ {% endfor %} +
+ +
+ + + + + + + 👀 + + + + \ No newline at end of file diff --git a/src/ver/0.0.3/webapp/templates/view_server.html b/src/ver/0.0.3/webapp/templates/view_server.html new file mode 100644 index 0000000..54bab9a --- /dev/null +++ b/src/ver/0.0.3/webapp/templates/view_server.html @@ -0,0 +1,67 @@ + + + + + + + + + + {{ discord['name'] }} | {{site_name}} + + + + +
+
+
+ {{ discord.name }} +
+
+
+
+ {% if discord.icon %} + + {% else %} + + {% endif %} + {% if discord.invite %} + Join Server + {% endif %} +
+
+

Description

+

{{ server.description or "No description available." }}

+
+

Tags

+

{{ server.tags or "No tags set." }}

+
+

Server Stats

+

+ Members: {{ human.format_number(discord.approximate_member_count) }} + Roles: {{ discord.roles | length }} + Boosts: {{ discord.premium_subscription_count }} (Tier {{ discord.premium_tier }}) +

+
+
+
+
+
+ +
+ + + + + + + \ No newline at end of file