first commit
This commit is contained in:
		
							
								
								
									
										112
									
								
								server/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								server/app.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| # Standard | ||||
| from typing import Any | ||||
|  | ||||
| # Libraries | ||||
| from flask import Flask, render_template, request, Response  # type: ignore | ||||
| from flask_cors import CORS  # type: ignore | ||||
| from flask_simple_captcha import CAPTCHA  # type: ignore | ||||
| from flask_limiter import Limiter  # type: ignore | ||||
| from flask_limiter.util import get_remote_address  # type: ignore | ||||
|  | ||||
| # Modules | ||||
| import db as DB | ||||
| import config as Config | ||||
| import procs as Procs | ||||
| import bundle as Bundle | ||||
|  | ||||
|  | ||||
| # --- | ||||
|  | ||||
|  | ||||
| app = Flask(__name__) | ||||
|  | ||||
| # Enable all cross origin requests | ||||
| CORS(app) | ||||
|  | ||||
| DB.init_app(app) | ||||
|  | ||||
| simple_captcha = CAPTCHA(config=Config.captcha) | ||||
| app = simple_captcha.init_app(app) | ||||
| rate_limit = f"{Config.rate_limit} per minute" | ||||
| rate_limit_change = f"{Config.rate_limit_change} per minute" | ||||
|  | ||||
| limiter = Limiter( | ||||
|     get_remote_address, | ||||
|     app=app, | ||||
|     default_limits=[rate_limit], | ||||
|     storage_uri="redis://localhost:6379", | ||||
|     strategy="fixed-window", | ||||
| ) | ||||
|  | ||||
| Bundle.bundle_dashboard() | ||||
|  | ||||
|  | ||||
| # --- | ||||
|  | ||||
|  | ||||
| invalid = "Error: Invalid request" | ||||
|  | ||||
|  | ||||
| @app.route("/", methods=["GET"])  # type: ignore | ||||
| @limiter.limit(rate_limit)  # type: ignore | ||||
| def index() -> Any: | ||||
|     return render_template("index.html") | ||||
|  | ||||
|  | ||||
| @app.route("/dashboard", methods=["GET"])  # type: ignore | ||||
| @limiter.limit(rate_limit)  # type: ignore | ||||
| def dashboard() -> Any: | ||||
|     version = Config.manifest.get("version", "0.0.0") | ||||
|     return render_template("dashboard.html", version=version) | ||||
|  | ||||
|  | ||||
| @app.route("/claim", methods=["POST", "GET"])  # type: ignore | ||||
| @limiter.limit(rate_limit)  # type: ignore | ||||
| def claim() -> Any: | ||||
|     if request.method == "POST": | ||||
|         try: | ||||
|             message = Procs.claim_proc(request) | ||||
|             return render_template("message.html", message=message) | ||||
|         except Exception as e: | ||||
|             print(e) | ||||
|             return Response(invalid, mimetype=Config.text_mtype) | ||||
|  | ||||
|     captcha = simple_captcha.create() | ||||
|     return render_template("claim.html", captcha=captcha) | ||||
|  | ||||
|  | ||||
| @app.route("/change", methods=["POST", "GET"])  # type: ignore | ||||
| @limiter.limit(rate_limit_change)  # type: ignore | ||||
| def change() -> Any: | ||||
|     if request.method == "POST": | ||||
|         try: | ||||
|             ans = Procs.change_proc(request) | ||||
|             return Response(ans, mimetype=Config.text_mtype) | ||||
|         except Exception as e: | ||||
|             print(e) | ||||
|             return Response(invalid, mimetype=Config.text_mtype) | ||||
|  | ||||
|     return render_template("change.html") | ||||
|  | ||||
|  | ||||
| @app.route("/<curl>", methods=["GET"])  # type: ignore | ||||
| @limiter.limit(rate_limit)  # type: ignore | ||||
| def get_curl(curl) -> Any: | ||||
|     try: | ||||
|         ans = Procs.curl_proc(curl) | ||||
|         return Response(ans, mimetype=Config.text_mtype) | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
|         return Response(invalid, mimetype=Config.text_mtype) | ||||
|  | ||||
|  | ||||
| @app.route("/curls", methods=["POST"])  # type: ignore | ||||
| @limiter.limit(rate_limit)  # type: ignore | ||||
| def get_curls() -> Any: | ||||
|     try: | ||||
|         return Procs.curls_proc(request) | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
|         return Response(invalid, mimetype=Config.text_mtype) | ||||
							
								
								
									
										44
									
								
								server/bundle.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/bundle.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| # Standard | ||||
| from pathlib import Path | ||||
|  | ||||
|  | ||||
| def get_files(path: str, ext: str) -> list[str]: | ||||
|     files = Path(path).glob(f"*.{ext}") | ||||
|     return [str(f) for f in files] | ||||
|  | ||||
|  | ||||
| def bundle_dashboard_js(what: str, first: list[str], last: list[str]) -> None: | ||||
|     files = get_files(f"static/dashboard/js/{what}", "js") | ||||
|  | ||||
|     def get_path(f: str) -> Path: | ||||
|         return Path(f"static/dashboard/js/{what}/{f}.js") | ||||
|  | ||||
|     with Path(f"static/dashboard/js/bundle.{what}.js").open("w") as f: | ||||
|         for file_ in first: | ||||
|             with get_path(file_).open("r") as js: | ||||
|                 f.write(js.read()) | ||||
|                 f.write("\n\n") | ||||
|  | ||||
|         for file_ in files: | ||||
|             file = Path(file_) | ||||
|  | ||||
|             if (file.stem not in first) and (file.stem not in last): | ||||
|                 with file.open("r") as js: | ||||
|                     f.write(js.read()) | ||||
|                     f.write("\n\n") | ||||
|  | ||||
|         for file_ in last: | ||||
|             with get_path(file_).open("r") as js: | ||||
|                 f.write(js.read()) | ||||
|                 f.write("\n\n") | ||||
|  | ||||
|  | ||||
| def bundle_dashboard() -> None: | ||||
|     bundle_dashboard_js("libs", [], []) | ||||
|     bundle_dashboard_js("main", ["app", "utils"], ["load"]) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     bundle_dashboard() | ||||
							
								
								
									
										43
									
								
								server/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								server/config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| # Standard | ||||
| import json | ||||
| import string | ||||
| from pathlib import Path | ||||
|  | ||||
|  | ||||
| curl_max_length = 20 | ||||
| key_length = 22 | ||||
| status_max_length = 500 | ||||
| max_curls = 100 | ||||
| rate_limit = 12 | ||||
| rate_limit_change = 3 | ||||
| captcha_key = "changeMe" | ||||
| captcha_cheat = "" | ||||
| text_mtype = "text/plain" | ||||
| captcha_key_file = Path("captcha_key.txt") | ||||
| captcha_cheat_file = Path("captcha_cheat.txt") | ||||
| manifest_file = Path("manifest.json") | ||||
| manifest = {} | ||||
|  | ||||
| if captcha_key_file.is_file(): | ||||
|     with captcha_key_file.open("r") as f: | ||||
|         captcha_key = f.read().strip() | ||||
|  | ||||
| if captcha_cheat_file.is_file(): | ||||
|     with captcha_cheat_file.open("r") as f: | ||||
|         captcha_cheat = f.read().strip() | ||||
|  | ||||
| if manifest_file.is_file(): | ||||
|     with manifest_file.open("r") as f: | ||||
|         manifest = json.loads(f.read().strip()) | ||||
|  | ||||
| captcha = { | ||||
|     "SECRET_CAPTCHA_KEY": captcha_key, | ||||
|     "CAPTCHA_LENGTH": 10, | ||||
|     "CAPTCHA_DIGITS": False, | ||||
|     "EXPIRE_SECONDS": 60, | ||||
|     "CAPTCHA_IMG_FORMAT": "JPEG", | ||||
|     "ONLY_UPPERCASE": False, | ||||
|     "CHARACTER_POOL": string.ascii_lowercase, | ||||
| } | ||||
							
								
								
									
										189
									
								
								server/curls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								server/curls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| # Standard | ||||
| import random | ||||
| import string | ||||
| import datetime | ||||
| from typing import Any | ||||
|  | ||||
| # Modules | ||||
| import db as DB | ||||
| import config as Config | ||||
|  | ||||
|  | ||||
| def get_value(curl: str, what: str) -> Any: | ||||
|     dbase = DB.get_db() | ||||
|     cursor = dbase.cursor() | ||||
|     db_string = f"SELECT {what} FROM curls WHERE curl = ?" | ||||
|     cursor.execute(db_string, (curl,)) | ||||
|     return cursor.fetchone() | ||||
|  | ||||
|  | ||||
| def add_curl(curl: str, key: str) -> None: | ||||
|     dbase = DB.get_db() | ||||
|     cursor = dbase.cursor() | ||||
|  | ||||
|     db_string = """ | ||||
|     INSERT INTO curls (created, updated, curl, key, status) | ||||
|     VALUES (?, ?, ?, ?, ?) | ||||
|     """ | ||||
|  | ||||
|     now = date_now() | ||||
|     cursor.execute(db_string, (now, now, curl, key, "")) | ||||
|     dbase.commit() | ||||
|  | ||||
|  | ||||
| def make_key(curl: str) -> str: | ||||
|     characters = string.ascii_letters + string.digits | ||||
|     chars = "".join(random.choice(characters) for i in range(Config.key_length)) | ||||
|     start = curl[:3] | ||||
|     rest = len(start) + 1 | ||||
|     return f"{start}_{chars[rest:]}" | ||||
|  | ||||
|  | ||||
| def check_key(curl: str, key: str) -> bool: | ||||
|     if not key: | ||||
|         return False | ||||
|  | ||||
|     if len(key) > Config.key_length: | ||||
|         return False | ||||
|  | ||||
|     result = get_value(curl, "key") | ||||
|     return bool(result) and (result[0] == key) | ||||
|  | ||||
|  | ||||
| def change_status(curl: str, status: str) -> None: | ||||
|     current = get_status(curl, False) | ||||
|  | ||||
|     if current and (current == status): | ||||
|         return | ||||
|  | ||||
|     dbase = DB.get_db() | ||||
|     cursor = dbase.cursor() | ||||
|  | ||||
|     db_string = """ | ||||
|     UPDATE curls | ||||
|     SET status = ?, updated = ?, changes = changes + 1 | ||||
|     WHERE curl = ? | ||||
|     """ | ||||
|  | ||||
|     now = date_now() | ||||
|     cursor.execute(db_string, (status, now, curl)) | ||||
|     dbase.commit() | ||||
|  | ||||
|  | ||||
| def curl_exists(curl: str) -> bool: | ||||
|     result = get_value(curl, "curl") | ||||
|     return bool(result) | ||||
|  | ||||
|  | ||||
| def get_status(curl: str, fill: bool = True) -> str: | ||||
|     result = get_value(curl, "status") | ||||
|  | ||||
|     if fill: | ||||
|         return fill_status(result) | ||||
|  | ||||
|     if result: | ||||
|         return str(result[0]) | ||||
|  | ||||
|     return "" | ||||
|  | ||||
|  | ||||
| def get_curl_list(curls: list[str]) -> list[dict[str, Any]]: | ||||
|     dbase = DB.get_db() | ||||
|     cursor = dbase.cursor() | ||||
|  | ||||
|     db_string = """ | ||||
|     SELECT created, curl, status, updated, changes | ||||
|     FROM curls | ||||
|     WHERE curl IN ({}) | ||||
|     """.format(",".join("?" * len(curls))) | ||||
|  | ||||
|     cursor.execute(db_string, curls) | ||||
|     results = cursor.fetchall() | ||||
|     items = [] | ||||
|  | ||||
|     for result in results: | ||||
|         if not result: | ||||
|             continue | ||||
|  | ||||
|         created = str(result[0]) or "" | ||||
|         curl = result[1] | ||||
|         status = result[2] | ||||
|         updated = str(result[3]) or "" | ||||
|         changes = result[4] or 0 | ||||
|  | ||||
|         items.append( | ||||
|             { | ||||
|                 "created": created, | ||||
|                 "curl": curl, | ||||
|                 "status": status, | ||||
|                 "updated": updated, | ||||
|                 "changes": changes, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     return items | ||||
|  | ||||
|  | ||||
| def fill_status(result: Any) -> str: | ||||
|     if not result: | ||||
|         return "Not claimed yet" | ||||
|  | ||||
|     status = result[0] | ||||
|  | ||||
|     if not status: | ||||
|         return "Not updated yet" | ||||
|  | ||||
|     return str(status) | ||||
|  | ||||
|  | ||||
| def curl_too_long() -> str: | ||||
|     return f"Error: Curl is too long (Max is {Config.curl_max_length} characters)" | ||||
|  | ||||
|  | ||||
| def status_too_long() -> str: | ||||
|     return f"Error: Text is too long (Max is {Config.status_max_length} characters)" | ||||
|  | ||||
|  | ||||
| def too_many_curls() -> str: | ||||
|     return f"Error: Too many curls (Max is {Config.max_curls})" | ||||
|  | ||||
|  | ||||
| def check_curl(curl: str) -> bool: | ||||
|     if not curl: | ||||
|         return False | ||||
|  | ||||
|     if len(curl) > Config.curl_max_length: | ||||
|         return False | ||||
|  | ||||
|     if not curl.isalnum(): | ||||
|         return False | ||||
|  | ||||
|     return True | ||||
|  | ||||
|  | ||||
| def check_status(status: str) -> bool: | ||||
|     if not status: | ||||
|         return False | ||||
|  | ||||
|     if len(status) > Config.status_max_length: | ||||
|         return False | ||||
|  | ||||
|     return True | ||||
|  | ||||
|  | ||||
| def date_now() -> datetime.datetime: | ||||
|     return datetime.datetime.now(datetime.timezone.utc) | ||||
|  | ||||
|  | ||||
| def clean_curl(curl: str) -> str: | ||||
|     return str(curl).strip().lower() | ||||
|  | ||||
|  | ||||
| def clean_key(key: str) -> str: | ||||
|     return str(key).strip() | ||||
|  | ||||
|  | ||||
| def clean_status(status: str) -> str: | ||||
|     return str(status).strip() | ||||
							
								
								
									
										44
									
								
								server/db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/db.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| # Standard | ||||
| import sqlite3 | ||||
| from typing import Any | ||||
|  | ||||
| # Libraries | ||||
| import click | ||||
| from flask import current_app, g  # type: ignore | ||||
|  | ||||
|  | ||||
| DATABASE = "curls.db" | ||||
|  | ||||
|  | ||||
| def get_db() -> Any: | ||||
|     if "db" not in g: | ||||
|         g.db = sqlite3.connect(DATABASE, detect_types=sqlite3.PARSE_DECLTYPES) | ||||
|  | ||||
|         g.db.row_factory = sqlite3.Row | ||||
|  | ||||
|     return g.db | ||||
|  | ||||
|  | ||||
| def close_db(e: Any) -> None: | ||||
|     db = g.pop("db", None) | ||||
|  | ||||
|     if db is not None: | ||||
|         db.close() | ||||
|  | ||||
|  | ||||
| @click.command("init-db") | ||||
| def init_db_command() -> None: | ||||
|     init_db() | ||||
|     click.echo("Initialized the database.") | ||||
|  | ||||
|  | ||||
| def init_db() -> None: | ||||
|     db = get_db() | ||||
|  | ||||
|     with current_app.open_resource("schema.sql") as f: | ||||
|         db.executescript(f.read().decode("utf8")) | ||||
|  | ||||
|  | ||||
| def init_app(app: Any) -> None: | ||||
|     app.teardown_appcontext(close_db) | ||||
|     app.cli.add_command(init_db_command) | ||||
							
								
								
									
										25
									
								
								server/eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								server/eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import globals from "globals"; | ||||
| import pluginJs from "@eslint/js"; | ||||
|  | ||||
| export default [ | ||||
|   pluginJs.configs.recommended, | ||||
|  | ||||
|   {languageOptions: {globals: globals.browser}}, | ||||
|  | ||||
|   { | ||||
|     rules: { | ||||
|       "semi": "off", | ||||
|       "no-undef": "off", | ||||
|       "no-unused-vars": "off", | ||||
|       "indent": ["error", 4], | ||||
|       "linebreak-style": ["error", "unix"], | ||||
|       "quotes": ["error", "backtick"], | ||||
|       "no-console": "error", | ||||
|       "space-before-function-paren": ["error", { | ||||
|         "anonymous": "always", | ||||
|         "named": "never", | ||||
|         "asyncArrow": "always" | ||||
|       }], | ||||
|     }, | ||||
|   } | ||||
| ] | ||||
							
								
								
									
										93
									
								
								server/fill.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								server/fill.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| import sys | ||||
| import datetime | ||||
| import random | ||||
| import json | ||||
| import sqlite3 | ||||
| from sqlite3 import Error | ||||
|  | ||||
|  | ||||
| def create_connection(db_file: str) -> sqlite3.Connection | None: | ||||
|     conn = None | ||||
|  | ||||
|     try: | ||||
|         conn = sqlite3.connect(db_file) | ||||
|         print(sqlite3.version) | ||||
|     except Error as e: | ||||
|         print(e) | ||||
|  | ||||
|     return conn | ||||
|  | ||||
|  | ||||
| def clean_db(conn: sqlite3.Connection) -> None: | ||||
|     sql = "DELETE FROM curls" | ||||
|     cur = conn.cursor() | ||||
|     cur.execute(sql) | ||||
|     conn.commit() | ||||
|  | ||||
|  | ||||
| def insert_into_db(conn: sqlite3.Connection, curl: str, status: str) -> None: | ||||
|     sql = """ INSERT INTO curls(created, updated, curl, key, status) | ||||
|               VALUES(?,?,?,?,?) """ | ||||
|     cur = conn.cursor() | ||||
|     now = ( | ||||
|         date_now() | ||||
|     )  # assuming date_now() is a function that returns current date and time | ||||
|     cur.execute(sql, (now, now, curl, "pass", status)) | ||||
|     conn.commit() | ||||
|  | ||||
|  | ||||
| def date_now() -> datetime.datetime: | ||||
|     return datetime.datetime.now(datetime.timezone.utc) | ||||
|  | ||||
|  | ||||
| def get_random_items(file: str, num: int) -> list[str]: | ||||
|     with open(file) as f: | ||||
|         names = json.load(f) | ||||
|         return random.sample(names, num) | ||||
|  | ||||
|  | ||||
| def clean_items(names: list[str]) -> list[str]: | ||||
|     clean = [] | ||||
|  | ||||
|     for word in names: | ||||
|         clean.append("".join(filter(str.isalpha, word))) | ||||
|  | ||||
|     clean = [x.lower() for x in clean] | ||||
|     return clean | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     num = 140 | ||||
|     colors = ["red", "green", "blue", "yellow", "purple", "white"] | ||||
|     names = get_random_items("names.json", num) | ||||
|     names = clean_items(names) | ||||
|  | ||||
|     obj: dict[str, list[str]] = {} | ||||
|     d = 20 | ||||
|     n1 = 0 | ||||
|     n2 = d | ||||
|  | ||||
|     for color in colors: | ||||
|         obj[color] = [] | ||||
|  | ||||
|         for name in names[n1:n2]: | ||||
|             obj[color].append(name) | ||||
|  | ||||
|         n1 += d | ||||
|         n2 += d | ||||
|  | ||||
|     with open("export.json", "w") as f: | ||||
|         json.dump(obj, f, indent=4) | ||||
|  | ||||
|     sents = get_random_items("sentences.json", num) | ||||
|     conn = create_connection("curls.db") | ||||
|  | ||||
|     if not conn: | ||||
|         print("Error: Could not connect to database.") | ||||
|         sys.exit(1) | ||||
|  | ||||
|     with conn: | ||||
|         clean_db(conn) | ||||
|  | ||||
|         for i, name in enumerate(names): | ||||
|             insert_into_db(conn, name, sents[i]) | ||||
							
								
								
									
										2
									
								
								server/init_db.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								server/init_db.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| #!/usr/bin/env bash | ||||
| venv/bin/python -m flask init-db | ||||
							
								
								
									
										6
									
								
								server/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								server/manifest.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|     "version": "23.0.0", | ||||
|     "title": "Curls", | ||||
|     "repo": "github.com/madprops/curls", | ||||
|     "description": "Text status hosting" | ||||
| } | ||||
							
								
								
									
										1002
									
								
								server/names.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1002
									
								
								server/names.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								server/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| { | ||||
|     "name": "Dashboard", | ||||
|     "version": "1.0.0", | ||||
|     "description": "Curls Dashboard", | ||||
|     "main": "eslint.config.js", | ||||
|     "scripts": { | ||||
|       "test": "echo \"Error: no test specified\" && exit 1" | ||||
|     }, | ||||
|     "author": "", | ||||
|     "license": "AGPL-3.0-only", | ||||
|     "devDependencies": { | ||||
|       "@eslint/js": "^9.4.0", | ||||
|       "eslint": "^9.4.0", | ||||
|       "globals": "^15.4.0" | ||||
|     } | ||||
|   } | ||||
							
								
								
									
										93
									
								
								server/procs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								server/procs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| # Standard | ||||
| from flask import jsonify, Response  # type: ignore | ||||
| from typing import Any | ||||
|  | ||||
| # Modules | ||||
| import app as App | ||||
| import config as Config | ||||
| import curls as Curls | ||||
|  | ||||
|  | ||||
| invalid_curl = "Error: Invalid curl" | ||||
| invalid_key = "Error: Invalid key" | ||||
| invalid_status = "Error: Invalid status" | ||||
|  | ||||
|  | ||||
| def claim_proc(request: Any) -> str: | ||||
|     c_hash = request.form.get("captcha-hash", "") | ||||
|     c_text = request.form.get("captcha-text", "") | ||||
|     curl = Curls.clean_curl(request.form.get("curl", "")) | ||||
|  | ||||
|     check_catpcha = True | ||||
|  | ||||
|     if Config.captcha_cheat and (c_text == Config.captcha_cheat): | ||||
|         check_catpcha = False | ||||
|  | ||||
|     if check_catpcha: | ||||
|         if not App.simple_captcha.verify(c_text, c_hash): | ||||
|             return "Error: Failed captcha" | ||||
|  | ||||
|     if not Curls.check_curl(curl): | ||||
|         return invalid_curl | ||||
|  | ||||
|     if Curls.curl_exists(curl): | ||||
|         return "Error: Curl already exists" | ||||
|  | ||||
|     key = Curls.make_key(curl) | ||||
|     Curls.add_curl(curl, key) | ||||
|  | ||||
|     lines = [ | ||||
|         f"Your curl is: <b>{curl}</b>", | ||||
|         f"Your key is: <b>{key}</b>", | ||||
|         "The key is secret and shouldn't be shared.", | ||||
|         "Save the key somewhere so it doesn't get lost.", | ||||
|         "There is no way to recover a lost key.", | ||||
|     ] | ||||
|  | ||||
|     return "<br>".join(lines) | ||||
|  | ||||
|  | ||||
| def change_proc(request: Any) -> str: | ||||
|     curl = Curls.clean_curl(request.form.get("curl", "")) | ||||
|     key = Curls.clean_key(request.form.get("key", "")) | ||||
|     status = Curls.clean_status(request.form.get("status", "")) | ||||
|  | ||||
|     if (not curl) or (not key) or (not status): | ||||
|         return "Error: Empty fields" | ||||
|  | ||||
|     if not Curls.check_curl(curl): | ||||
|         return invalid_curl | ||||
|  | ||||
|     if not Curls.check_status(status): | ||||
|         return invalid_status | ||||
|  | ||||
|     if not Curls.check_key(curl, key): | ||||
|         return invalid_key | ||||
|  | ||||
|     Curls.change_status(curl, status) | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| def curl_proc(curl: str) -> str: | ||||
|     if not Curls.check_curl(curl): | ||||
|         return invalid_curl | ||||
|  | ||||
|     return Curls.get_status(curl) | ||||
|  | ||||
|  | ||||
| def curls_proc(request: Any) -> Any: | ||||
|     curls = request.form.getlist("curl") | ||||
|  | ||||
|     if len(curls) > Config.max_curls: | ||||
|         ans = Curls.too_many_curls() | ||||
|         return Response(ans, mimetype=Config.text_mtype) | ||||
|  | ||||
|     for curl in curls: | ||||
|         if not Curls.check_curl(curl): | ||||
|             ans = invalid_curl | ||||
|             return Response(ans, mimetype=Config.text_mtype) | ||||
|  | ||||
|     results = Curls.get_curl_list(curls) | ||||
|     return jsonify(results) | ||||
							
								
								
									
										6
									
								
								server/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								server/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| Flask == 3.0.3 | ||||
| Flask-Cors == 4.0.1 | ||||
| Flask-Limiter == 3.7.0 | ||||
| flask-simple-captcha == 5.5.5 | ||||
| gunicorn == 22.0.0 | ||||
| redis == 5.0.6 | ||||
							
								
								
									
										46
									
								
								server/ruff.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								server/ruff.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| [lint] | ||||
|  | ||||
| select = [ | ||||
|     "T", | ||||
|     "Q", | ||||
|     "W", | ||||
|     "B", | ||||
|     "N", | ||||
|     "F", | ||||
|     "FA", | ||||
|     "RET", | ||||
|     "PTH", | ||||
|     "ERA", | ||||
|     "PLW", | ||||
|     "PERF", | ||||
|     "RUF", | ||||
|     "FLY", | ||||
|     "PT", | ||||
|     "PYI", | ||||
|     "PIE", | ||||
|     "ICN", | ||||
|     "UP", | ||||
|     "TRY", | ||||
|     "C4", | ||||
|     "E401", | ||||
|     "E713", | ||||
|     "E721", | ||||
|     "S101", | ||||
|     "S113", | ||||
|     "SIM103", | ||||
|     "SIM114", | ||||
|     "SIM118", | ||||
|     "SIM210", | ||||
|     "PLR5501", | ||||
|     "PLR1711", | ||||
| ] | ||||
|  | ||||
| ignore = [ | ||||
|     "T201", | ||||
|     "N812", | ||||
| ] | ||||
|  | ||||
| exclude = [ | ||||
|     "pyperclip.py", | ||||
|     "tests.py", | ||||
| ] | ||||
							
								
								
									
										24
									
								
								server/schema.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/schema.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| DROP TABLE IF EXISTS curls; | ||||
|  | ||||
| CREATE TABLE curls ( | ||||
|     -- Internal ID of the curl | ||||
|     id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
|  | ||||
|     -- The date when the curl was created | ||||
|     created TIMESTAMP NOT NULL, | ||||
|  | ||||
|     -- The date when the curl was last changed | ||||
|     updated TIMESTAMP NOT NULL, | ||||
|  | ||||
|     -- The public name of the curl | ||||
|     curl TEXT NOT NULL UNIQUE, | ||||
|  | ||||
|     -- The secret key of the curl | ||||
|     key TEXT NOT NULL, | ||||
|  | ||||
|     -- The current text of the curl | ||||
|     status TEXT NOT NULL, | ||||
|  | ||||
|     -- The total number of changes made | ||||
|     changes INTEGER NOT NULL DEFAULT 0 | ||||
| ); | ||||
							
								
								
									
										156
									
								
								server/sentences.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								server/sentences.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| [ | ||||
|     "All your base are belong to us", | ||||
|     "It's not just a game, it's a life", | ||||
|     "I'm not bad. I'm just drawn that way", | ||||
|     "The cake is a lie!", | ||||
|     "It does so with all deliberate speed", | ||||
|     "All gurls are the same", | ||||
|     "You shall not pass... into this cave, that is", | ||||
|     "I am the one who knocks..", | ||||
|     "Why so serious?", | ||||
|     "Get over here!", | ||||
|     "Carry the One", | ||||
|     "You are not prepared", | ||||
|     "I am Error", | ||||
|     "The path of righteousness will be yours to choose..", | ||||
|     "It's over, man. It's all over", | ||||
|     "Are you ready for this?", | ||||
|     "Life is like a game, full of bugs", | ||||
|     "The greatest trick the Devil ever pulled was convincing the world he didn't exist", | ||||
|     "I'd rather be playing D&D", | ||||
|     "You are cordially invited to visit... your own grave", | ||||
|     "Gotta catch 'em all!", | ||||
|     "Time waits for no man, not even a hero", | ||||
|     "I'm just a simple game developer trying to make a difference..", | ||||
|     "It doesn't matter how many lives you have left, it matters how well you play them", | ||||
|     "This is the end... and this is the beginning", | ||||
|     "The truth is not in here", | ||||
|     "I'm a little tea pot short and stout..", | ||||
|     "When you're fast, be faster. When you're clever, be more clever", | ||||
|     "You will never be able to find peace", | ||||
|     "We're all just pawns in a game... and we don't even know the rules!", | ||||
|     "Do you have what it takes to save the world?", | ||||
|     "I'm not sure I want to go, but I have to", | ||||
|     "It's only a game", | ||||
|     "I'm a hero, and you're just a pawn!", | ||||
|     "The future is not set in stone, it's a puzzle that needs to be solved", | ||||
|     "You know what I like about this game? It's a game..", | ||||
|     "It doesn't matter if you're a man or a woman... it matters how well you play the game", | ||||
|     "You are not worthy!", | ||||
|     "It's time to make things right, and take back what's yours!", | ||||
|     "You're not going anywhere", | ||||
|     "Life is a series of choices... choose wisely", | ||||
|     "I'm not afraid of death; I just don't want to die yet", | ||||
|     "In a world where time has no meaning, what's the point?", | ||||
|     "You know the drill... you gotta be prepared for anything!", | ||||
|     "I'm not sure I believe in fate... but I do believe in my abilities", | ||||
|     "It's over, it's finally over... or is it?", | ||||
|     "You're a warrior, a hero... but you're also a pawn", | ||||
|     "I don't want to be a hero... I just want to live my life", | ||||
|     "The game's not over yet!", | ||||
|     "I'm the only one who can save this world..", | ||||
|     "You're too slow, you're too weak, and you're no match for me!", | ||||
|     "Life is but a game... and I am the player", | ||||
|     "It's not about winning or losing, it's about playing the game", | ||||
|     "You know what they say... 'all's fair in love and war'..", | ||||
|     "I'm a hero, but I'm also a villain", | ||||
|     "It's time to put an end to this game", | ||||
|     "You're not alone... you have me", | ||||
|     "Life is full of choices... choose wisely, and always remember the consequences", | ||||
|     "I'm not a hero... I'm just a man who's trying to make a difference", | ||||
|     "You're going down, pal!", | ||||
|     "It's time for me to take my leave... but the game is far from over", | ||||
|     "I'm not sure what the future holds... but I know it won't be easy", | ||||
|     "The game is afoot!", | ||||
|     "It's time to put an end to this war... and bring peace to the land", | ||||
|     "You're not going anywhere, at least not yet..", | ||||
|     "Life is but a dream... or is it?", | ||||
|     "I'm not sure what the future holds... but I know I'll face it head-on", | ||||
|     "You're not alone... you have me, and together we can make a difference!", | ||||
|     "It's time to take back what's rightfully yours..", | ||||
|     "I'm a hero, but I'm also a villain... and sometimes it's hard to tell the difference", | ||||
|     "You're not going anywhere... at least not yet..", | ||||
|     "Life is full of choices... choose wisely, and always remember the consequences", | ||||
|     "I'm not sure what the future holds... but I know it won't be easy", | ||||
|     "The game is afoot!", | ||||
|     "It's time to put an end to this war... and bring peace to the land", | ||||
|     "You're not going anywhere, at least not yet..", | ||||
|     "Life is but a dream... or is it?", | ||||
|     "I'm not sure what the future holds... but I know I'll face it head-on", | ||||
|     "I'll be back", | ||||
|     "You had me at 'hello'", | ||||
|     "I see dead people", | ||||
|     "Fasten your seatbelts. It's going to be a bumpy night", | ||||
|     "You're gonna need a bigger boat", | ||||
|     "We're gonna make him an offer he can't refuse", | ||||
|     "All right, Mr. DeMille, I'm ready for my close-up", | ||||
|     "Nobody puts Baby in the corner!", | ||||
|     "You talking to me?", | ||||
|     "It's not the years, honey. It's the mileage", | ||||
|     "I am serious... and don't call me Shirley", | ||||
|     "Fat guy in a little coat!", | ||||
|     "Nobody moves. Nobody talks. Nobody breathes. Just listen", | ||||
|     "We're on a mission from God", | ||||
|     "You had my curiosity, but not my interest", | ||||
|     "May the Force be with you", | ||||
|     "Show me the money!", | ||||
|     "You keep using that word. I do not think it means what you think it means", | ||||
|     "I'll get you, and your little dog too!", | ||||
|     "I'm gonna make him an offer he can't refuse. And if he doesn't accept it, there's going to be a war", | ||||
|     "It's alive! It's alive!", | ||||
|     "You're only given a little spark of madness. You mustn't lose it", | ||||
|     "E.T. phone home", | ||||
|     "I'm the king of the world!", | ||||
|     "Say hello to my little friend!", | ||||
|     "Do you feel lucky, punk? Do you?", | ||||
|     "Fasten your seatbelts. It's going to be a bumpy ride", | ||||
|     "Nobody knows anything", | ||||
|     "It's only after we've lost everything that we're free to do anything", | ||||
|     "I'm not bad. I'm just drawn that way", | ||||
|     "What you see before me is the real thing", | ||||
|     "You don't know what it means, do you?", | ||||
|     "I find your lack of faith disturbing", | ||||
|     "Hasta la vista, baby!", | ||||
|     "Get to the chopper!", | ||||
|     "You're gonna need a bigger map", | ||||
|     "I'll never let go. I'll never let go", | ||||
|     "That's not my nose! That's my face!", | ||||
|     "Life moves pretty fast. If you don't stop and look around once in a while, you could miss it", | ||||
|     "You had me at 'hello'", | ||||
|     "It's not the size of the dog in the fight, it's the size of the fight in the dog", | ||||
|     "I am serious... and don't call me Shirley", | ||||
|     "Fat guy in a little coat!", | ||||
|     "Nobody puts Baby in the corner!", | ||||
|     "You talking to me?", | ||||
|     "May the Force be with you", | ||||
|     "I'll never let go. I'll never let go", | ||||
|     "It's only after we've lost everything that we're free to do anything", | ||||
|     "All right, Mr. DeMille, I'm ready for my close-up", | ||||
|     "We're on a mission from God", | ||||
|     "Nobody knows anything", | ||||
|     "Get to the chopper!", | ||||
|     "Do you feel lucky, punk? Do you?", | ||||
|     "Fasten your seatbelts. It's going to be a bumpy ride", | ||||
|     "That's not my nose! That's my face!", | ||||
|     "It's alive! It's alive!", | ||||
|     "Say hello to my little friend!", | ||||
|     "What you see before me is the real thing", | ||||
|     "I'm the king of the world!", | ||||
|     "E.T. phone home", | ||||
|     "Hasta la vista, baby!", | ||||
|     "You're only given a little spark of madness. You mustn't lose it", | ||||
|     "It's not the years, honey. It's the mileage", | ||||
|     "Show me the money!", | ||||
|     "I find your lack of faith disturbing", | ||||
|     "We're gonna make him an offer he can't refuse", | ||||
|     "Nobody moves. Nobody talks. Nobody breathes. Just listen", | ||||
|     "Do you know what I am?", | ||||
|     "You're gonna need a bigger boat", | ||||
|     "Fasten your seatbelts. It's going to be a bumpy night", | ||||
|     "May the Force be with you", | ||||
|     "I'll get you, and your little dog too!", | ||||
|     "You're only given a little spark of madness. You mustn't lose it", | ||||
|     "Get off my lawn!", | ||||
|     "Nobody puts Baby in the corner!", | ||||
|     "It's not the size of the dog in the fight, it's the size of the fight in the dog." | ||||
| ] | ||||
							
								
								
									
										429
									
								
								server/static/dashboard/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										429
									
								
								server/static/dashboard/css/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,429 @@ | ||||
| :root { | ||||
|     --background: rgb(12, 12, 12); | ||||
|     --vertical_padding: 0.74rem; | ||||
|     --horizontal_padding: 0.66rem; | ||||
|     --alt_background_color: rgb(18, 18, 18); | ||||
| } | ||||
|  | ||||
| body, | ||||
| html | ||||
| { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     font-family: var(--font); | ||||
|     background-color: var(--background); | ||||
|     color: var(--color); | ||||
|     font-size: 18px; | ||||
| } | ||||
|  | ||||
| #main { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 0.8rem; | ||||
|     width: 100vw; | ||||
|     height: 100vh; | ||||
|     padding-left: var(--horizontal_padding); | ||||
|     padding-right: var(--horizontal_padding); | ||||
|     padding-top: var(--vertical_padding); | ||||
|     box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| #container_outer { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     overflow-x: hidden; | ||||
|     overflow-y: auto; | ||||
| } | ||||
|  | ||||
| #container { | ||||
|     display: table; | ||||
|     border-collapse: collapse; | ||||
|     table-layout: fixed; | ||||
|     width: 100%; | ||||
|     box-sizing: border-box; | ||||
|     outline: none; | ||||
| } | ||||
|  | ||||
| #infobar { | ||||
|     cursor: default; | ||||
|     user-select: none; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1rem; | ||||
| } | ||||
|  | ||||
| #infobar_curls, | ||||
| #infobar_date | ||||
| { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .infobar_separator { | ||||
|     opacity: 0.5; | ||||
| } | ||||
|  | ||||
| .item { | ||||
|     display: table-row; | ||||
|     border: var(--border); | ||||
|     cursor: default; | ||||
| } | ||||
|  | ||||
| .item_icon, | ||||
| .item_curl, | ||||
| .item_status, | ||||
| .item_updated | ||||
| { | ||||
|     display: table-cell; | ||||
|     padding-top: 0.8rem; | ||||
|     padding-bottom: 0.8rem; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| .item_icon { | ||||
|     width: 2.5rem; | ||||
|     justify-content: center; | ||||
|     cursor: pointer; | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| .item_icon_canvas { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|     text-align: left; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .item_icon:hover .item_icon_canvas { | ||||
|     filter: brightness(1.5); | ||||
| } | ||||
|  | ||||
| .item_curl { | ||||
|     width: 150px; | ||||
|     max-width: 150px; | ||||
|     color: var(--color); | ||||
|     text-align: left; | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| .item_status { | ||||
|     text-align: left; | ||||
|     padding-right: 0.8rem; | ||||
|     padding-left: 0.8rem; | ||||
|     word-break: break-word; | ||||
|     white-space: pre-wrap; | ||||
| } | ||||
|  | ||||
| .item_status.nowrap { | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| .item_updated { | ||||
|     width: 225px; | ||||
|     max-width: 225px; | ||||
|     color: var(--color); | ||||
|     white-space: nowrap; | ||||
|     text-align: right; | ||||
|     padding-right: 0.5rem; | ||||
| } | ||||
|  | ||||
| .item:hover { | ||||
|     background-color: var(--color_alpha_0); | ||||
| } | ||||
|  | ||||
| .item.selected { | ||||
|     background-color: var(--color_alpha_1); | ||||
| } | ||||
|  | ||||
| /*  */ | ||||
|  | ||||
| #controls { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     margin-bottom: 0.5rem; | ||||
|     row-gap: 0.8rem; | ||||
|     column-gap: 1.2rem; | ||||
|     flex-wrap: wrap; | ||||
| } | ||||
|  | ||||
| .control_bar | ||||
| { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     gap: 1.2rem; | ||||
| } | ||||
|  | ||||
| .control_section { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     gap: 0.66rem; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     padding: 0.5rem; | ||||
| } | ||||
|  | ||||
| a:visited, | ||||
| a:link, | ||||
| a:hover { | ||||
|     color: var(--color); | ||||
| } | ||||
|  | ||||
| .pointer { | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| input[type="text"], | ||||
| input[type="password"] | ||||
| { | ||||
|     background-color: var(--background); | ||||
|     color: var(--color); | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     padding: 0.09rem; | ||||
|     padding-left: 0.25rem; | ||||
|     outline: none; | ||||
|     width: 8rem; | ||||
|     font-size: 1rem; | ||||
|     font: var(--font); | ||||
| } | ||||
|  | ||||
| .button { | ||||
|     background-color: var(--background); | ||||
|     color: var(--color); | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     cursor: pointer; | ||||
|     outline: none; | ||||
|     font-size: 1rem; | ||||
|     padding: 0.05rem; | ||||
|     padding-left: 0.25rem; | ||||
|     padding-right: 0.25rem; | ||||
|     user-select: none; | ||||
|     white-space: nowrap; | ||||
|     min-width: 1rem; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .button:hover { | ||||
|     background-color: var(--color); | ||||
|     color: var(--background); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| @keyframes blink { | ||||
|     0% { | ||||
|         opacity: 1; | ||||
|     } | ||||
|  | ||||
|     50% { | ||||
|         opacity: 0; | ||||
|     } | ||||
|  | ||||
|     100% { | ||||
|         opacity: 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .blink { | ||||
|     animation: blink 500ms infinite; | ||||
| } | ||||
|  | ||||
| @keyframes colorchange { | ||||
|     0% { | ||||
|         border-color: rgb(255, 102, 102); | ||||
|     } | ||||
|  | ||||
|     25% { | ||||
|         border-color: rgb(125, 255, 125); | ||||
|     } | ||||
|  | ||||
|     50% { | ||||
|         border-color: rgb(157, 157, 255); | ||||
|     } | ||||
|  | ||||
|     100% { | ||||
|         border-color: white; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .button.active { | ||||
|     animation: colorchange 1s infinite; | ||||
|     border-radius: 8px; | ||||
| } | ||||
|  | ||||
| .glow:hover { | ||||
|     text-shadow: 0 0 10px var(--color), 0 0 20px var(--color), 0 0 30px var(--color), 0 0 40px var(--color), 0 0 50px var(--color), 0 0 60px var(--color); | ||||
| } | ||||
|  | ||||
| .glow_white:hover { | ||||
|     text-shadow: 0 0 10px white, 0 0 20px white, 0 0 30px white, 0 0 40px white, 0 0 50px white, 0 0 60px white; | ||||
| } | ||||
|  | ||||
| .glow { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .glow_white { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .noselect { | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| #claim { | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| #footer { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     justify-content: flex-start; | ||||
|     border-top: 1px solid var(--color_alpha_2); | ||||
|     margin-top: 0.35rem; | ||||
|     padding-top: 0.5rem; | ||||
|     padding-bottom: var(--vertical_padding); | ||||
|     gap: 1.25rem; | ||||
| } | ||||
|  | ||||
| .footer_item { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     gap: 0.5rem; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| #version { | ||||
|     margin-left: auto; | ||||
| } | ||||
|  | ||||
| .disabled { | ||||
|     pointer-events: none; | ||||
|     opacity: 0.6; | ||||
| } | ||||
|  | ||||
| .modal_message { | ||||
|     max-width: 30rem; | ||||
|     white-space: pre-wrap; | ||||
|     cursor: auto; | ||||
| } | ||||
|  | ||||
| .modal_button { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     border: 1px solid var(--color_alpha_2); | ||||
|     padding-left: 0.4rem; | ||||
|     padding-right: 0.4rem; | ||||
|     padding-top: 0.05rem; | ||||
|     padding-bottom: 0.05rem; | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|     outline: none; | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .modal_button:hover { | ||||
|     border: 1px solid var(--color); | ||||
| } | ||||
|  | ||||
| .modal_items { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1rem; | ||||
| } | ||||
|  | ||||
| #alert_buttons { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1rem; | ||||
| } | ||||
|  | ||||
| #alert_message_container { | ||||
|     max-height: 14rem; | ||||
|     overflow-x: hidden; | ||||
|     overflow-y: auto; | ||||
| } | ||||
|  | ||||
| #prompt_input { | ||||
|     width: 14rem; | ||||
|     padding: 0.2rem; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| #needcontext-main { | ||||
|     background-color: rgba(0, 0, 0, 0.44) !important; | ||||
| } | ||||
|  | ||||
| #needcontext-container { | ||||
|     background-color: var(--alt_background_color) !important; | ||||
|     border: 2px solid var(--color_alpha_2) !important; | ||||
|     color: var(--color) !important; | ||||
| } | ||||
|  | ||||
| .needcontext-text { | ||||
|     max-width: 300px !important; | ||||
|     white-space: wrap !important; | ||||
| } | ||||
|  | ||||
| .needcontext-item-selected { | ||||
|     background-color: var(--color_alpha_1) !important; | ||||
| } | ||||
|  | ||||
| .needcontext-text { | ||||
|     font-family: var(--font) !important; | ||||
| } | ||||
|  | ||||
| .Msg-window { | ||||
|     color: var(--color) !important; | ||||
|     background-color: var(--alt_background_color) !important; | ||||
|     border: 2px solid var(--color_alpha_2) !important; | ||||
| } | ||||
|  | ||||
| .Msg-titlebar { | ||||
|     color: var(--color) !important; | ||||
|     background-color: rgb(42, 42, 42) !important; | ||||
|     font-family: var(--font) !important; | ||||
| } | ||||
|  | ||||
| .Msg-progressbar { | ||||
|     background-color: var(--color_alpha_2) !important; | ||||
| } | ||||
|  | ||||
| .Msg-titlebar { | ||||
|     padding-top: 0.24rem !important; | ||||
|     padding-bottom: 0.24rem !important; | ||||
|     padding-left: 0.8rem !important; | ||||
|     padding-right: 0.8rem !important; | ||||
| } | ||||
|  | ||||
| .Msg-window-inner-x:hover { | ||||
|     background-color: var(--color_alpha_1) !important; | ||||
| } | ||||
|  | ||||
| .Msg-content-modal { | ||||
|     padding: 0.8rem !important; | ||||
| } | ||||
							
								
								
									
										125
									
								
								server/static/dashboard/js/libs/dateformat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								server/static/dashboard/js/libs/dateformat.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| /* | ||||
| * Date Format 1.2.3 | ||||
| * (c) 2007-2009 Steven Levithan <stevenlevithan.com> | ||||
| * MIT license | ||||
| * | ||||
| * Includes enhancements by Scott Trenda <scott.trenda.net> | ||||
| * and Kris Kowal <cixar.com/~kris.kowal/> | ||||
| * | ||||
| * Accepts a date, a mask, or a date and a mask. | ||||
| * Returns a formatted version of the given date. | ||||
| * The date defaults to the current date/time. | ||||
| * The mask defaults to dateFormat.masks.default. | ||||
| */ | ||||
|  | ||||
| var dateFormat = function () { | ||||
|     var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, | ||||
| 		timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, | ||||
| 		timezoneClip = /[^-+\dA-Z]/g, | ||||
| 		pad = function (val, len) { | ||||
| 		    val = String(val); | ||||
| 		    len = len || 2; | ||||
| 		    while (val.length < len) val = "0" + val; | ||||
| 		    return val; | ||||
| 		}; | ||||
|  | ||||
|     // Regexes and supporting functions are cached through closure | ||||
|     return function (date, mask, utc) { | ||||
|         var dF = dateFormat; | ||||
|  | ||||
|         // You can't provide utc if you skip other args (use the "UTC:" mask prefix) | ||||
|         if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { | ||||
|             mask = date; | ||||
|             date = undefined; | ||||
|         } | ||||
|  | ||||
|         // Passing date through Date applies Date.parse, if necessary | ||||
|         date = date ? new Date(date) : new Date; | ||||
|         if (isNaN(date)) throw SyntaxError("invalid date"); | ||||
|  | ||||
|         mask = String(dF.masks[mask] || mask || dF.masks["default"]); | ||||
|  | ||||
|         // Allow setting the utc argument via the mask | ||||
|         if (mask.slice(0, 4) == "UTC:") { | ||||
|             mask = mask.slice(4); | ||||
|             utc = true; | ||||
|         } | ||||
|  | ||||
|         var _ = utc ? "getUTC" : "get", | ||||
| 			d = date[_ + "Date"](), | ||||
| 			D = date[_ + "Day"](), | ||||
| 			m = date[_ + "Month"](), | ||||
| 			y = date[_ + "FullYear"](), | ||||
| 			H = date[_ + "Hours"](), | ||||
| 			M = date[_ + "Minutes"](), | ||||
| 			s = date[_ + "Seconds"](), | ||||
| 			L = date[_ + "Milliseconds"](), | ||||
| 			o = utc ? 0 : date.getTimezoneOffset(), | ||||
| 			flags = { | ||||
| 			    d: d, | ||||
| 			    dd: pad(d), | ||||
| 			    ddd: dF.i18n.dayNames[D], | ||||
| 			    dddd: dF.i18n.dayNames[D + 7], | ||||
| 			    m: m + 1, | ||||
| 			    mm: pad(m + 1), | ||||
| 			    mmm: dF.i18n.monthNames[m], | ||||
| 			    mmmm: dF.i18n.monthNames[m + 12], | ||||
| 			    yy: String(y).slice(2), | ||||
| 			    yyyy: y, | ||||
| 			    h: H % 12 || 12, | ||||
| 			    hh: pad(H % 12 || 12), | ||||
| 			    H: H, | ||||
| 			    HH: pad(H), | ||||
| 			    M: M, | ||||
| 			    MM: pad(M), | ||||
| 			    s: s, | ||||
| 			    ss: pad(s), | ||||
| 			    l: pad(L, 3), | ||||
| 			    L: pad(L > 99 ? Math.round(L / 10) : L), | ||||
| 			    t: H < 12 ? "a" : "p", | ||||
| 			    tt: H < 12 ? "am" : "pm", | ||||
| 			    T: H < 12 ? "A" : "P", | ||||
| 			    TT: H < 12 ? "AM" : "PM", | ||||
| 			    Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), | ||||
| 			    o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), | ||||
| 			    S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] | ||||
| 			}; | ||||
|  | ||||
|         return mask.replace(token, function ($0) { | ||||
|             return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); | ||||
|         }); | ||||
|     }; | ||||
| } (); | ||||
|  | ||||
| // Some common format strings | ||||
| dateFormat.masks = { | ||||
|     "default": "ddd mmm dd yyyy HH:MM:ss", | ||||
|     shortDate: "m/d/yy", | ||||
|     mediumDate: "mmm d, yyyy", | ||||
|     longDate: "mmmm d, yyyy", | ||||
|     fullDate: "dddd, mmmm d, yyyy", | ||||
|     shortTime: "h:MM TT", | ||||
|     mediumTime: "h:MM:ss TT", | ||||
|     longTime: "h:MM:ss TT Z", | ||||
|     isoDate: "yyyy-mm-dd", | ||||
|     isoTime: "HH:MM:ss", | ||||
|     isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", | ||||
|     isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" | ||||
| }; | ||||
|  | ||||
| // Internationalization strings | ||||
| dateFormat.i18n = { | ||||
|     dayNames: [ | ||||
| 		"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", | ||||
| 		"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" | ||||
| 	], | ||||
|     monthNames: [ | ||||
| 		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", | ||||
| 		"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" | ||||
| 	] | ||||
| }; | ||||
|  | ||||
| // For convenience... | ||||
| Date.prototype.format = function (mask, utc) { | ||||
|     return dateFormat(this, mask, utc); | ||||
| }; | ||||
							
								
								
									
										143
									
								
								server/static/dashboard/js/libs/dom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								server/static/dashboard/js/libs/dom.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| // DOM v1.0.0 | ||||
| const DOM = {} | ||||
| DOM.dataset_obj = {} | ||||
| DOM.dataset_id = 0 | ||||
|  | ||||
| // Select a single element | ||||
| DOM.el = (query, root = document) => { | ||||
|   return root.querySelector(query) | ||||
| } | ||||
|  | ||||
| // Select an array of elements | ||||
| DOM.els = (query, root = document) => { | ||||
|   return Array.from(root.querySelectorAll(query)) | ||||
| } | ||||
|  | ||||
| // Select a single element or self | ||||
| DOM.el_or_self = (query, root = document) => { | ||||
|   let el = root.querySelector(query) | ||||
|  | ||||
|   if (!el) { | ||||
|     if (root.classList.contains(query.replace(`.`, ``))) { | ||||
|       el = root | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return el | ||||
| } | ||||
|  | ||||
| // Select an array of elements or self | ||||
| DOM.els_or_self = (query, root = document) => { | ||||
|   let els = Array.from(root.querySelectorAll(query)) | ||||
|  | ||||
|   if (els.length === 0) { | ||||
|     if (root.classList.contains(query.replace(`.`, ``))) { | ||||
|       els = [root] | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return els | ||||
| } | ||||
|  | ||||
| // Clone element | ||||
| DOM.clone = (el) => { | ||||
|   return el.cloneNode(true) | ||||
| } | ||||
|  | ||||
| // Clone element children | ||||
| DOM.clone_children = (query) => { | ||||
|   let items = [] | ||||
|   let children = Array.from(DOM.el(query).children) | ||||
|  | ||||
|   for (let c of children) { | ||||
|     items.push(DOM.clone(c)) | ||||
|   } | ||||
|  | ||||
|   return items | ||||
| } | ||||
|  | ||||
| // Data set manager | ||||
| DOM.dataset = (el, value, setvalue) => { | ||||
|   if (!el) { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   let id = el.dataset.dataset_id | ||||
|  | ||||
|   if (!id) { | ||||
|     id = DOM.dataset_id | ||||
|     DOM.dataset_id += 1 | ||||
|     el.dataset.dataset_id = id | ||||
|     DOM.dataset_obj[id] = {} | ||||
|   } | ||||
|  | ||||
|   if (setvalue !== undefined) { | ||||
|     DOM.dataset_obj[id][value] = setvalue | ||||
|   } | ||||
|   else { | ||||
|     return DOM.dataset_obj[id][value] | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Create an html element | ||||
| DOM.create = (type, classes = ``, id = ``) => { | ||||
|   let el = document.createElement(type) | ||||
|  | ||||
|   if (classes) { | ||||
|     let classlist = classes.split(` `).filter(x => x != ``) | ||||
|  | ||||
|     for (let cls of classlist) { | ||||
|       el.classList.add(cls) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (id) { | ||||
|     el.id = id | ||||
|   } | ||||
|  | ||||
|   return el | ||||
| } | ||||
|  | ||||
| // Add an event listener | ||||
| DOM.ev = (element, event, callback, extra) => { | ||||
|   element.addEventListener(event, callback, extra) | ||||
| } | ||||
|  | ||||
| // Add multiple event listeners | ||||
| DOM.evs = (element, events, callback, extra) => { | ||||
|   for (let event of events) { | ||||
|     element.addEventListener(event, callback, extra) | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Like jQuery's nextAll | ||||
| DOM.next_all = function* (e, selector) { | ||||
|   while (e = e.nextElementSibling) { | ||||
|     if (e.matches(selector)) { | ||||
|       yield e; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Get item index | ||||
| DOM.index = (el) => { | ||||
|   return Array.from(el.parentNode.children).indexOf(el) | ||||
| } | ||||
|  | ||||
| // Show an element | ||||
| DOM.show = (item) => { | ||||
|   if (typeof item === `string`) { | ||||
|     item = DOM.el(item) | ||||
|   } | ||||
|  | ||||
|   item.classList.remove(`hidden`) | ||||
| } | ||||
|  | ||||
| // Hide an element | ||||
| DOM.hide = (item) => { | ||||
|   if (typeof item === `string`) { | ||||
|     item = DOM.el(item) | ||||
|   } | ||||
|  | ||||
|   item.classList.add(`hidden`) | ||||
| } | ||||
							
								
								
									
										1507
									
								
								server/static/dashboard/js/libs/jdenticon.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1507
									
								
								server/static/dashboard/js/libs/jdenticon.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2339
									
								
								server/static/dashboard/js/libs/msg.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2339
									
								
								server/static/dashboard/js/libs/msg.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1019
									
								
								server/static/dashboard/js/libs/needcontext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1019
									
								
								server/static/dashboard/js/libs/needcontext.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										81
									
								
								server/static/dashboard/js/main/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								server/static/dashboard/js/main/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| const App = {} | ||||
|  | ||||
| App.setup = () => { | ||||
|     NeedContext.init() | ||||
|  | ||||
|     Block.setup() | ||||
|     Curls.setup() | ||||
|     Colors.setup() | ||||
|     Infobar.setup() | ||||
|     Container.setup() | ||||
|     Select.setup() | ||||
|     Drag.setup() | ||||
|     Move.setup() | ||||
|     Update.setup() | ||||
|     Sort.setup() | ||||
|     Change.setup() | ||||
|     Picker.setup() | ||||
|     Status.setup() | ||||
|     Filter.setup() | ||||
|     Menu.setup() | ||||
|     More.setup() | ||||
|     Font.setup() | ||||
|     Border.setup() | ||||
|     Dates.setup() | ||||
|     Controls.setup() | ||||
|     Windows.setup() | ||||
|     Footer.setup() | ||||
|     Intro.setup() | ||||
|     Storage.setup() | ||||
|  | ||||
|     App.start_mouse() | ||||
|     App.update_autocomplete() | ||||
|  | ||||
|     Update.do_update() | ||||
| } | ||||
|  | ||||
| App.update_title = () => { | ||||
|     let color = Utils.capitalize(Colors.mode) | ||||
|     document.title = `Curls - ${color}` | ||||
| } | ||||
|  | ||||
| App.start_mouse = () => { | ||||
|     DOM.evs(DOM.el(`#main`), [`mousedown`], (e) => { | ||||
|         App.check_selection(e) | ||||
|     }) | ||||
|  | ||||
|     DOM.ev(window, `mouseup`, (e) => { | ||||
|         Select.mouseup() | ||||
|     }) | ||||
| } | ||||
|  | ||||
| App.update_autocomplete = () => { | ||||
|     let data_list = DOM.el(`#curls_datalist`) | ||||
|     data_list.innerHTML = `` | ||||
|  | ||||
|     for (let word of Curls.get_curls()) { | ||||
|         var option = document.createElement(`option`) | ||||
|         option.value = word | ||||
|         data_list.append(option) | ||||
|     } | ||||
| } | ||||
|  | ||||
| App.check_selection = (e) => { | ||||
|     if (e.ctrlKey || e.shiftKey) { | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (e.button !== 0) { | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (e.target.closest(`.item_icon`)) { | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (e.target.closest(`#infobar_curls`)) { | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     Select.deselect_all() | ||||
| } | ||||
							
								
								
									
										48
									
								
								server/static/dashboard/js/main/block.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server/static/dashboard/js/main/block.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| /* | ||||
|  | ||||
| This is used to rate limit certain operations | ||||
| Every operation adds 1 charge to a registered instance | ||||
| If the charge is above the limit, the operation is blocked | ||||
| Charges are decreased over time | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Block { | ||||
|     static instances = [] | ||||
|     static interval_delay = 2000 | ||||
|     static date_delay = 500 | ||||
|     static relief = 0.1 | ||||
|  | ||||
|     constructor(limit = 180) { | ||||
|         this.limit = limit | ||||
|         this.charge = 0 | ||||
|         this.date = 0 | ||||
|         Block.instances.push(this) | ||||
|     } | ||||
|  | ||||
|     static setup() { | ||||
|         setInterval(() => { | ||||
|             for (let block of Block.instances) { | ||||
|                 if ((Utils.now() - block.date) < this.date_delay) { | ||||
|                     continue | ||||
|                 } | ||||
|  | ||||
|                 if (block.charge > 0) { | ||||
|                     let dec = Math.max(1, Math.round(block.charge * this.relief)) | ||||
|                     block.charge -= parseInt(dec) | ||||
|                 } | ||||
|             } | ||||
|         }, this.interval_delay) | ||||
|     } | ||||
|  | ||||
|     add_charge(num = 1) { | ||||
|         this.date = Utils.now() | ||||
|  | ||||
|         if (this.charge >= this.limit) { | ||||
|             return true | ||||
|         } | ||||
|  | ||||
|         this.charge += num | ||||
|         return false | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								server/static/dashboard/js/main/border.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								server/static/dashboard/js/main/border.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /* | ||||
|  | ||||
| The border between the items of the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Border { | ||||
|     static default_mode = `solid` | ||||
|     static ls_name = `border` | ||||
|  | ||||
|     static modes = [ | ||||
|         {value: `solid`, name: `Solid`, info: `Normal solid border`}, | ||||
|         {value: `dotted`, name: `Dotted`, info: `Dotted border`}, | ||||
|         {value: `dashed`, name: `Dashed`, info: `Dashed border`}, | ||||
|         {value: Utils.separator}, | ||||
|         {value: `none`, name: `None`, info: `No border`}, | ||||
|     ] | ||||
|  | ||||
|     static setup() { | ||||
|         let border = DOM.el(`#border`) | ||||
|         this.mode = this.load_border() | ||||
|  | ||||
|         this.combo = new Combo({ | ||||
|             title: `Border Modes`, | ||||
|             items: this.modes, | ||||
|             value: this.mode, | ||||
|             element: border, | ||||
|             default: this.default_mode, | ||||
|             action: (value) => { | ||||
|                 this.change(value) | ||||
|                 this.apply() | ||||
|             }, | ||||
|             get: () => { | ||||
|                 return this.mode | ||||
|             }, | ||||
|         }) | ||||
|  | ||||
|         this.apply() | ||||
|     } | ||||
|  | ||||
|     static change(value) { | ||||
|         this.mode = value | ||||
|         Utils.save(this.ls_name, value) | ||||
|     } | ||||
|  | ||||
|     static apply() { | ||||
|         let border | ||||
|  | ||||
|         if (this.mode === `solid`) { | ||||
|             border = `1px solid var(--color_alpha_2)` | ||||
|         } | ||||
|         else if (this.mode === `dotted`) { | ||||
|             border = `2px dotted var(--color_alpha_2)` | ||||
|         } | ||||
|         else if (this.mode === `dashed`) { | ||||
|             border = `2px dashed var(--color_alpha_2)` | ||||
|         } | ||||
|         else { | ||||
|             border = `none` | ||||
|         } | ||||
|  | ||||
|         document.documentElement.style.setProperty(`--border`, border) | ||||
|     } | ||||
|  | ||||
|     static load_border() { | ||||
|         return Utils.load_modes(this.ls_name, this.modes, this.default_mode) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										164
									
								
								server/static/dashboard/js/main/change.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								server/static/dashboard/js/main/change.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| /* | ||||
|  | ||||
| This changes the status of a curl | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Change { | ||||
|     static debouncer_delay = 250 | ||||
|     static changing = false | ||||
|     static clear_delay = 800 | ||||
|     static status_max_length = 500 | ||||
|     static key_length = 22 | ||||
|  | ||||
|     static setup() { | ||||
|         let curl = DOM.el(`#change_curl`) | ||||
|         let key = DOM.el(`#change_key`) | ||||
|         let submit = DOM.el(`#change_submit`) | ||||
|  | ||||
|         DOM.ev(submit, `click`, () => { | ||||
|             this.change() | ||||
|         }) | ||||
|  | ||||
|         this.debouncer = Utils.create_debouncer(() => { | ||||
|             this.do_change() | ||||
|         }, this.debouncer_delay) | ||||
|  | ||||
|         DOM.ev(curl, `keyup`, (e) => { | ||||
|             if (e.key === `Enter`) { | ||||
|                 this.change() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(curl, `focus`, (e) => { | ||||
|             let value = curl.value | ||||
|  | ||||
|             if (value) { | ||||
|                 Select.curl(value) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(curl, `blur`, (e) => { | ||||
|             Select.deselect_all() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(curl, `wheel`, (e) => { | ||||
|             Utils.scroll_wheel(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(key, `keyup`, (e) => { | ||||
|             if (e.key === `Enter`) { | ||||
|                 this.change() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(key, `wheel`, (e) => { | ||||
|             Utils.scroll_wheel(e) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static change() { | ||||
|         this.debouncer.call() | ||||
|     } | ||||
|  | ||||
|     static do_change() { | ||||
|         this.debouncer.cancel() | ||||
|         Utils.info(`Change: Trigger`) | ||||
|  | ||||
|         if (this.changing) { | ||||
|             Utils.error(`Slow down`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let curl = DOM.el(`#change_curl`).value.toLowerCase() | ||||
|         let key = DOM.el(`#change_key`).value | ||||
|         let status = DOM.el(`#change_status`).value.trim() | ||||
|  | ||||
|         if (!curl || !key || !status) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (curl.length > Curls.max_length) { | ||||
|             Utils.error(Utils.curl_too_long) | ||||
|             Windows.alert({title: `Error`, message: Utils.curl_too_long}) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (key.length > this.key_length) { | ||||
|             Utils.error(Utils.key_too_long) | ||||
|             Windows.alert({title: `Error`, message: Utils.key_too_long}) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (status.length > this.status_max_length) { | ||||
|             Utils.error(Utils.status_too_long) | ||||
|             Windows.alert({title: `Error`, message: Utils.status_too_long}) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let url = `/change` | ||||
|         let params = new URLSearchParams() | ||||
|  | ||||
|         params.append(`curl`, curl) | ||||
|         params.append(`key`, key) | ||||
|         params.append(`status`, status) | ||||
|  | ||||
|         this.show_changing() | ||||
|         Status.save(status) | ||||
|         this.changing = true | ||||
|         Utils.info(`Change: Request ${Utils.network}`) | ||||
|  | ||||
|         fetch(url, { | ||||
|             method: `POST`, | ||||
|             headers: { | ||||
|                 "Content-Type": `application/x-www-form-urlencoded` | ||||
|             }, | ||||
|             body: params, | ||||
|         }) | ||||
|             .then(response => response.text()) | ||||
|             .then(ans => { | ||||
|                 Utils.info(`Response: ${ans}`) | ||||
|                 this.clear_changing() | ||||
|  | ||||
|                 if (ans === `ok`) { | ||||
|                     this.clear_status() | ||||
|                     Update.update({ curls: [curl] }) | ||||
|                     Curls.add_owned(curl) | ||||
|                     Picker.add() | ||||
|                 } | ||||
|                 else { | ||||
|                     let lines = [ | ||||
|                         `You might have hit the rate limit`, | ||||
|                         `Or the curl and key you used are incorrect` | ||||
|                     ] | ||||
|  | ||||
|                     let msg = lines.join(`\n`) | ||||
|                     Windows.alert({message: msg}) | ||||
|                 } | ||||
|             }) | ||||
|             .catch(e => { | ||||
|                 Utils.error(`Failed to change`) | ||||
|                 Utils.error(e) | ||||
|                 this.clear_changing() | ||||
|             }) | ||||
|     } | ||||
|  | ||||
|     static clear_status() { | ||||
|         DOM.el(`#change_status`).value = `` | ||||
|     } | ||||
|  | ||||
|     static show_changing() { | ||||
|         let button = DOM.el(`#change_submit`) | ||||
|         clearTimeout(this.clear_changing_timeout) | ||||
|         button.classList.add(`active`) | ||||
|     } | ||||
|  | ||||
|     static clear_changing() { | ||||
|         this.changing = false | ||||
|  | ||||
|         this.clear_changing_timeout = setTimeout(() => { | ||||
|             let button = DOM.el(`#change_submit`) | ||||
|             button.classList.remove(`active`) | ||||
|         }, this.clear_delay) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										188
									
								
								server/static/dashboard/js/main/colors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								server/static/dashboard/js/main/colors.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| /* | ||||
|  | ||||
| Color functions | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Colors { | ||||
|     static default_mode = `green` | ||||
|     static ls_name = `color` | ||||
|     static alpha_0 = {} | ||||
|     static alpha_1 = {} | ||||
|     static alpha_2 = {} | ||||
|  | ||||
|     static modes = [ | ||||
|         {value: `green`, name: `Green`, info: `Go to Green`, icon: `🟢`}, | ||||
|         {value: `blue`, name: `Blue`, info: `Go to Blue`, icon: `🔵`}, | ||||
|         {value: `red`, name: `Red`, info: `Go to Red`, icon: `🔴`}, | ||||
|         {value: `yellow`, name: `Yellow`, info: `Go to Yellow`, icon: `🟡`}, | ||||
|         {value: `purple`, name: `Purple`, info: `Go to Purple`, icon: `🟣`}, | ||||
|         {value: `white`, name: `White`, info: `Go to White`, icon: `⚪`}, | ||||
|     ] | ||||
|  | ||||
|     static colors = { | ||||
|         red: `rgb(255, 89, 89)`, | ||||
|         green: `rgb(87, 255, 87)`, | ||||
|         blue: `rgb(118, 120, 255)`, | ||||
|         yellow: `rgb(255, 219, 78)`, | ||||
|         purple: `rgb(193, 56, 255)`, | ||||
|         white: `rgb(255, 255, 255)`, | ||||
|     } | ||||
|  | ||||
|     static setup() { | ||||
|         let color = DOM.el(`#color`) | ||||
|         this.mode = this.load_color() | ||||
|  | ||||
|         this.combo = new Combo({ | ||||
|             title: `Color Modes`, | ||||
|             items: this.modes, | ||||
|             value: this.mode, | ||||
|             element: color, | ||||
|             default: this.default_mode, | ||||
|             action: (value) => { | ||||
|                 this.change(value) | ||||
|             }, | ||||
|             get: () => { | ||||
|                 return this.mode | ||||
|             }, | ||||
|             extra_title: `Ctrl Left/Right to cycle`, | ||||
|         }) | ||||
|  | ||||
|         this.make_alpha(this.alpha_0, `0.055`) | ||||
|         this.make_alpha(this.alpha_1, `0.18`) | ||||
|         this.make_alpha(this.alpha_2, `0.5`) | ||||
|  | ||||
|         this.apply() | ||||
|     } | ||||
|  | ||||
|     static make_alpha(obj, a) { | ||||
|         for (let color in this.colors) { | ||||
|             let numbers = this.colors[color].match(/\d+/g) | ||||
|             let rgba = `rgba(${numbers[0]}, ${numbers[1]}, ${numbers[2]}, ${a})` | ||||
|             obj[color] = rgba | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static set_value(value) { | ||||
|         if (this.mode === value) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.combo.set_value(value) | ||||
|     } | ||||
|  | ||||
|     static change(value) { | ||||
|         if (this.mode === value) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.mode = value | ||||
|         Utils.save(this.ls_name, value) | ||||
|         this.apply() | ||||
|         Items.reset() | ||||
|         Container.show_loading() | ||||
|         Infobar.hide() | ||||
|         Update.update() | ||||
|     } | ||||
|  | ||||
|     static load_color() { | ||||
|         return Utils.load_modes(this.ls_name, this.modes, this.default_mode) | ||||
|     } | ||||
|  | ||||
|     static apply() { | ||||
|         let normal = this.colors[this.mode] | ||||
|  | ||||
|         let alpha_0 = this.alpha_0[this.mode] | ||||
|         let alpha_1 = this.alpha_1[this.mode] | ||||
|         let alpha_2 = this.alpha_2[this.mode] | ||||
|  | ||||
|         document.documentElement.style.setProperty(`--color`, normal) | ||||
|         document.documentElement.style.setProperty(`--color_alpha_0`, alpha_0) | ||||
|         document.documentElement.style.setProperty(`--color_alpha_1`, alpha_1) | ||||
|         document.documentElement.style.setProperty(`--color_alpha_2`, alpha_2) | ||||
|  | ||||
|         App.update_title() | ||||
|     } | ||||
|  | ||||
|     static move(curls, e) { | ||||
|         let items = [] | ||||
|  | ||||
|         let add = (mode) => { | ||||
|             if (this.mode === mode.value) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             items.push({ | ||||
|                 text: mode.name, | ||||
|                 action: () => { | ||||
|                     this.do_move(mode.value, curls) | ||||
|                 }, | ||||
|                 icon: mode.icon, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         for (let key in this.modes) { | ||||
|             add(this.modes[key]) | ||||
|         } | ||||
|  | ||||
|         Utils.context({items: items, e: e}) | ||||
|     } | ||||
|  | ||||
|     static do_move(color, curls) { | ||||
|         let current = Curls.get_curls() | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let curl of current) { | ||||
|             if (!curls.includes(curl)) { | ||||
|                 cleaned.push(curl) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let cleaned_items = [] | ||||
|  | ||||
|         for (let item of Items.list) { | ||||
|             if (!curls.includes(item.curl)) { | ||||
|                 cleaned_items.push(item) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Items.list = cleaned_items | ||||
|         Curls.save_curls(cleaned) | ||||
|         let new_curls = Curls.get_curls(color) | ||||
|  | ||||
|         for (let curl of curls) { | ||||
|             if (new_curls.includes(curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             new_curls.unshift(curl) | ||||
|         } | ||||
|  | ||||
|         Curls.save_curls(new_curls, color) | ||||
|         Container.update() | ||||
|     } | ||||
|  | ||||
|     static prev() { | ||||
|         let index = this.modes.findIndex(x => x.value === this.mode) | ||||
|         let prev = index - 1 | ||||
|  | ||||
|         if (prev < 0) { | ||||
|             prev = this.modes.length - 1 | ||||
|         } | ||||
|  | ||||
|         let value = this.modes[prev].value | ||||
|         this.set_value(value) | ||||
|     } | ||||
|  | ||||
|     static next() { | ||||
|         let index = this.modes.findIndex(x => x.value === this.mode) | ||||
|         let next = index + 1 | ||||
|  | ||||
|         if (next >= this.modes.length) { | ||||
|             next = 0 | ||||
|         } | ||||
|  | ||||
|         let value = this.modes[next].value | ||||
|         this.set_value(value) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										130
									
								
								server/static/dashboard/js/main/combo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								server/static/dashboard/js/main/combo.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| /* | ||||
|  | ||||
| This is a button widget | ||||
| It can be used to cycle through a list of items | ||||
| It uses NeedContext to show the menu | ||||
| It's similar to a select widget | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Combo { | ||||
|     constructor(args) { | ||||
|         this.args = args | ||||
|         this.prepare() | ||||
|     } | ||||
|  | ||||
|     prepare() { | ||||
|         DOM.evs(this.args.element, [`click`, `contextmenu`], (e) => { | ||||
|             this.show_menu(e) | ||||
|             e.preventDefault() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(this.args.element, `auxclick`, (e) => { | ||||
|             if (e.button === 1) { | ||||
|                 this.reset() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(this.args.element, `wheel`, (e) => { | ||||
|             let direction = Utils.wheel_direction(e) | ||||
|             this.cycle(direction) | ||||
|             e.preventDefault() | ||||
|         }) | ||||
|  | ||||
|         let lines = [ | ||||
|             this.args.title, | ||||
|             `Click to pick option`, | ||||
|             `Wheel to cycle option`, | ||||
|             `Middle Click to reset`, | ||||
|         ] | ||||
|  | ||||
|         this.args.element.title = lines.join(`\n`) | ||||
|  | ||||
|         if (this.args.extra_title) { | ||||
|             this.args.element.title += `\n${this.args.extra_title}` | ||||
|         } | ||||
|  | ||||
|         this.block = new Block() | ||||
|         this.update_text() | ||||
|     } | ||||
|  | ||||
|     get_item() { | ||||
|         return this.args.items.find(x => x.value === this.args.get()) | ||||
|     } | ||||
|  | ||||
|     update_text() { | ||||
|         let item = this.get_item(this.args) | ||||
|         this.args.element.textContent = item.name | ||||
|     } | ||||
|  | ||||
|     show_menu(e) { | ||||
|         let items = [] | ||||
|         let current = this.args.get() | ||||
|  | ||||
|         for (let item of this.args.items) { | ||||
|             if (item.value === Utils.separator) { | ||||
|                 items.push({ separator: true }) | ||||
|             } | ||||
|             else { | ||||
|                 items.push({ | ||||
|                     text: item.name, | ||||
|                     action: () => { | ||||
|                         this.action(item.value) | ||||
|                     }, | ||||
|                     selected: item.value === current, | ||||
|                     info: item.info, | ||||
|                     icon: item.icon, | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Utils.context({ items: items, e: e, input: this.args.input }) | ||||
|     } | ||||
|  | ||||
|     action(value) { | ||||
|         this.args.action(value) | ||||
|         this.update_text() | ||||
|     } | ||||
|  | ||||
|     reset() { | ||||
|         this.action(this.args.default) | ||||
|     } | ||||
|  | ||||
|     get_values() { | ||||
|         return this.args.items | ||||
|             .filter(x => x.value !== Utils.separator) | ||||
|             .filter(x => !x.skip) | ||||
|             .map(x => x.value) | ||||
|     } | ||||
|  | ||||
|     cycle(direction) { | ||||
|         if (this.block.add_charge()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let value = this.args.get() | ||||
|         let values = this.get_values(this.args) | ||||
|         let index = values.indexOf(value) | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             index -= 1 | ||||
|         } | ||||
|         else if (direction === `down`) { | ||||
|             index += 1 | ||||
|         } | ||||
|  | ||||
|         if (index < 0) { | ||||
|             index = values.length - 1 | ||||
|         } | ||||
|         else if (index >= values.length) { | ||||
|             index = 0 | ||||
|         } | ||||
|  | ||||
|         let new_value = values[index] | ||||
|         this.action(new_value) | ||||
|     } | ||||
|  | ||||
|     set_value(value) { | ||||
|         this.action(value) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										452
									
								
								server/static/dashboard/js/main/container.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										452
									
								
								server/static/dashboard/js/main/container.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,452 @@ | ||||
| /* | ||||
|  | ||||
| This is the main container widget with the vertical items | ||||
| Most action happens here | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Container { | ||||
|     static wrap_enabled = true | ||||
|     static ls_wrap = `wrap_enabled` | ||||
|     static scroll_step = 100 | ||||
|  | ||||
|     static setup() { | ||||
|         this.empty_info = [ | ||||
|             `Add some curls to the list by using the menu.`, | ||||
|             `These will be monitored for status changes.`, | ||||
|             `Above you can change the status of your own curls.`, | ||||
|             `Each color has its own set of curls.`, | ||||
|         ].join(`<br>`) | ||||
|  | ||||
|         let outer = this.get_outer() | ||||
|         let container = this.get_container() | ||||
|  | ||||
|         DOM.ev(container, `mousedown`, (e) => { | ||||
|             if (e.ctrlKey || e.shiftKey) { | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(container, `click`, (e) => { | ||||
|             let item = this.extract_item(e) | ||||
|  | ||||
|             if (!item) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             if (this.extract_updated(e)) { | ||||
|                 Dates.change_mode() | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             this.focus() | ||||
|             let is_icon = this.extract_icon(e) | ||||
|  | ||||
|             if (e.shiftKey) { | ||||
|                 Select.range(item) | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else if (e.ctrlKey) { | ||||
|                 Select.toggle(item) | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else { | ||||
|                 if (is_icon) { | ||||
|                     Select.single(item) | ||||
|                     e.preventDefault() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(container, `auxclick`, (e) => { | ||||
|             let item = this.extract_item(e) | ||||
|  | ||||
|             if (!item) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             if (e.button == 1) { | ||||
|                 let curl = this.extract_curl(item) | ||||
|                 Select.check(item) | ||||
|                 Curls.remove_selected(curl) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(container, `contextmenu`, (e) => { | ||||
|             let item = this.extract_item(e) | ||||
|  | ||||
|             if (!item) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             let curl = this.extract_curl(item) | ||||
|             Select.check(item) | ||||
|             Items.show_menu({curl: curl, e: e}) | ||||
|             e.preventDefault() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(outer, `contextmenu`, (e) => { | ||||
|             let item = this.extract_item(e) | ||||
|             e.preventDefault() | ||||
|  | ||||
|             if (item) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             Menu.show(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(outer, `click`, (e) => { | ||||
|             this.focus() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(outer, `mousedown`, (e) => { | ||||
|             Select.mousedown(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(outer, `mouseup`, () => { | ||||
|             Select.mouseup() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(outer, `mouseover`, (e) => { | ||||
|             Select.mouseover(e) | ||||
|         }) | ||||
|  | ||||
|         this.wrap_enabled = this.load_wrap_enabled() | ||||
|         this.setup_keyboard() | ||||
|         this.focus() | ||||
|     } | ||||
|  | ||||
|     static clear() { | ||||
|         let container = this.get_container() | ||||
|         container.innerHTML = `` | ||||
|     } | ||||
|  | ||||
|     static show_empty() { | ||||
|         Infobar.hide() | ||||
|         this.set_info(this.empty_info) | ||||
|     } | ||||
|  | ||||
|     static check_empty() { | ||||
|         let els = this.get_items() | ||||
|  | ||||
|         if (!els || !els.length) { | ||||
|             this.show_empty() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static show_loading() { | ||||
|         this.set_info(`Loading...`) | ||||
|     } | ||||
|  | ||||
|     static set_info(info) { | ||||
|         let container = this.get_container() | ||||
|         let item = DOM.create(`div`, `info_item`) | ||||
|         item.innerHTML = info | ||||
|         container.innerHTML = `` | ||||
|         container.append(item) | ||||
|         Utils.deselect() | ||||
|     } | ||||
|  | ||||
|     static get_items() { | ||||
|         return DOM.els(`#container .item`) | ||||
|     } | ||||
|  | ||||
|     static scroll_top() { | ||||
|         let item = this.get_items()[0] | ||||
|         Utils.scroll_element({item: item, behavior: `smooth`, block: `center`}) | ||||
|     } | ||||
|  | ||||
|     static scroll_bottom() { | ||||
|         let item = Utils.last(this.get_items()) | ||||
|         Utils.scroll_element({item: item, behavior: `smooth`, block: `center`}) | ||||
|     } | ||||
|  | ||||
|     static save_wrap_enabled() { | ||||
|         Utils.save(this.ls_wrap, this.wrap_enabled) | ||||
|     } | ||||
|  | ||||
|     static load_wrap_enabled() { | ||||
|         return Utils.load_boolean(this.ls_wrap) | ||||
|     } | ||||
|  | ||||
|     static add(items, curls) { | ||||
|         let normal = Items.list.filter(item => !item.missing) | ||||
|         Items.list = [...items] | ||||
|  | ||||
|         for (let item of normal) { | ||||
|             if (Items.list.find(x => x.curl === item.curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             Items.list.push(item) | ||||
|         } | ||||
|  | ||||
|         let missing = Items.find_missing() | ||||
|         Items.list.push(...missing) | ||||
|         Items.fill() | ||||
|         this.update({select: curls}) | ||||
|     } | ||||
|  | ||||
|     static insert(items) { | ||||
|         Items.list = items | ||||
|         Items.list.map(x => x.missing = false) | ||||
|         let missing = Items.find_missing() | ||||
|         Items.list.push(...missing) | ||||
|         Items.fill() | ||||
|         this.update() | ||||
|     } | ||||
|  | ||||
|     static update(args = {}) { | ||||
|         let def_args = { | ||||
|             items: Items.list, | ||||
|             check_filter: true, | ||||
|             select: [], | ||||
|         } | ||||
|  | ||||
|         Utils.def_args(def_args, args) | ||||
|         Utils.info(`Updating Container`) | ||||
|         this.clear() | ||||
|         Sort.sort(args.items) | ||||
|  | ||||
|         for (let item of args.items) { | ||||
|             this.create_element(item) | ||||
|         } | ||||
|  | ||||
|         Utils.deselect() | ||||
|         this.check_empty() | ||||
|  | ||||
|         if (args.check_filter) { | ||||
|             Filter.check() | ||||
|         } | ||||
|  | ||||
|         if (args.select.length) { | ||||
|             Select.curls(args.select) | ||||
|         } | ||||
|  | ||||
|         Infobar.update() | ||||
|     } | ||||
|  | ||||
|     static create_element(item) { | ||||
|         let container = this.get_container() | ||||
|         let el = DOM.create(`div`, `item`) | ||||
|         let item_icon = DOM.create(`div`, `item_icon`) | ||||
|         item_icon.draggable = true | ||||
|  | ||||
|         let lines = [ | ||||
|             `Click to select`, | ||||
|             `Ctrl Click to toggle`, | ||||
|             `Shift Click to select range`, | ||||
|             `Middle Click to remove`, | ||||
|             `Drag to reorder`, | ||||
|         ] | ||||
|  | ||||
|         item_icon.title = lines.join(`\n`) | ||||
|  | ||||
|         let canvas = DOM.create(`canvas`, `item_icon_canvas`) | ||||
|         jdenticon.update(canvas, item.curl) | ||||
|         item_icon.append(canvas) | ||||
|  | ||||
|         let item_curl = DOM.create(`div`, `item_curl`) | ||||
|         let item_status = DOM.create(`div`, `item_status`) | ||||
|  | ||||
|         if (!this.wrap_enabled) { | ||||
|             item_status.classList.add(`nowrap`) | ||||
|         } | ||||
|  | ||||
|         item_curl.textContent = item.curl | ||||
|         item_curl.title = item.curl | ||||
|         let status = item.status || `Not updated yet` | ||||
|         item_status.innerHTML = Utils.sanitize(status) | ||||
|         Utils.urlize(item_status) | ||||
|         let item_updated = DOM.create(`div`, `item_updated glow`) | ||||
|  | ||||
|         let dates = [ | ||||
|             `Updated: ${item.updated_text}`, | ||||
|             `Added: ${item.added_text}`, | ||||
|             `Created: ${item.created_text}`, | ||||
|         ] | ||||
|  | ||||
|         let date_text = dates.join(`\n`) | ||||
|  | ||||
|         if (Dates.enabled) { | ||||
|             item_status.title = status | ||||
|  | ||||
|             if (item.missing) { | ||||
|                 item_updated.textContent = `No Date` | ||||
|                 item_updated.title = `No date information available` | ||||
|             } | ||||
|             else { | ||||
|                 item_updated.textContent = item.updated_text | ||||
|  | ||||
|                 let lines_2 = [ | ||||
|                     date_text, | ||||
|                     `Click to toggle between 12 and 24 hours`, | ||||
|                 ] | ||||
|  | ||||
|                 item_updated.title = lines_2.join(`\n`) | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             item_status.title = `${status}\n${date_text}` | ||||
|             item_updated.classList.add(`hidden`) | ||||
|         } | ||||
|  | ||||
|         if (!item.missing) { | ||||
|             item_status.title += `\nChanges: ${item.changes}` | ||||
|         } | ||||
|  | ||||
|         el.append(item_icon) | ||||
|         el.append(item_curl) | ||||
|         el.append(item_status) | ||||
|         el.append(item_updated) | ||||
|  | ||||
|         el.dataset.curl = item.curl | ||||
|         el.dataset.selected_id = 0 | ||||
|  | ||||
|         container.append(el) | ||||
|         container.append(el) | ||||
|  | ||||
|         item.element = el | ||||
|     } | ||||
|  | ||||
|     static extract_curl(item) { | ||||
|         return item.dataset.curl | ||||
|     } | ||||
|  | ||||
|     static setup_keyboard() { | ||||
|         let container = this.get_container() | ||||
|  | ||||
|         DOM.ev(container, `keydown`, (e) => { | ||||
|             if (e.key === `Delete`) { | ||||
|                 Curls.remove_selected() | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else if (e.key === `ArrowUp`) { | ||||
|                 if (e.ctrlKey) { | ||||
|                     Move.up() | ||||
|                 } | ||||
|                 else { | ||||
|                     Select.vertical(`up`, e.shiftKey) | ||||
|                 } | ||||
|  | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else if (e.key === `ArrowDown`) { | ||||
|                 if (e.ctrlKey) { | ||||
|                     Move.down() | ||||
|                 } | ||||
|                 else { | ||||
|                     Select.vertical(`down`, e.shiftKey) | ||||
|                 } | ||||
|  | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else if (e.key === `ArrowLeft`) { | ||||
|                 if (e.ctrlKey) { | ||||
|                     Colors.prev() | ||||
|                     e.preventDefault() | ||||
|                 } | ||||
|             } | ||||
|             else if (e.key === `ArrowRight`) { | ||||
|                 if (e.ctrlKey) { | ||||
|                     Colors.next() | ||||
|                     e.preventDefault() | ||||
|                 } | ||||
|             } | ||||
|             else if (e.key === `Escape`) { | ||||
|                 Select.deselect_all() | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else if (e.key === `a`) { | ||||
|                 if (e.ctrlKey) { | ||||
|                     Select.all() | ||||
|                     e.preventDefault() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static is_visible(item) { | ||||
|         return !item.classList.contains(`hidden`) | ||||
|     } | ||||
|  | ||||
|     static get_visible() { | ||||
|         let items = this.get_items() | ||||
|         return items.filter(x => this.is_visible(x)) | ||||
|     } | ||||
|  | ||||
|     static get_curls() { | ||||
|         let items = this.get_items() | ||||
|         return items.map(item => Container.extract_curl(item)) | ||||
|     } | ||||
|  | ||||
|     static get_item(curl) { | ||||
|         let items = this.get_items() | ||||
|         return items.find(x => x.dataset.curl === curl) | ||||
|     } | ||||
|  | ||||
|     static focus() { | ||||
|         this.get_container().focus() | ||||
|     } | ||||
|  | ||||
|     static get_outer() { | ||||
|         return DOM.el(`#container_outer`) | ||||
|     } | ||||
|  | ||||
|     static get_container() { | ||||
|         return DOM.el(`#container`) | ||||
|     } | ||||
|  | ||||
|     static extract_item(e) { | ||||
|         return e.target.closest(`.item`) | ||||
|     } | ||||
|  | ||||
|     static extract_icon(e) { | ||||
|         return e.target.closest(`.item_icon`) | ||||
|     } | ||||
|  | ||||
|     static extract_updated(e) { | ||||
|         return e.target.closest(`.item_updated`) | ||||
|     } | ||||
|  | ||||
|     static scroll_up() { | ||||
|         let outer = this.get_outer() | ||||
|         outer.scrollBy(0, -this.scroll_step) | ||||
|     } | ||||
|  | ||||
|     static scroll_down() { | ||||
|         let outer = this.get_outer() | ||||
|         outer.scrollBy(0, this.scroll_step) | ||||
|     } | ||||
|  | ||||
|     static scroller() { | ||||
|         let outer = this.get_outer() | ||||
|         let height = outer.clientHeight | ||||
|         let scroll = outer.scrollHeight | ||||
|         let scrolltop = outer.scrollTop | ||||
|  | ||||
|         if (scrolltop < (scroll - height)) { | ||||
|             this.scroll_bottom() | ||||
|         } | ||||
|         else { | ||||
|             this.scroll_top() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static scroll(e) { | ||||
|         let direction = Utils.wheel_direction(e) | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             Container.scroll_up() | ||||
|         } | ||||
|         else { | ||||
|             Container.scroll_down() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static save_curls() { | ||||
|         let curls = Container.get_curls() | ||||
|         return Curls.save_curls(curls) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								server/static/dashboard/js/main/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/static/dashboard/js/main/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| /* | ||||
|  | ||||
| This shows or hides the controls | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Controls { | ||||
|     static enabled = true | ||||
|     static ls_name = `controls_enabled` | ||||
|  | ||||
|     static setup() { | ||||
|         this.enabled = this.load_enabled() | ||||
|         this.check_enabled() | ||||
|     } | ||||
|  | ||||
|     static save_enabled() { | ||||
|         Utils.save(this.ls_name, this.enabled) | ||||
|     } | ||||
|  | ||||
|     static load_enabled() { | ||||
|         return Utils.load_boolean(this.ls_name) | ||||
|     } | ||||
|  | ||||
|     static check_enabled() { | ||||
|         if (this.enabled) { | ||||
|             DOM.show(`#controls`) | ||||
|         } | ||||
|         else { | ||||
|             DOM.hide(`#controls`) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										521
									
								
								server/static/dashboard/js/main/curls.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										521
									
								
								server/static/dashboard/js/main/curls.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,521 @@ | ||||
| /* | ||||
|  | ||||
| These are curl operations | ||||
| This takes care of storing curl data | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Curls { | ||||
|     static max_curls = 100 | ||||
|     static max_length = 20 | ||||
|     static old_delay = Utils.YEAR * 1 | ||||
|     static colors = {} | ||||
|  | ||||
|     static setup() { | ||||
|         this.fill_colors() | ||||
|     } | ||||
|  | ||||
|     static fill_colors() { | ||||
|         for (let color in Colors.colors) { | ||||
|             this.colors[color] = this.load_curls(color) | ||||
|  | ||||
|             if (this.fill(this.colors[color])) { | ||||
|                 this.save(this.colors[color], color, true) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static add() { | ||||
|         Windows.prompt({title: `Add Curls`, callback: (value) => { | ||||
|             this.add_submit(value) | ||||
|         }, message: `Enter one or more curls`}) | ||||
|     } | ||||
|  | ||||
|     static add_submit(curls) { | ||||
|         if (!curls) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let added = Utils.smart_list(curls) | ||||
|  | ||||
|         if (!added.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.prepend(added) | ||||
|     } | ||||
|  | ||||
|     static prepend(added) { | ||||
|         added = added.filter(x => this.check(x)) | ||||
|  | ||||
|         if (!added.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let curls = this.get_curls() | ||||
|         let new_curls = Array.from(new Set([...added, ...curls])) | ||||
|  | ||||
|         if (this.save_curls(new_curls)) { | ||||
|             added.reverse() | ||||
|             Update.update({ curls: added }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static new_item(curl) { | ||||
|         return { | ||||
|             curl: curl, | ||||
|             added: this.default_added(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static add_owned(curl) { | ||||
|         let curls = this.get_curls() | ||||
|  | ||||
|         if (curls.includes(curl)) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.prepend([curl]) | ||||
|     } | ||||
|  | ||||
|     static to_top(curls) { | ||||
|         let cleaned = [...curls] | ||||
|  | ||||
|         for (let curl of this.get_curls()) { | ||||
|             if (cleaned.includes(curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(curl) | ||||
|         } | ||||
|  | ||||
|         this.after_move(cleaned, curls) | ||||
|     } | ||||
|  | ||||
|     static to_bottom(curls) { | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let curl of this.get_curls()) { | ||||
|             if (cleaned.includes(curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             if (curls.includes(curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(curl) | ||||
|         } | ||||
|  | ||||
|         cleaned.push(...curls) | ||||
|         this.after_move(cleaned, curls) | ||||
|     } | ||||
|  | ||||
|     static after_move(new_curls, curls) { | ||||
|         this.save_curls(new_curls) | ||||
|         Sort.set_value(`order`) | ||||
|         Sort.sort_if_order() | ||||
|         Select.deselect_all() | ||||
|  | ||||
|         for (let curl of curls) { | ||||
|             Select.curl(curl) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static save(items, color = Colors.mode, force = false) { | ||||
|         items = this.clean(items) | ||||
|         let same = true | ||||
|         let current = this.get(color) | ||||
|  | ||||
|         if (current.length !== items.length) { | ||||
|             same = false | ||||
|         } | ||||
|  | ||||
|         if (same) { | ||||
|             for (let i = 0; i < current.length; i++) { | ||||
|                 if (current[i].curl !== items[i].curl) { | ||||
|                     same = false | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (same && !force) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         let name = this.get_name(color) | ||||
|         this.colors[color] = [...items] | ||||
|         Utils.save(name, JSON.stringify(items)) | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     static default_added() { | ||||
|         return Utils.now() | ||||
|     } | ||||
|  | ||||
|     static fill(items) { | ||||
|         let filled = false | ||||
|  | ||||
|         for (let item of items) { | ||||
|             if (item.added === undefined) { | ||||
|                 item.added = this.default_added() | ||||
|                 filled = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return filled | ||||
|     } | ||||
|  | ||||
|     static get(color = Colors.mode) { | ||||
|         return this.colors[color] | ||||
|     } | ||||
|  | ||||
|     static get_curls(color = Colors.mode) { | ||||
|         return this.get(color).map(x => x.curl) | ||||
|     } | ||||
|  | ||||
|     static load_curls(color = Colors.mode) { | ||||
|         let name = this.get_name(color) | ||||
|         let saved = Utils.load_array(name) | ||||
|         return this.clean(saved) | ||||
|     } | ||||
|  | ||||
|     static replace() { | ||||
|         Windows.prompt({title: `Replace Curls`, callback: (value) => { | ||||
|             this.replace_submit(value) | ||||
|         }, message: `Replace the entire list with this`}) | ||||
|     } | ||||
|  | ||||
|     static replace_submit(curls) { | ||||
|         if (!curls) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let units = curls.split(` `).filter(x => x) | ||||
|  | ||||
|         if (!units.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.clear() | ||||
|         let added = [] | ||||
|  | ||||
|         for (let curl of units) { | ||||
|             if (this.check(curl)) { | ||||
|                 added.push(curl) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (added) { | ||||
|             if (this.save_curls(added)) { | ||||
|                 Update.update() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static clear(color = Colors.mode) { | ||||
|         this.save([], color) | ||||
|     } | ||||
|  | ||||
|     static edit(curl) { | ||||
|         Windows.prompt({title: `Edit Curl`, callback: (value) => { | ||||
|             this.edit_submit(curl, value) | ||||
|         }, value: curl, message: `Change the name of this curl`}) | ||||
|     } | ||||
|  | ||||
|     static edit_submit(curl, new_curl) { | ||||
|         if (!new_curl) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.do_edit(curl, new_curl) | ||||
|     } | ||||
|  | ||||
|     static do_edit(curl, new_curl) { | ||||
|         if (!this.check(new_curl)) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (curl === new_curl) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let curls = this.get_curls().slice() | ||||
|         let index = curls.indexOf(curl) | ||||
|  | ||||
|         if (index === -1) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         curls[index] = new_curl | ||||
|  | ||||
|         if (this.save_curls(curls)) { | ||||
|             Items.remove_curl(curl) | ||||
|             Update.update({ curls: [new_curl] }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static check(curl) { | ||||
|         if (!curl) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         if (curl.length > this.max_length) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         if (!/^[a-zA-Z0-9]+$/.test(curl)) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     static clean(items) { | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let item of items) { | ||||
|             if (cleaned.some(x => x.curl === item.curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             if (!this.check(item.curl)) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(item) | ||||
|  | ||||
|             if (cleaned.length >= this.max_curls) { | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return cleaned | ||||
|     } | ||||
|  | ||||
|     static get_name(color) { | ||||
|         return `curls_${color}` | ||||
|     } | ||||
|  | ||||
|     static remove(curls) { | ||||
|         let cleaned = [] | ||||
|         let removed = [] | ||||
|  | ||||
|         for (let curl of this.get_curls()) { | ||||
|             if (!curls.includes(curl)) { | ||||
|                 cleaned.push(curl) | ||||
|             } | ||||
|             else { | ||||
|                 removed.push(curl) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!removed.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.save_cleaned(cleaned, removed) | ||||
|     } | ||||
|  | ||||
|     static remove_selected(curl = ``) { | ||||
|         let curls = Select.get_curls() | ||||
|  | ||||
|         if (curl) { | ||||
|             if (!curls.includes(curl)) { | ||||
|                 curls = [curl] | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.remove(curls) | ||||
|     } | ||||
|  | ||||
|     static remove_all() { | ||||
|         Windows.confirm({title: `Remove All Curls`, ok: () => { | ||||
|             this.clear() | ||||
|             Container.show_empty() | ||||
|         }, message: `Remove all curls in the current color`}) | ||||
|     } | ||||
|  | ||||
|     static show_remove_menu(e) { | ||||
|         let items = [ | ||||
|             { | ||||
|                 text: `Remove One`, | ||||
|                 action: () => { | ||||
|                     this.remove_one() | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 text: `Remove Not Found`, | ||||
|                 action: () => { | ||||
|                     this.remove_not_found() | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 text: `Remove Empty`, | ||||
|                 action: () => { | ||||
|                     this.remove_empty() | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 text: `Remove Old`, | ||||
|                 action: () => { | ||||
|                     this.remove_old() | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 text: `Remove All`, | ||||
|                 action: () => { | ||||
|                     this.remove_all() | ||||
|                 } | ||||
|             }, | ||||
|         ] | ||||
|  | ||||
|         Utils.context({items: items, e: e}) | ||||
|     } | ||||
|  | ||||
|     static remove_one() { | ||||
|         Windows.prompt({title: `Remove Curl`, callback: (value) => { | ||||
|             this.remove_one_submit(value) | ||||
|         }, message: `Enter the curl to remove`}) | ||||
|     } | ||||
|  | ||||
|     static remove_one_submit(curl) { | ||||
|         if (!curl) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.do_remove(curl) | ||||
|     } | ||||
|  | ||||
|     static do_remove(curl, remove_item = true) { | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let curl_ of this.get_curls()) { | ||||
|             if (curl_ !== curl) { | ||||
|                 cleaned.push(curl_) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.save_curls(cleaned) | ||||
|  | ||||
|         if (remove_item) { | ||||
|             Items.remove([curl]) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static remove_not_found() { | ||||
|         let missing = Items.get_missing().map(x => x.curl) | ||||
|         let cleaned = [] | ||||
|         let removed = [] | ||||
|  | ||||
|         for (let curl of this.get_curls()) { | ||||
|             if (!missing.includes(curl)) { | ||||
|                 cleaned.push(curl) | ||||
|             } | ||||
|             else { | ||||
|                 removed.push(curl) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!removed.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.save_cleaned(cleaned, removed) | ||||
|     } | ||||
|  | ||||
|     static remove_empty() { | ||||
|         let cleaned = [] | ||||
|         let removed = [] | ||||
|  | ||||
|         for (let curl of this.get_curls()) { | ||||
|             let item = Items.get(curl) | ||||
|  | ||||
|             if (!item) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             if (!item.status) { | ||||
|                 removed.push(curl) | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(curl) | ||||
|         } | ||||
|  | ||||
|         if (!removed.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.save_cleaned(cleaned, removed) | ||||
|     } | ||||
|  | ||||
|     static remove_old() { | ||||
|         let now = Utils.now() | ||||
|         let cleaned = [] | ||||
|         let removed = [] | ||||
|  | ||||
|         for (let curl of this.get_curls()) { | ||||
|             let item = Items.get(curl) | ||||
|  | ||||
|             if (!item) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             let date = item.updated | ||||
|  | ||||
|             if (date) { | ||||
|                 let datetime = new Date(date + `Z`).getTime() | ||||
|  | ||||
|                 if ((now - datetime) > (this.old_delay)) { | ||||
|                     removed.push(curl) | ||||
|                     continue | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             cleaned.push(curl) | ||||
|         } | ||||
|  | ||||
|         if (!removed.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.save_cleaned(cleaned, removed) | ||||
|     } | ||||
|  | ||||
|     static save_cleaned(cleaned, removed) { | ||||
|         let s = Utils.plural(removed.length, `Curl`, `Curls`) | ||||
|         let curls = removed.join(`, `) | ||||
|  | ||||
|         Windows.confirm({title: `Remove ${removed.length} ${s}`, ok: () => { | ||||
|             this.save_curls(cleaned) | ||||
|             Items.remove(removed) | ||||
|         }, message: curls}) | ||||
|     } | ||||
|  | ||||
|     static copy() { | ||||
|         let curls = this.get_curls() | ||||
|         let text = curls.join(` `) | ||||
|         Utils.copy_to_clipboard(text) | ||||
|     } | ||||
|  | ||||
|     static save_curls(curls, color = Colors.mode) { | ||||
|         let current = this.get(color) | ||||
|         let items = [] | ||||
|  | ||||
|         for (let curl of curls) { | ||||
|             let item = current.find(x => x.curl === curl) | ||||
|  | ||||
|             if (item) { | ||||
|                 items.push(item) | ||||
|             } | ||||
|             else { | ||||
|                 item = this.new_item(curl) | ||||
|                 items.push(item) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return this.save(items, color) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										80
									
								
								server/static/dashboard/js/main/dates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/static/dashboard/js/main/dates.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| /* | ||||
|  | ||||
| This manages the dates shown in the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Dates { | ||||
|     static ls_mode = `date_mode` | ||||
|     static ls_enabled = `date_enabled` | ||||
|     static default_mode = `12` | ||||
|  | ||||
|     static setup() { | ||||
|         this.mode = this.load_mode() | ||||
|         this.enabled = this.load_enabled() | ||||
|     } | ||||
|  | ||||
|     static change_mode() { | ||||
|         let selected = window.getSelection().toString() | ||||
|  | ||||
|         if (selected) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.mode = this.mode === `12` ? `24` : `12` | ||||
|         Utils.save(this.ls_mode, this.mode) | ||||
|         Items.fill() | ||||
|         Container.update() | ||||
|     } | ||||
|  | ||||
|     static load_mode() { | ||||
|         return Utils.load_string(this.ls_mode, this.default_mode) | ||||
|     } | ||||
|  | ||||
|     static save_enabled() { | ||||
|         Utils.save(this.ls_enabled, this.enabled) | ||||
|     } | ||||
|  | ||||
|     static load_enabled() { | ||||
|         return Utils.load_boolean(this.ls_enabled) | ||||
|     } | ||||
|  | ||||
|     static fill(item) { | ||||
|         // Updated | ||||
|         let date = new Date(item.updated + `Z`) | ||||
|         let s_date | ||||
|  | ||||
|         if (this.mode === `12`) { | ||||
|             s_date = dateFormat(date, `dd/mmm/yy - h:MM tt`) | ||||
|         } | ||||
|         else if (this.mode === `24`) { | ||||
|             s_date = dateFormat(date, `dd/mmm/yy - HH:MM`) | ||||
|         } | ||||
|  | ||||
|         item.updated_text = s_date | ||||
|  | ||||
|         // Created | ||||
|         date = new Date(item.created + `Z`) | ||||
|  | ||||
|         if (this.mode === `12`) { | ||||
|             s_date = dateFormat(date, `dd/mmm/yy - h:MM tt`) | ||||
|         } | ||||
|         else if (this.mode === `24`) { | ||||
|             s_date = dateFormat(date, `dd/mmm/yy - HH:MM`) | ||||
|         } | ||||
|  | ||||
|         item.created_text = s_date | ||||
|  | ||||
|         // Added | ||||
|         date = new Date(item.added + `Z`) | ||||
|  | ||||
|         if (this.mode === `12`) { | ||||
|             s_date = dateFormat(item.added, `dd/mmm/yy - h:MM tt`) | ||||
|         } | ||||
|         else if (this.mode === `24`) { | ||||
|             s_date = dateFormat(item.added, `dd/mmm/yy - HH:MM`) | ||||
|         } | ||||
|  | ||||
|         item.added_text = s_date | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								server/static/dashboard/js/main/drag.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/static/dashboard/js/main/drag.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| /* | ||||
|  | ||||
| Controls dragging of items in the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Drag { | ||||
|     static drag_items = [] | ||||
|     static drag_y = 0 | ||||
|  | ||||
|     static setup() { | ||||
|         let container = Container.get_container() | ||||
|  | ||||
|         DOM.ev(container, `dragstart`, (e) => { | ||||
|             this.drag_start(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(container, `dragenter`, (e) => { | ||||
|             this.drag_enter(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(container, `dragend`, (e) => { | ||||
|             this.drag_end(e) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static drag_start(e) { | ||||
|         let item = Container.extract_item(e) | ||||
|         let curl = Container.extract_curl(item) | ||||
|         this.drag_y = e.clientY | ||||
|  | ||||
|         e.dataTransfer.setData(`text`, curl) | ||||
|         e.dataTransfer.setDragImage(new Image(), 0, 0) | ||||
|  | ||||
|         let selected = Select.get() | ||||
|  | ||||
|         if (selected.length && selected.includes(item)) { | ||||
|             this.drag_items = selected | ||||
|         } | ||||
|         else { | ||||
|             if (!selected.includes(item)) { | ||||
|                 Select.single(item) | ||||
|             } | ||||
|  | ||||
|             this.drag_items = [item] | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static drag_enter(e) { | ||||
|         let items = Container.get_items() | ||||
|         let item = Container.extract_item(e) | ||||
|         let index = items.indexOf(item) | ||||
|  | ||||
|         if (index === -1) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let direction = (e.clientY > this.drag_y) ? `down` : `up` | ||||
|         this.drag_y = e.clientY | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             item.before(...this.drag_items) | ||||
|         } | ||||
|         else if (direction === `down`) { | ||||
|             item.after(...this.drag_items) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static drag_end(e) { | ||||
|         if (Container.save_curls()) { | ||||
|             Sort.set_value(`order`) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										278
									
								
								server/static/dashboard/js/main/filter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								server/static/dashboard/js/main/filter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | ||||
| /* | ||||
|  | ||||
| This is the filter for the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Filter { | ||||
|     static debouncer_delay = 250 | ||||
|     static default_mode = `all` | ||||
|     static timeout_delay = Utils.SECOND * 3 | ||||
|     static ls_items = `filter_items` | ||||
|     static max_items = 100 | ||||
|  | ||||
|     static modes = [ | ||||
|         { value: `all`, name: `All`, info: `Show all curls` }, | ||||
|         { value: Utils.separator }, | ||||
|         { value: `today`, name: `Today`, info: `Show the curls that changed today` }, | ||||
|         { value: `week`, name: `Week`, info: `Show the curls that changed this week` }, | ||||
|         { value: `month`, name: `Month`, info: `Show the curls that changed this month` }, | ||||
|         { value: Utils.separator }, | ||||
|         { value: `curl`, name: `Curl`, info: `Filter by curl` }, | ||||
|         { value: `status`, name: `Status`, info: `Filter by status` }, | ||||
|         { value: `date`, name: `Date`, info: `Filter by date` }, | ||||
|         { value: Utils.separator }, | ||||
|         { value: `owned`, name: `Owned`, info: `Show the curls that you control` }, | ||||
|     ] | ||||
|  | ||||
|     static setup() { | ||||
|         let filter = this.get_filter() | ||||
|  | ||||
|         DOM.ev(filter, `keydown`, (e) => { | ||||
|             this.filter() | ||||
|         }) | ||||
|  | ||||
|         this.debouncer = Utils.create_debouncer(() => { | ||||
|             this.do_filter() | ||||
|         }, this.debouncer_delay) | ||||
|  | ||||
|         filter.value = `` | ||||
|  | ||||
|         let lines = [ | ||||
|             `Filter the items`, | ||||
|             `Press Escape to clear`, | ||||
|         ] | ||||
|  | ||||
|         filter.title = lines.join(`\n`) | ||||
|         let modes_button = DOM.el(`#filter_modes`) | ||||
|         this.mode = this.default_mode | ||||
|  | ||||
|         this.combo = new Combo({ | ||||
|             title: `Filter Modes`, | ||||
|             items: this.modes, | ||||
|             value: this.filer_mode, | ||||
|             element: modes_button, | ||||
|             default: this.default_mode, | ||||
|             input: filter, | ||||
|             action: (value) => { | ||||
|                 this.change(value) | ||||
|             }, | ||||
|             get: () => { | ||||
|                 return this.mode | ||||
|             }, | ||||
|         }) | ||||
|  | ||||
|         let button = DOM.el(`#filter_button`) | ||||
|  | ||||
|         DOM.ev(filter, `wheel`, (e) => { | ||||
|             Utils.scroll_wheel(e) | ||||
|         }) | ||||
|  | ||||
|         this.list = new List( | ||||
|             button, | ||||
|             filter, | ||||
|             this.ls_items, | ||||
|             this.max_items, | ||||
|             (value) => { | ||||
|                 this.action(value) | ||||
|             }, | ||||
|             () => { | ||||
|                 this.clear() | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     static change(value) { | ||||
|         if (this.mode === value) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.mode = value | ||||
|         this.do_filter() | ||||
|     } | ||||
|  | ||||
|     static unfilter() { | ||||
|         let els = DOM.els(`#container .item`) | ||||
|  | ||||
|         if (!els.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         for (let el of els) { | ||||
|             DOM.show(el) | ||||
|         } | ||||
|  | ||||
|         this.after() | ||||
|     } | ||||
|  | ||||
|     static clear() { | ||||
|         this.get_filter().value = `` | ||||
|         this.unfilter() | ||||
|     } | ||||
|  | ||||
|     static filter() { | ||||
|         this.debouncer.call() | ||||
|     } | ||||
|  | ||||
|     static do_filter() { | ||||
|         this.debouncer.cancel() | ||||
|         let els = Container.get_items() | ||||
|  | ||||
|         if (!els.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let value = this.get_value() | ||||
|         let is_special = false | ||||
|         let special = [] | ||||
|         let scope = `all` | ||||
|  | ||||
|         if (this.mode === `owned`) { | ||||
|             special = Items.get_owned() | ||||
|             is_special = true | ||||
|         } | ||||
|         else if (this.mode === `today`) { | ||||
|             special = Items.get_today() | ||||
|             is_special = true | ||||
|         } | ||||
|         else if (this.mode === `week`) { | ||||
|             special = Items.get_week() | ||||
|             is_special = true | ||||
|         } | ||||
|         else if (this.mode === `month`) { | ||||
|             special = Items.get_month() | ||||
|             is_special = true | ||||
|         } | ||||
|         else if (this.mode === `curl`) { | ||||
|             scope = `curl` | ||||
|             is_special = true | ||||
|         } | ||||
|         else if (this.mode === `status`) { | ||||
|             scope = `status` | ||||
|             is_special = true | ||||
|         } | ||||
|         else if (this.mode === `date`) { | ||||
|             scope = `date` | ||||
|             is_special = true | ||||
|         } | ||||
|  | ||||
|         if (!value && !is_special) { | ||||
|             this.unfilter() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if ((scope !== `all`) && !value) { | ||||
|             this.unfilter() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let check = (curl, status, updated) => { | ||||
|             return curl.includes(value) || status.includes(value) || updated.includes(value) | ||||
|         } | ||||
|  | ||||
|         let hide = (el) => { | ||||
|             DOM.hide(el) | ||||
|         } | ||||
|  | ||||
|         let show = (el) => { | ||||
|             DOM.show(el) | ||||
|         } | ||||
|  | ||||
|         for (let el of els) { | ||||
|             let item = Items.get(el.dataset.curl) | ||||
|             let curl = item.curl.toLowerCase() | ||||
|             let status = item.status.toLowerCase() | ||||
|             let updated = item.updated_text.toLowerCase() | ||||
|  | ||||
|             if (scope === `curl`) { | ||||
|                 if (curl.includes(value)) { | ||||
|                     show(el) | ||||
|                 } | ||||
|                 else { | ||||
|                     hide(el) | ||||
|                 } | ||||
|             } | ||||
|             else if (scope === `status`) { | ||||
|                 if (status.includes(value)) { | ||||
|                     show(el) | ||||
|                 } | ||||
|                 else { | ||||
|                     hide(el) | ||||
|                 } | ||||
|             } | ||||
|             else if (scope === `date`) { | ||||
|                 if (updated.includes(value)) { | ||||
|                     show(el) | ||||
|                 } | ||||
|                 else { | ||||
|                     hide(el) | ||||
|                 } | ||||
|             } | ||||
|             else if (is_special) { | ||||
|                 if (special.find(s => s.curl === item.curl)) { | ||||
|                     if (check(curl, status, updated)) { | ||||
|                         show(el) | ||||
|                     } | ||||
|                     else { | ||||
|                         hide(el) | ||||
|                     } | ||||
|                 } | ||||
|                 else { | ||||
|                     hide(el) | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 if (check(curl, status, updated)) { | ||||
|                     show(el) | ||||
|                 } | ||||
|                 else { | ||||
|                     hide(el) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.after() | ||||
|     } | ||||
|  | ||||
|     static check() { | ||||
|         let filter = this.get_filter() | ||||
|  | ||||
|         if (filter.value || (this.mode !== this.default_mode)) { | ||||
|             this.do_filter() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static after() { | ||||
|         clearTimeout(this.timeout) | ||||
|  | ||||
|         this.timeout = setTimeout(() => { | ||||
|             this.save() | ||||
|         }, this.timeout_delay) | ||||
|  | ||||
|         Infobar.update_curls() | ||||
|     } | ||||
|  | ||||
|     static save() { | ||||
|         let value = this.get_value() | ||||
|         this.list.save(value) | ||||
|     } | ||||
|  | ||||
|     static get_items() { | ||||
|         return Utils.load_array(this.ls_items) | ||||
|     } | ||||
|  | ||||
|     static get_value() { | ||||
|         return this.get_filter().value.toLowerCase().trim() | ||||
|     } | ||||
|  | ||||
|     static get_filter() { | ||||
|         return DOM.el(`#filter`) | ||||
|     } | ||||
|  | ||||
|     static action(value) { | ||||
|         let filter = this.get_filter() | ||||
|         filter.value = value | ||||
|         filter.focus() | ||||
|         this.filter() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										52
									
								
								server/static/dashboard/js/main/font.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								server/static/dashboard/js/main/font.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| /* | ||||
|  | ||||
| The font of the interface | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Font { | ||||
|     static default_mode = `sans-serif` | ||||
|     static ls_name = `font` | ||||
|  | ||||
|     static modes = [ | ||||
|         {value: `sans-serif`, name: `Sans`, info: `Use Sans-Serif as the font`}, | ||||
|         {value: `serif`, name: `Serif`, info: `Use Serif as the font`}, | ||||
|         {value: `monospace`, name: `Mono`, info: `Use Monospace as the font`}, | ||||
|         {value: `cursive`, name: `Cursive`, info: `Use Cursive as the font`}, | ||||
|     ] | ||||
|  | ||||
|     static setup() { | ||||
|         let font = DOM.el(`#font`) | ||||
|         this.mode = this.load_font() | ||||
|  | ||||
|         this.combo = new Combo({ | ||||
|             title: `Font Modes`, | ||||
|             items: this.modes, | ||||
|             value: this.mode, | ||||
|             element: font, | ||||
|             default: this.default_mode, | ||||
|             action: (value) => { | ||||
|                 this.change(value) | ||||
|                 this.apply() | ||||
|             }, | ||||
|             get: () => { | ||||
|                 return this.mode | ||||
|             }, | ||||
|         }) | ||||
|  | ||||
|         this.apply() | ||||
|     } | ||||
|  | ||||
|     static change(value) { | ||||
|         this.mode = value | ||||
|         Utils.save(this.ls_name, value) | ||||
|     } | ||||
|  | ||||
|     static apply() { | ||||
|         document.documentElement.style.setProperty(`--font`, this.mode) | ||||
|     } | ||||
|  | ||||
|     static load_font() { | ||||
|         return Utils.load_modes(this.ls_name, this.modes, this.default_mode) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								server/static/dashboard/js/main/footer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								server/static/dashboard/js/main/footer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| class Footer { | ||||
|     static setup() { | ||||
|         let footer = DOM.el(`#footer`) | ||||
|  | ||||
|         DOM.ev(footer, `contextmenu`, (e) => { | ||||
|             e.preventDefault() | ||||
|             Menu.show(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(footer, `dblclick`, (e) => { | ||||
|             if (e.target !== footer) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             Curls.add() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(footer, `wheel`, (e) => { | ||||
|             if (e.target !== footer) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             Container.scroll(e) | ||||
|         }) | ||||
|  | ||||
|         let lines = [ | ||||
|             `Right Click to show the main menu`, | ||||
|             `Double Click to add curls`, | ||||
|             `Wheel to scroll the container`, | ||||
|         ] | ||||
|  | ||||
|         footer.title = lines.join(`\n`) | ||||
|         let scroller = DOM.el(`#scroller`) | ||||
|  | ||||
|         DOM.ev(scroller, `click`, () => { | ||||
|             Container.scroller() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(scroller, `wheel`, (e) => { | ||||
|             Container.scroll(e) | ||||
|         }) | ||||
|  | ||||
|         let version = DOM.el(`#version`) | ||||
|  | ||||
|         DOM.ev(version, `click`, () => { | ||||
|             Intro.show() | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										131
									
								
								server/static/dashboard/js/main/infobar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								server/static/dashboard/js/main/infobar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| class Infobar { | ||||
|     static interval_delay = Utils.SECOND * 30 | ||||
|     static curls_debouncer_delay = 100 | ||||
|     static date_debouncer_delay = 100 | ||||
|  | ||||
|     static setup() { | ||||
|         let infobar = DOM.el(`#infobar`) | ||||
|         this.hide() | ||||
|  | ||||
|         DOM.ev(infobar, `click`, () => { | ||||
|             Container.scroll_top() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(infobar, `contextmenu`, (e) => { | ||||
|             e.preventDefault() | ||||
|             Menu.show(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(infobar, `auxclick`, (e) => { | ||||
|             if (e.button === 1) { | ||||
|                 Container.scroll_bottom() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(infobar, `wheel`, (e) => { | ||||
|             Container.scroll(e) | ||||
|         }) | ||||
|  | ||||
|         this.start_interval() | ||||
|  | ||||
|         this.curls_debouncer = Utils.create_debouncer(() => { | ||||
|             this.do_update_curls() | ||||
|         }, this.curls_debouncer_delay) | ||||
|  | ||||
|         this.date_debouncer = Utils.create_debouncer(() => { | ||||
|             this.do_update_date() | ||||
|         }, this.date_debouncer_delay) | ||||
|  | ||||
|         let curls = DOM.el(`#infobar_curls`) | ||||
|         curls.title = `Number of curls being monitored\nClick to select all` | ||||
|  | ||||
|         DOM.ev(curls, `click`, () => { | ||||
|             this.curls_action() | ||||
|         }) | ||||
|  | ||||
|         let date = DOM.el(`#infobar_date`) | ||||
|         date.title = `How long ago items were updated\nClick to update now` | ||||
|  | ||||
|         DOM.ev(date, `click`, () => { | ||||
|             this.date_action() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static start_interval() { | ||||
|         clearInterval(this.interval) | ||||
|  | ||||
|         this.interval = setInterval(() => { | ||||
|             this.update_date() | ||||
|         }, this.interval_delay) | ||||
|     } | ||||
|  | ||||
|     static update() { | ||||
|         if (!Items.list.length) { | ||||
|             this.hide() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.show() | ||||
|         this.do_update_curls() | ||||
|         this.do_update_date() | ||||
|         this.start_interval() | ||||
|     } | ||||
|  | ||||
|     static update_curls() { | ||||
|         this.curls_debouncer.call() | ||||
|     } | ||||
|  | ||||
|     static do_update_curls() { | ||||
|         this.curls_debouncer.cancel() | ||||
|         let el = DOM.el(`#infobar_curls`) | ||||
|         let visible = Container.get_visible() | ||||
|         let selected = Select.get() | ||||
|         let text | ||||
|  | ||||
|         if (visible.length === Items.list.length) { | ||||
|             text = `${Items.list.length} Curls` | ||||
|         } | ||||
|         else { | ||||
|             text = `${visible.length} / ${Items.list.length} Curls` | ||||
|         } | ||||
|  | ||||
|         if (selected.length) { | ||||
|             if (selected.length === visible.length) { | ||||
|                 text += ` (All)` | ||||
|             } | ||||
|             else { | ||||
|                 text += ` (${selected.length})` | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         el.textContent = text | ||||
|     } | ||||
|  | ||||
|     static update_date() { | ||||
|         this.date_debouncer.call() | ||||
|     } | ||||
|  | ||||
|     static do_update_date() { | ||||
|         this.date_debouncer.cancel() | ||||
|         let el = DOM.el(`#infobar_date`) | ||||
|         let ago = Utils.timeago(Update.last_update) | ||||
|         el.textContent = ago | ||||
|     } | ||||
|  | ||||
|     static hide() { | ||||
|         DOM.hide(`#infobar`) | ||||
|     } | ||||
|  | ||||
|     static show() { | ||||
|         DOM.show(`#infobar`) | ||||
|     } | ||||
|  | ||||
|     static curls_action() { | ||||
|         Select.toggle_all() | ||||
|     } | ||||
|  | ||||
|     static date_action() { | ||||
|         DOM.el(`#infobar_date`).textContent = `Updating` | ||||
|         Update.update() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								server/static/dashboard/js/main/intro.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								server/static/dashboard/js/main/intro.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /* | ||||
|  | ||||
| This shows an intro on the first visit | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Intro { | ||||
|     static ls_name = `intro_shown` | ||||
|  | ||||
|     static setup() { | ||||
|         this.intro = [ | ||||
|             `Curls are pointers to text that you control.`, | ||||
|             `You can claim your own curl and receive a key.`, | ||||
|             `With this key you can change the status of the curl.`, | ||||
|             `The key can't be recovered and should be saved securely.`, | ||||
|             `In this Dashboard you can monitor the curls you want.`, | ||||
|             `Each color has its own set of curls.`, | ||||
|             `You are limited to 100 curls per color.`, | ||||
|         ].join(`\n`) | ||||
|  | ||||
|         let shown = this.load_intro() | ||||
|  | ||||
|         if (!shown) { | ||||
|             this.show() | ||||
|             this.save() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static save() { | ||||
|         Utils.save(this.ls_name, true) | ||||
|     } | ||||
|  | ||||
|     static load_intro() { | ||||
|         return Utils.load_boolean(this.ls_name, false) | ||||
|     } | ||||
|  | ||||
|     static show() { | ||||
|         Windows.alert({title: `Curls ${App.version}`, message: this.intro}) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										240
									
								
								server/static/dashboard/js/main/items.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								server/static/dashboard/js/main/items.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | ||||
| /* | ||||
|  | ||||
| This manages the item list | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Items { | ||||
|     static list = [] | ||||
|  | ||||
|     static get(curl) { | ||||
|         return this.list.find(item => item.curl === curl) | ||||
|     } | ||||
|  | ||||
|     static find_missing() { | ||||
|         let used = Curls.get_curls() | ||||
|         let curls = used.filter(curl => !this.list.find(item => item.curl === curl)) | ||||
|         let missing = [] | ||||
|  | ||||
|         for (let curl of curls) { | ||||
|             missing.push({ | ||||
|                 curl: curl, | ||||
|                 status: `Not found`, | ||||
|                 created: `0`, | ||||
|                 updated: `0`, | ||||
|                 changes: 0, | ||||
|                 missing: true, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         return missing | ||||
|     } | ||||
|  | ||||
|     static get_missing() { | ||||
|         return this.list.filter(item => item.missing) | ||||
|     } | ||||
|  | ||||
|     static get_owned() { | ||||
|         let picker_items = Picker.get_items() | ||||
|  | ||||
|         return this.list.filter(item => picker_items.find( | ||||
|             picker => picker.curl === item.curl)) | ||||
|     } | ||||
|  | ||||
|     static get_by_date(what) { | ||||
|         let cleaned = [] | ||||
|         let now = Utils.now() | ||||
|  | ||||
|         for (let item of this.list) { | ||||
|             let date = new Date(item.updated + `Z`) | ||||
|             let diff = now - date | ||||
|  | ||||
|             if (diff < what) { | ||||
|                 cleaned.push(item) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return cleaned | ||||
|     } | ||||
|  | ||||
|     static get_today() { | ||||
|         return this.get_by_date(Utils.DAY) | ||||
|     } | ||||
|  | ||||
|     static get_week() { | ||||
|         return this.get_by_date(Utils.WEEK) | ||||
|     } | ||||
|  | ||||
|     static get_month() { | ||||
|         return this.get_by_date(Utils.MONTH) | ||||
|     } | ||||
|  | ||||
|     static reset() { | ||||
|         this.list = [] | ||||
|     } | ||||
|  | ||||
|     static copy(curl) { | ||||
|         let blink = (icon) => { | ||||
|             if (!icon) { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             icon.classList.add(`blink`) | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 icon.classList.remove(`blink`) | ||||
|             }, 1000) | ||||
|         } | ||||
|  | ||||
|         let curls = Select.get_curls() | ||||
|  | ||||
|         if (!curls.includes(curl)) { | ||||
|             curls = [curl] | ||||
|         } | ||||
|  | ||||
|         let msgs = [] | ||||
|  | ||||
|         for (let curl of curls) { | ||||
|             let item = this.get(curl) | ||||
|             msgs.push(`${item.curl}\n${item.status}\n${item.updated_text}`) | ||||
|             blink(DOM.el(`.item_icon`, item.element)) | ||||
|         } | ||||
|  | ||||
|         let msg = msgs.join(`\n\n`) | ||||
|         Utils.copy_to_clipboard(msg) | ||||
|     } | ||||
|  | ||||
|     static show_menu(args = {}) { | ||||
|         let items = [] | ||||
|         let curls = Select.get_curls() | ||||
|  | ||||
|         if (curls.length > 1) { | ||||
|             items = [ | ||||
|                 { | ||||
|                     text: `Copy`, | ||||
|                     action: () => { | ||||
|                         this.copy(args.curl) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Move`, | ||||
|                     action: () => { | ||||
|                         Colors.move(curls, args.e) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Remove`, | ||||
|                     action: () => { | ||||
|                         Curls.remove_selected() | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     separator: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `To Top`, | ||||
|                     action: () => { | ||||
|                         Curls.to_top(curls) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `To Bottom`, | ||||
|                     action: () => { | ||||
|                         Curls.to_bottom(curls) | ||||
|                     } | ||||
|                 }, | ||||
|             ] | ||||
|         } | ||||
|         else { | ||||
|             items = [ | ||||
|                 { | ||||
|                     text: `Copy`, | ||||
|                     action: () => { | ||||
|                         this.copy(args.curl) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Edit`, | ||||
|                     action: () => { | ||||
|                         Curls.edit(args.curl) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Move`, | ||||
|                     action: () => { | ||||
|                         Colors.move([args.curl], args.e) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Remove`, | ||||
|                     action: () => { | ||||
|                         Curls.remove([args.curl]) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     separator: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `To Top`, | ||||
|                     action: () => { | ||||
|                         Curls.to_top([args.curl]) | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `To Bottom`, | ||||
|                     action: () => { | ||||
|                         Curls.to_bottom([args.curl]) | ||||
|                     } | ||||
|                 }, | ||||
|             ] | ||||
|         } | ||||
|  | ||||
|         Utils.context({items: items, e: args.e}) | ||||
|     } | ||||
|  | ||||
|     static remove_curl(curl) { | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let item of this.list) { | ||||
|             if (item.curl !== curl) { | ||||
|                 cleaned.push(item) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.list = cleaned | ||||
|     } | ||||
|  | ||||
|     static remove(removed) { | ||||
|         for (let curl of removed) { | ||||
|             let item = this.get(curl) | ||||
|             let el = item.element | ||||
|  | ||||
|             if (el) { | ||||
|                 el.remove() | ||||
|             } | ||||
|  | ||||
|             let index = this.list.indexOf(item) | ||||
|             this.list.splice(index, 1) | ||||
|         } | ||||
|  | ||||
|         Container.check_empty() | ||||
|         Infobar.update_curls() | ||||
|     } | ||||
|  | ||||
|     static fill() { | ||||
|         let items = Curls.get() | ||||
|  | ||||
|         for (let item of Items.list) { | ||||
|             let item_ = items.find(x => x.curl === item.curl) | ||||
|  | ||||
|             if (item_ && item_.added) { | ||||
|                 item.added = item_.added | ||||
|             } | ||||
|             else { | ||||
|                 item.added = `0` | ||||
|             } | ||||
|  | ||||
|             Dates.fill(item) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										190
									
								
								server/static/dashboard/js/main/list.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								server/static/dashboard/js/main/list.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| class List { | ||||
|     constructor(button, input, ls_items, max_items, action, clear_action) { | ||||
|         this.button = button | ||||
|         this.input = input | ||||
|         this.ls_items = ls_items | ||||
|         this.max_items = max_items | ||||
|         this.action = action | ||||
|         this.clear_action = clear_action | ||||
|         this.menu_max_length = 110 | ||||
|         this.prepare() | ||||
|     } | ||||
|  | ||||
|     prepare() { | ||||
|         DOM.ev(this.button, `click`, (e) => { | ||||
|             this.show_menu(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(this.button, `auxclick`, (e) => { | ||||
|             if (e.button === 1) { | ||||
|                 this.clear_action() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(this.button, `wheel`, (e) => { | ||||
|             this.cycle(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(this.input, `keydown`, (e) => { | ||||
|             if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) { | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|             else if (e.key === `Escape`) { | ||||
|                 this.input.value = `` | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(this.input, `keyup`, (e) => { | ||||
|             if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) { | ||||
|                 this.show_menu() | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         let lines = [ | ||||
|             `Use previous items`, | ||||
|             `Middle Click to clear input`, | ||||
|             `Middle Click items to remove`, | ||||
|             `Wheel to cycle`, | ||||
|         ] | ||||
|  | ||||
|         this.button.title = lines.join(`\n`) | ||||
|     } | ||||
|  | ||||
|     get_items() { | ||||
|         return Utils.load_array(this.ls_items) | ||||
|     } | ||||
|  | ||||
|     show_menu(e, show_empty = true) { | ||||
|         let list = this.get_items() | ||||
|  | ||||
|         if (!list.length) { | ||||
|             if (show_empty) { | ||||
|                 Windows.alert({ | ||||
|                     title: `Empty List`, | ||||
|                     message: `Items appear here after you use them`, | ||||
|                 }) | ||||
|             } | ||||
|  | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let items = [] | ||||
|  | ||||
|         for (let item of list) { | ||||
|             items.push({ | ||||
|                 text: item.substring(0, this.menu_max_length), | ||||
|                 action: () => { | ||||
|                     this.action(item) | ||||
|                 }, | ||||
|                 alt_action: () => { | ||||
|                     this.remove(item) | ||||
|                 }, | ||||
|  | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         items.push({ | ||||
|             separator: true, | ||||
|         }) | ||||
|  | ||||
|         items.push({ | ||||
|             text: `Clear`, | ||||
|             action: () => { | ||||
|                 this.clear() | ||||
|             }, | ||||
|         }) | ||||
|  | ||||
|         this.last_e = e | ||||
|  | ||||
|         Utils.context({ | ||||
|             e: e, | ||||
|             items: items, | ||||
|             element: this.button, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     save(value) { | ||||
|         value = value.trim() | ||||
|  | ||||
|         if (!value) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let item of this.get_items()) { | ||||
|             if (item !== value) { | ||||
|                 cleaned.push(item) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let list = [value, ...cleaned].slice(0, this.max_items) | ||||
|         Utils.save(this.ls_items, JSON.stringify(list)) | ||||
|     } | ||||
|  | ||||
|     remove(status) { | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let status_ of this.get_items()) { | ||||
|             if (status_ === status) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(status_) | ||||
|         } | ||||
|  | ||||
|         Utils.save(this.ls_items, JSON.stringify(cleaned)) | ||||
|         this.show_menu(this.last_e, false) | ||||
|     } | ||||
|  | ||||
|     clear() { | ||||
|         Windows.confirm({title: `Clear List`, ok: () => { | ||||
|             Utils.save(this.ls_items, `[]`) | ||||
|         }, message: `Remove all items from the list`}) | ||||
|     } | ||||
|  | ||||
|     cycle(e) { | ||||
|         let direction = Utils.wheel_direction(e) | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             this.action(this.get_prev()) | ||||
|         } | ||||
|         else { | ||||
|             this.action(this.get_next()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     get_next() { | ||||
|         let list = this.get_items() | ||||
|         let current = this.input.value.trim() | ||||
|         let index = list.indexOf(current) | ||||
|  | ||||
|         if (index === -1) { | ||||
|             return list[0] | ||||
|         } | ||||
|  | ||||
|         if (index === list.length - 1) { | ||||
|             return list[0] | ||||
|         } | ||||
|  | ||||
|         return list[index + 1] | ||||
|     } | ||||
|  | ||||
|     get_prev() { | ||||
|         let list = this.get_items() | ||||
|         let current = this.input.value.trim() | ||||
|         let index = list.indexOf(current) | ||||
|  | ||||
|         if (index === -1) { | ||||
|             return Utils.last(list) | ||||
|         } | ||||
|  | ||||
|         if (index === 0) { | ||||
|             return list[list.length - 1] | ||||
|         } | ||||
|  | ||||
|         return list[index - 1] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								server/static/dashboard/js/main/load.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								server/static/dashboard/js/main/load.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| window.onload = () => { | ||||
|     App.setup() | ||||
| } | ||||
							
								
								
									
										154
									
								
								server/static/dashboard/js/main/menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								server/static/dashboard/js/main/menu.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| class Menu { | ||||
|     static setup() { | ||||
|         let menu = DOM.el(`#menu`) | ||||
|  | ||||
|         DOM.ev(menu, `click`, (e) => { | ||||
|             this.show(e) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static show(e) { | ||||
|         let curls = Curls.get_curls() | ||||
|         let items | ||||
|  | ||||
|         let data = [ | ||||
|             { | ||||
|                 separator: true, | ||||
|             }, | ||||
|             { | ||||
|                 text: `Export`, | ||||
|                 action: (e) => { | ||||
|                     this.export(e) | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 text: `Import`, | ||||
|                 action: () => { | ||||
|                     this.import() | ||||
|                 } | ||||
|             }, | ||||
|         ] | ||||
|  | ||||
|         if (curls.length) { | ||||
|             items = [ | ||||
|                 { | ||||
|                     text: `Add`, | ||||
|                     action: () => { | ||||
|                         Curls.add() | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     separator: true, | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Copy`, | ||||
|                     action: () => { | ||||
|                         Curls.copy() | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Replace`, | ||||
|                     action: () => { | ||||
|                         Curls.replace() | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     text: `Remove`, | ||||
|                     action: (e) => { | ||||
|                         Curls.show_remove_menu(e) | ||||
|                     } | ||||
|                 }, | ||||
|                 ...data, | ||||
|             ] | ||||
|         } | ||||
|         else { | ||||
|             items = [ | ||||
|                 { | ||||
|                     text: `Add`, | ||||
|                     action: () => { | ||||
|                         Curls.add() | ||||
|                     } | ||||
|                 }, | ||||
|                 ...data, | ||||
|             ] | ||||
|         } | ||||
|  | ||||
|         items.push({ | ||||
|             separator: true, | ||||
|         }) | ||||
|  | ||||
|         items.push({ | ||||
|             text: `Claim`, | ||||
|             action: () => { | ||||
|                 this.claim() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         Utils.context({items: items, e: e}) | ||||
|     } | ||||
|  | ||||
|     static export() { | ||||
|         let colors = {} | ||||
|  | ||||
|         for (let color in Colors.colors) { | ||||
|             let curls = Curls.get_curls(color) | ||||
|  | ||||
|             if (!curls.length) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             colors[color] = curls | ||||
|         } | ||||
|  | ||||
|         if (!Object.keys(colors).length) { | ||||
|             Windows.alert({message: `No curls to export`}) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         Windows.alert_export(colors) | ||||
|     } | ||||
|  | ||||
|     static import() { | ||||
|         Windows.prompt({title: `Paste Data`, callback: (value) => { | ||||
|             this.import_submit(value) | ||||
|         }, message: `You get this data in Export`}) | ||||
|     } | ||||
|  | ||||
|     static import_submit(data) { | ||||
|         if (!data) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             let colors = JSON.parse(data) | ||||
|             let modified = false | ||||
|  | ||||
|             for (let color in colors) { | ||||
|                 let curls = colors[color] | ||||
|  | ||||
|                 if (!curls.length) { | ||||
|                     continue | ||||
|                 } | ||||
|  | ||||
|                 Curls.clear(color) | ||||
|                 Curls.save_curls(curls, color) | ||||
|                 modified = true | ||||
|             } | ||||
|  | ||||
|             if (!modified) { | ||||
|                 Windows.alert({message: `No curls to import`}) | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             Update.update() | ||||
|         } | ||||
|         catch (err) { | ||||
|             Utils.error(err) | ||||
|             Windows.alert({title: `Error`, message: err}) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static claim() { | ||||
|         window.open(`/claim`, `_blank`) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										166
									
								
								server/static/dashboard/js/main/more.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								server/static/dashboard/js/main/more.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| /* | ||||
|  | ||||
| This is a button that sits on the footer | ||||
| It is used to toggle some options | ||||
|  | ||||
| */ | ||||
|  | ||||
| class More { | ||||
|     static setup() { | ||||
|         let button = DOM.el(`#footer_more`) | ||||
|  | ||||
|         DOM.ev(button, `click`, (e) => { | ||||
|             this.show_menu(e) | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(button, `auxclick`, (e) => { | ||||
|             if (e.button == 1) { | ||||
|                 this.reset(e) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         let lines = [ | ||||
|             `More options`, | ||||
|             `Middle Click to reset`, | ||||
|         ] | ||||
|  | ||||
|         button.title = lines.join(`\n`) | ||||
|     } | ||||
|  | ||||
|     static change_wrap(what, actions = true) { | ||||
|         if (Container.wrap_enabled == what) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         Container.wrap_enabled = what | ||||
|         Container.save_wrap_enabled() | ||||
|  | ||||
|         if (actions) { | ||||
|             Container.update() | ||||
|         } | ||||
|  | ||||
|         this.popup(`Wrap`, what) | ||||
|     } | ||||
|  | ||||
|     static change_controls(what, actions = true) { | ||||
|         if (Controls.enabled == what) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         Controls.enabled = what | ||||
|         Controls.save_enabled() | ||||
|  | ||||
|         if (actions) { | ||||
|             Controls.check_enabled() | ||||
|         } | ||||
|  | ||||
|         this.popup(`Controls`, what) | ||||
|     } | ||||
|  | ||||
|     static change_dates(what, actions = true) { | ||||
|         if (Dates.enabled == what) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         Dates.enabled = what | ||||
|         Dates.save_enabled() | ||||
|  | ||||
|         if (actions) { | ||||
|             Container.update() | ||||
|         } | ||||
|  | ||||
|         this.popup(`Dates`, what) | ||||
|     } | ||||
|  | ||||
|     static show_menu(e) { | ||||
|         let items = [] | ||||
|  | ||||
|         if (Container.wrap_enabled) { | ||||
|             items.push({ | ||||
|                 text: `Disable Wrap`, | ||||
|                 action: () => { | ||||
|                     this.change_wrap(false) | ||||
|                 }, | ||||
|                 info: `Disable text wrapping in the container`, | ||||
|             }) | ||||
|         } | ||||
|         else { | ||||
|             items.push({ | ||||
|                 text: `Enable Wrap`, | ||||
|                 action: () => { | ||||
|                     this.change_wrap(true) | ||||
|                 }, | ||||
|                 info: `Enable text wrapping in the container`, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         if (Dates.enabled) { | ||||
|             items.push({ | ||||
|                 text: `Disable Dates`, | ||||
|                 action: () => { | ||||
|                     this.change_dates(false) | ||||
|                 }, | ||||
|                 info: `Disable dates in the container`, | ||||
|             }) | ||||
|         } | ||||
|         else { | ||||
|             items.push({ | ||||
|                 text: `Enable Dates`, | ||||
|                 action: () => { | ||||
|                     this.change_dates(true) | ||||
|                 }, | ||||
|                 info: `Enable dates in the container`, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         if (Controls.enabled) { | ||||
|             items.push({ | ||||
|                 text: `Disable Controls`, | ||||
|                 action: () => { | ||||
|                     this.change_controls(false) | ||||
|                 }, | ||||
|                 info: `Disable the controls`, | ||||
|             }) | ||||
|         } | ||||
|         else { | ||||
|             items.push({ | ||||
|                 text: `Enable Controls`, | ||||
|                 action: () => { | ||||
|                     this.change_controls(true) | ||||
|                 }, | ||||
|                 info: `Enable the controls`, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         Utils.context({items: items, e: e}) | ||||
|     } | ||||
|  | ||||
|     static reset() { | ||||
|         let vars = [ | ||||
|             Container.wrap_enabled, | ||||
|             Dates.enabled, | ||||
|             Controls.enabled, | ||||
|         ] | ||||
|  | ||||
|         if (vars.every((x) => x)) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         Windows.confirm({title: `Reset Options`, ok: () => { | ||||
|             this.do_reset() | ||||
|         }, message: `Reset all options to default`}) | ||||
|     } | ||||
|  | ||||
|     static do_reset() { | ||||
|         this.change_wrap(true, false) | ||||
|         this.change_controls(true, false) | ||||
|         this.change_dates(true, false) | ||||
|         Controls.check_enabled() | ||||
|         Container.update() | ||||
|     } | ||||
|  | ||||
|     static popup(what, value) { | ||||
|         let text = `${what} ${value ? `Enabled` : `Disabled`}` | ||||
|         Windows.popup(text) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										54
									
								
								server/static/dashboard/js/main/move.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								server/static/dashboard/js/main/move.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| /* | ||||
|  | ||||
| Moves items up and down in the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Move { | ||||
|     static setup() { | ||||
|         this.block = new Block() | ||||
|     } | ||||
|  | ||||
|     static up() { | ||||
|         if (this.block.add_charge()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let items = Container.get_visible() | ||||
|         let selected = Select.get() | ||||
|         let first_index = items.indexOf(selected[0]) | ||||
|  | ||||
|         if (first_index > 0) { | ||||
|             first_index -= 1 | ||||
|         } | ||||
|  | ||||
|         let prev = items[first_index] | ||||
|         prev.before(...selected) | ||||
|         Utils.scroll_element({item: selected[0]}) | ||||
|         this.save() | ||||
|     } | ||||
|  | ||||
|     static down() { | ||||
|         if (this.block.add_charge()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let items = Container.get_visible() | ||||
|         let selected = Select.get() | ||||
|         let last_index = items.indexOf(Utils.last(selected)) | ||||
|  | ||||
|         if (last_index < (items.length - 1)) { | ||||
|             last_index += 1 | ||||
|         } | ||||
|  | ||||
|         let next = items[last_index] | ||||
|         next.after(...selected) | ||||
|         Utils.scroll_element({item: Utils.last(selected)}) | ||||
|         this.save() | ||||
|     } | ||||
|  | ||||
|     static save() { | ||||
|         Container.save_curls() | ||||
|         Sort.set_value(`order`) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										165
									
								
								server/static/dashboard/js/main/picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								server/static/dashboard/js/main/picker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| /* | ||||
|  | ||||
| The picker stores owned curls | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Picker { | ||||
|     static max_items = 1000 | ||||
|     static ls_name = `picker` | ||||
|  | ||||
|     static setup() { | ||||
|         let picker = DOM.el(`#picker`) | ||||
|  | ||||
|         DOM.ev(picker, `click`, (e) => { | ||||
|             this.show(e) | ||||
|         }) | ||||
|  | ||||
|         let items = this.get_items() | ||||
|  | ||||
|         if (items.length) { | ||||
|             let first = items[0] | ||||
|             DOM.el(`#change_curl`).value = first.curl | ||||
|             DOM.el(`#change_key`).value = first.key | ||||
|         } | ||||
|  | ||||
|         let curl = DOM.el(`#change_curl`) | ||||
|  | ||||
|         DOM.ev(curl, `keydown`, (e) => { | ||||
|             if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) { | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(curl, `keyup`, (e) => { | ||||
|             if ((e.key === `ArrowUp`) || (e.key === `ArrowDown`)) { | ||||
|                 this.show(e) | ||||
|                 e.preventDefault() | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static get_items() { | ||||
|         return Utils.load_array(this.ls_name) | ||||
|     } | ||||
|  | ||||
|     static add() { | ||||
|         let curl = DOM.el(`#change_curl`).value.toLowerCase() | ||||
|         let key = DOM.el(`#change_key`).value | ||||
|         let cleaned = [{curl, key}] | ||||
|  | ||||
|         for (let item of this.get_items()) { | ||||
|             if (item.curl === curl) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(item) | ||||
|  | ||||
|             if (cleaned.length >= this.max_items) { | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Utils.save(this.ls_name, JSON.stringify(cleaned)) | ||||
|     } | ||||
|  | ||||
|     static show(e) { | ||||
|         let items = [] | ||||
|         let picker_items = this.get_items() | ||||
|  | ||||
|         if (!picker_items.length) { | ||||
|             items.push({ | ||||
|                 text: `Import`, | ||||
|                 action: () => { | ||||
|                     this.import() | ||||
|                 }, | ||||
|             }) | ||||
|         } | ||||
|         else { | ||||
|             for (let item of picker_items) { | ||||
|                 items.push({ | ||||
|                     text: item.curl, | ||||
|                     action: () => { | ||||
|                         let curl = DOM.el(`#change_curl`) | ||||
|                         let key = DOM.el(`#change_key`) | ||||
|                         curl.value = item.curl | ||||
|                         key.value = item.key | ||||
|                         curl.focus() | ||||
|                         this.add() | ||||
|                     }, | ||||
|                     alt_action: () => { | ||||
|                         this.remove_item(item.curl) | ||||
|                     }, | ||||
|                 }) | ||||
|             } | ||||
|  | ||||
|             if (items.length) { | ||||
|                 items.push({ | ||||
|                     separator: true, | ||||
|                 }) | ||||
|             } | ||||
|  | ||||
|             items.push({ | ||||
|                 text: `Export`, | ||||
|                 action: () => { | ||||
|                     this.export() | ||||
|                 }, | ||||
|             }) | ||||
|  | ||||
|             items.push({ | ||||
|                 text: `Import`, | ||||
|                 action: () => { | ||||
|                     this.import() | ||||
|                 }, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         let el = DOM.el(`#picker`) | ||||
|         Utils.context({items: items, element: el, e: e}) | ||||
|     } | ||||
|  | ||||
|     static export() { | ||||
|         Windows.alert_export(this.get_items()) | ||||
|     } | ||||
|  | ||||
|     static import() { | ||||
|         Windows.prompt({title: `Paste Data`, callback: (value) => { | ||||
|             this.import_submit(value) | ||||
|         }, message: `You get this data in Export`}) | ||||
|     } | ||||
|  | ||||
|     static import_submit(data) { | ||||
|         if (!data) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             let items = JSON.parse(data) | ||||
|             Utils.save(this.ls_name, JSON.stringify(items)) | ||||
|         } | ||||
|         catch (err) { | ||||
|             Utils.error(err) | ||||
|             Windows.alert({title: `Error`, message: err}) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static remove_item(curl) { | ||||
|         Windows.confirm({title: `Remove Picker Item`, ok: () => { | ||||
|             this.do_remove_item(curl) | ||||
|         }, message: curl}) | ||||
|     } | ||||
|  | ||||
|     static do_remove_item(curl) { | ||||
|         let cleaned = [] | ||||
|  | ||||
|         for (let item of this.get_items()) { | ||||
|             if (item.curl === curl) { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             cleaned.push(item) | ||||
|         } | ||||
|  | ||||
|         Utils.save(this.ls_name, JSON.stringify(cleaned)) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										379
									
								
								server/static/dashboard/js/main/select.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								server/static/dashboard/js/main/select.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,379 @@ | ||||
| /* | ||||
|  | ||||
| Used for selecting items in the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Select { | ||||
|     static selected_class = `selected` | ||||
|     static selected_id = 0 | ||||
|     static mouse_down = false | ||||
|     static mouse_selected = false | ||||
|  | ||||
|     static setup() { | ||||
|         this.block = new Block() | ||||
|     } | ||||
|  | ||||
|     static curl(curl) { | ||||
|         let item = Container.get_item(curl) | ||||
|  | ||||
|         if (item) { | ||||
|             this.select(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static curls(curls) { | ||||
|         for (let curl of curls) { | ||||
|             this.curl(curl) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static check(item) { | ||||
|         let selected = this.get() | ||||
|  | ||||
|         if (!selected.includes(item)) { | ||||
|             this.single(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static range(item) { | ||||
|         let selected = this.get() | ||||
|  | ||||
|         if (!selected.length) { | ||||
|             this.single(item) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let items = Container.get_visible() | ||||
|  | ||||
|         if (items.length <= 1) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let history = this.history() | ||||
|         let prev = history[0] | ||||
|         let prev_prev = history[1] || prev | ||||
|  | ||||
|         if (item === prev) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let index = items.indexOf(item) | ||||
|         let prev_index = items.indexOf(prev) | ||||
|         let prev_prev_index = items.indexOf(prev_prev) | ||||
|         let direction | ||||
|  | ||||
|         if (prev_index === prev_prev_index) { | ||||
|             if (index < prev_index) { | ||||
|                 direction = `up` | ||||
|             } | ||||
|             else { | ||||
|                 direction = `down` | ||||
|             } | ||||
|         } | ||||
|         else if (prev_index < prev_prev_index) { | ||||
|             direction = `up` | ||||
|         } | ||||
|         else { | ||||
|             direction = `down` | ||||
|         } | ||||
|  | ||||
|         let action = (a, b) => { | ||||
|             this.do_range(a, b, direction) | ||||
|         } | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             if (index < prev_index) { | ||||
|                 action(index, prev_index) | ||||
|             } | ||||
|             else { | ||||
|                 action(index, prev_prev_index) | ||||
|             } | ||||
|         } | ||||
|         else if (direction === `down`) { | ||||
|             if (index > prev_index) { | ||||
|                 action(prev_index, index) | ||||
|             } | ||||
|             else { | ||||
|                 action(prev_prev_index, index) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.id_item(item) | ||||
|     } | ||||
|  | ||||
|     static do_range(start, end, direction) { | ||||
|         let items = Container.get_visible() | ||||
|         let select = [] | ||||
|  | ||||
|         for (let i = 0; i < items.length; i++) { | ||||
|             if (i < start) { | ||||
|                 if (direction === `up`) { | ||||
|                     this.deselect(items[i]) | ||||
|                 } | ||||
|  | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             if (i > end) { | ||||
|                 if (direction === `down`) { | ||||
|                     this.deselect(items[i]) | ||||
|                 } | ||||
|  | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             select.push(items[i]) | ||||
|         } | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             select.reverse() | ||||
|         } | ||||
|  | ||||
|         for (let item of select) { | ||||
|             this.select(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static vertical(direction, shift) { | ||||
|         if (this.block.add_charge()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let items = Container.get_visible() | ||||
|  | ||||
|         if (!items.length) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (items.length === 1) { | ||||
|             this.single(items[0]) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let selected = this.get() | ||||
|         let history = this.history() | ||||
|         let prev = history[0] | ||||
|         let prev_index = items.indexOf(prev) | ||||
|         let first_index = items.indexOf(selected[0]) | ||||
|  | ||||
|         if (!selected.length) { | ||||
|             let item | ||||
|  | ||||
|             if (direction === `up`) { | ||||
|                 item = Utils.last(items) | ||||
|             } | ||||
|             else if (direction === `down`) { | ||||
|                 item = items[0] | ||||
|             } | ||||
|  | ||||
|             this.single(item) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             if (shift) { | ||||
|                 let item = items[prev_index - 1] | ||||
|  | ||||
|                 if (!item) { | ||||
|                     return | ||||
|                 } | ||||
|  | ||||
|                 this.range(item) | ||||
|             } | ||||
|             else { | ||||
|                 let item | ||||
|  | ||||
|                 if (selected.length > 1) { | ||||
|                     item = selected[0] | ||||
|                 } | ||||
|                 else { | ||||
|                     let index = first_index - 1 | ||||
|  | ||||
|                     if (index < 0) { | ||||
|                         index = items.length - 1 | ||||
|                     } | ||||
|  | ||||
|                     item = items[index] | ||||
|                 } | ||||
|  | ||||
|                 if (!item) { | ||||
|                     return | ||||
|                 } | ||||
|  | ||||
|                 this.single(item) | ||||
|             } | ||||
|         } | ||||
|         else if (direction === `down`) { | ||||
|             if (shift) { | ||||
|                 let item = items[prev_index + 1] | ||||
|  | ||||
|                 if (!item) { | ||||
|                     return | ||||
|                 } | ||||
|  | ||||
|                 this.range(item) | ||||
|             } | ||||
|             else { | ||||
|                 let item | ||||
|  | ||||
|                 if (selected.length > 1) { | ||||
|                     item = Utils.last(selected) | ||||
|                 } | ||||
|                 else { | ||||
|                     let index = first_index + 1 | ||||
|  | ||||
|                     if (index >= items.length) { | ||||
|                         index = 0 | ||||
|                     } | ||||
|  | ||||
|                     item = items[index] | ||||
|                 } | ||||
|  | ||||
|                 if (!item) { | ||||
|                     return | ||||
|                 } | ||||
|  | ||||
|                 this.single(item) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static history() { | ||||
|         let items = this.get() | ||||
|  | ||||
|         items = items.filter(item => { | ||||
|             return parseInt(item.dataset.selected_id) > 0 | ||||
|         }) | ||||
|  | ||||
|         items.sort((a, b) => { | ||||
|             return parseInt(b.dataset.selected_id) - parseInt(a.dataset.selected_id) | ||||
|         }) | ||||
|  | ||||
|         return items | ||||
|     } | ||||
|  | ||||
|     static get() { | ||||
|         return DOM.els(`#container .item.${this.selected_class}`) | ||||
|     } | ||||
|  | ||||
|     static get_curls() { | ||||
|         let selected = this.get() | ||||
|         return selected.map(item => Container.extract_curl(item)) | ||||
|     } | ||||
|  | ||||
|     static id_item(item) { | ||||
|         this.selected_id += 1 | ||||
|         item.dataset.selected_id = this.selected_id | ||||
|     } | ||||
|  | ||||
|     static select(item, set_id = false) { | ||||
|         item.classList.add(this.selected_class) | ||||
|  | ||||
|         if (set_id) { | ||||
|             this.id_item(item) | ||||
|         } | ||||
|  | ||||
|         Utils.scroll_element({item: item}) | ||||
|         Infobar.update_curls() | ||||
|     } | ||||
|  | ||||
|     static deselect(item) { | ||||
|         item.classList.remove(this.selected_class) | ||||
|         item.dataset.selected_id = 0 | ||||
|         Infobar.update_curls() | ||||
|     } | ||||
|  | ||||
|     static toggle(item) { | ||||
|         if (item.classList.contains(this.selected_class)) { | ||||
|             this.deselect(item) | ||||
|         } | ||||
|         else { | ||||
|             this.select(item, true) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static deselect_all() { | ||||
|         let items = this.get() | ||||
|  | ||||
|         for (let item of items) { | ||||
|             this.deselect(item) | ||||
|         } | ||||
|  | ||||
|         this.selected_id = 0 | ||||
|     } | ||||
|  | ||||
|     static single(item) { | ||||
|         this.deselect_all() | ||||
|         this.selected_id = 0 | ||||
|         this.select(item, true) | ||||
|     } | ||||
|  | ||||
|     static mousedown(e) { | ||||
|         let item = Container.extract_item(e) | ||||
|  | ||||
|         if (item) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.mouse_down = true | ||||
|         this.mouse_selected = false | ||||
|         e.preventDefault() | ||||
|     } | ||||
|  | ||||
|     static mouseup() { | ||||
|         this.mouse_down = false | ||||
|         this.mouse_selected = false | ||||
|     } | ||||
|  | ||||
|     static mouseover(e) { | ||||
|         if (!this.mouse_down) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let item = Container.extract_item(e) | ||||
|  | ||||
|         if (!item) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let items = Container.get_visible() | ||||
|         let index = items.indexOf(item) | ||||
|  | ||||
|         for (let i = 0; i < items.length; i++) { | ||||
|             if (i < index) { | ||||
|                 this.deselect(items[i]) | ||||
|             } | ||||
|             else { | ||||
|                 this.select(items[i]) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.id_item(item) | ||||
|     } | ||||
|  | ||||
|     static all() { | ||||
|         let items = Container.get_items() | ||||
|  | ||||
|         for (let item of items) { | ||||
|             if (Container.is_visible(item)) { | ||||
|                 this.select(item) | ||||
|             } | ||||
|             else { | ||||
|                 this.deselect(item) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static toggle_all() { | ||||
|         let visible = Container.get_visible() | ||||
|         let selected = this.get() | ||||
|  | ||||
|         if (selected.length === visible.length) { | ||||
|             this.deselect_all() | ||||
|         } | ||||
|         else { | ||||
|             this.all() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										127
									
								
								server/static/dashboard/js/main/sort.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								server/static/dashboard/js/main/sort.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| /* | ||||
|  | ||||
| This sorts the container | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Sort { | ||||
|     static default_mode = `recent` | ||||
|     static ls_name = `sort` | ||||
|  | ||||
|     static modes = [ | ||||
|         {value: `order`, name: `Order`, info: `Keep the custom order of items`}, | ||||
|         {value: Utils.separator}, | ||||
|         {value: `recent`, name: `Recent`, info: `Sort by the date when curls were last updated`}, | ||||
|         {value: `added`, name: `Added`, info: `Sort by the date when curls were added by you`}, | ||||
|         {value: Utils.separator}, | ||||
|         {value: `curls`, name: `Curls`, info: `Sort curls alphabetically`}, | ||||
|         {value: `status`, name: `Status`, info: `Sort status alphabetically`}, | ||||
|         {value: Utils.separator}, | ||||
|         {value: `active`, name: `Active`, info: `Sort by the number of changes`}, | ||||
|     ] | ||||
|  | ||||
|     static setup() { | ||||
|         let sort = DOM.el(`#sort`) | ||||
|         this.mode = this.load_sort() | ||||
|  | ||||
|         this.combo = new Combo({ | ||||
|             title: `Sort Modes`, | ||||
|             items: this.modes, | ||||
|             value: this.mode, | ||||
|             element: sort, | ||||
|             default: this.default_mode, | ||||
|             action: (value) => { | ||||
|                 this.change(value) | ||||
|             }, | ||||
|             get: () => { | ||||
|                 return this.mode | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static set_value(value) { | ||||
|         if (this.mode === value) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.combo.set_value(value) | ||||
|     } | ||||
|  | ||||
|     static change(value) { | ||||
|         if (this.mode === value) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.mode = value | ||||
|         Utils.save(this.ls_name, value) | ||||
|  | ||||
|         if (this.mode === `order`) { | ||||
|             Container.save_curls() | ||||
|         } | ||||
|         else { | ||||
|             Container.update() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static sort_if_order() { | ||||
|         if (this.mode == `order`) { | ||||
|             Container.update() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static sort(items) { | ||||
|         let mode = this.mode | ||||
|  | ||||
|         if (mode === `order`) { | ||||
|             let curls = Curls.get_curls() | ||||
|  | ||||
|             items.sort((a, b) => { | ||||
|                 let a_index = curls.indexOf(a.curl) | ||||
|                 let b_index = curls.indexOf(b.curl) | ||||
|                 return a_index - b_index | ||||
|             }) | ||||
|         } | ||||
|         else if (mode === `recent`) { | ||||
|             items.sort((a, b) => { | ||||
|                 let compare = b.updated.localeCompare(a.updated) | ||||
|                 return compare !== 0 ? compare : a.curl.localeCompare(b.curl) | ||||
|             }) | ||||
|         } | ||||
|         else if (mode === `added`) { | ||||
|             let items_ = Curls.get() | ||||
|  | ||||
|             items.sort((a, b) => { | ||||
|                 let item_a = Utils.find_item(items_, `curl`, a.curl) | ||||
|                 let item_b = Utils.find_item(items_, `curl`, b.curl) | ||||
|                 let diff = item_b.added - item_a.added | ||||
|  | ||||
|                 if (diff !== 0) { | ||||
|                     return diff | ||||
|                 } | ||||
|  | ||||
|                 return a.curl.localeCompare(b.curl) | ||||
|             }) | ||||
|         } | ||||
|         else if (mode === `curls`) { | ||||
|             items.sort((a, b) => { | ||||
|                 return a.curl.localeCompare(b.curl) | ||||
|             }) | ||||
|         } | ||||
|         else if (mode === `status`) { | ||||
|             items.sort((a, b) => { | ||||
|                 let compare = a.status.localeCompare(b.status) | ||||
|                 return compare !== 0 ? compare : a.curl.localeCompare(b.curl) | ||||
|             }) | ||||
|         } | ||||
|         else if (mode === `active`) { | ||||
|             items.sort((a, b) => { | ||||
|                 let compare = b.changes - a.changes | ||||
|                 return compare !== 0 ? compare : a.curl.localeCompare(b.curl) | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static load_sort() { | ||||
|         return Utils.load_modes(this.ls_name, this.modes, this.default_mode) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								server/static/dashboard/js/main/status.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/static/dashboard/js/main/status.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| /* | ||||
|  | ||||
| This stores status items | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Status { | ||||
|     static max_items = 100 | ||||
|     static ls_items = `status_items` | ||||
|  | ||||
|     static setup() { | ||||
|         let status = this.get_status() | ||||
|         let button = DOM.el(`#status_button`) | ||||
|  | ||||
|         DOM.ev(status, `keyup`, (e) => { | ||||
|             if (e.key === `Enter`) { | ||||
|                 Change.change() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(status, `wheel`, (e) => { | ||||
|             Utils.scroll_wheel(e) | ||||
|         }) | ||||
|  | ||||
|         status.value = `` | ||||
|  | ||||
|         let lines = [ | ||||
|             `Enter the new status of the curl`, | ||||
|             `Press Enter to submit the change`, | ||||
|             `Press Escape to clear`, | ||||
|         ] | ||||
|  | ||||
|         status.title = lines.join(`\n`) | ||||
|  | ||||
|         this.list = new List( | ||||
|             button, | ||||
|             status, | ||||
|             this.ls_items, | ||||
|             this.max_items, | ||||
|             (value) => { | ||||
|                 this.action(value) | ||||
|             }, | ||||
|             () => { | ||||
|                 this.clear() | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     static save(status) { | ||||
|         this.list.save(status) | ||||
|     } | ||||
|  | ||||
|     static get_items() { | ||||
|         return Utils.load_array(this.ls_items) | ||||
|     } | ||||
|  | ||||
|     static focus() { | ||||
|         this.get_status().focus() | ||||
|     } | ||||
|  | ||||
|     static clear() { | ||||
|         this.get_status().value = `` | ||||
|     } | ||||
|  | ||||
|     static get_status() { | ||||
|         return DOM.el(`#change_status`) | ||||
|     } | ||||
|  | ||||
|     static action(value) { | ||||
|         let status = this.get_status() | ||||
|         status.value = value | ||||
|         status.focus() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								server/static/dashboard/js/main/storage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/static/dashboard/js/main/storage.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| class Storage { | ||||
|     static debouncer_delay = 250 | ||||
|  | ||||
|     static setup = () => { | ||||
|         this.debouncer = Utils.create_debouncer((key) => { | ||||
|             this.do_check(key) | ||||
|         }, this.debouncer_delay) | ||||
|  | ||||
|         window.addEventListener(`storage`, (e) => { | ||||
|             this.check(e.key) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static check = (key) => { | ||||
|         this.debouncer.call(key) | ||||
|     } | ||||
|  | ||||
|     static do_check = (key) => { | ||||
|         if (key.startsWith(`curls_data`)) { | ||||
|             Curls.fill_colors() | ||||
|             Update.update() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										209
									
								
								server/static/dashboard/js/main/update.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								server/static/dashboard/js/main/update.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| /* | ||||
|  | ||||
| Update manager | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Update { | ||||
|     static default_mode = `minutes_5` | ||||
|     static enabled = false | ||||
|     static delay = Utils.MINUTE * 5 | ||||
|     static debouncer_delay = 250 | ||||
|     static updating = false | ||||
|     static clear_delay = 800 | ||||
|     static ls_name = `update` | ||||
|     static last_update = 0 | ||||
|  | ||||
|     static modes = [ | ||||
|         {value: `now`, name: `Update`, skip: true, info: `Update now`}, | ||||
|         {value: Utils.separator}, | ||||
|         {value: `minutes_1`}, | ||||
|         {value: `minutes_5`}, | ||||
|         {value: `minutes_10`}, | ||||
|         {value: `minutes_20`}, | ||||
|         {value: `minutes_30`}, | ||||
|         {value: `minutes_60`}, | ||||
|         {value: Utils.separator}, | ||||
|         {value: `disabled`, name: `Disabled`, info: `Do not update automatically`}, | ||||
|     ] | ||||
|  | ||||
|     static setup() { | ||||
|         let updater = DOM.el(`#updater`) | ||||
|         this.mode = this.load_update() | ||||
|         this.fill_modes() | ||||
|  | ||||
|         this.combo = new Combo({ | ||||
|             title: `Update Modes`, | ||||
|             items: this.modes, | ||||
|             value: this.mode, | ||||
|             element: updater, | ||||
|             default: this.default_mode, | ||||
|             action: (value) => { | ||||
|                 this.change(value) | ||||
|             }, | ||||
|             get: () => { | ||||
|                 return this.mode | ||||
|             }, | ||||
|         }) | ||||
|  | ||||
|         this.debouncer = Utils.create_debouncer(async (args) => { | ||||
|             await this.do_update(args) | ||||
|             this.restart() | ||||
|         }, this.debouncer_delay) | ||||
|  | ||||
|         this.check() | ||||
|     } | ||||
|  | ||||
|     static load_update() { | ||||
|         return Utils.load_modes(this.ls_name, this.modes, this.default_mode) | ||||
|     } | ||||
|  | ||||
|     static check() { | ||||
|         let mode = this.mode | ||||
|  | ||||
|         if (mode.startsWith(`minutes_`)) { | ||||
|             let minutes = parseInt(mode.split(`_`)[1]) | ||||
|             this.delay = Utils.MINUTE * minutes | ||||
|             this.enabled = true | ||||
|         } | ||||
|         else { | ||||
|             this.enabled = false | ||||
|         } | ||||
|  | ||||
|         this.restart() | ||||
|     } | ||||
|  | ||||
|     static restart() { | ||||
|         clearTimeout(this.timeout) | ||||
|  | ||||
|         if (this.enabled) { | ||||
|             this.start_timeout() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static start_timeout() { | ||||
|         this.timeout = setTimeout(() => { | ||||
|             this.update() | ||||
|         }, this.delay) | ||||
|     } | ||||
|  | ||||
|     static update(args) { | ||||
|         this.debouncer.call(args) | ||||
|     } | ||||
|  | ||||
|     static async do_update(args = {}) { | ||||
|         this.debouncer.cancel() | ||||
|  | ||||
|         let def_args = { | ||||
|             curls: [], | ||||
|         } | ||||
|  | ||||
|         Utils.def_args(def_args, args) | ||||
|         Utils.info(`Update: Trigger`) | ||||
|  | ||||
|         if (this.updating) { | ||||
|             Utils.error(`Slow down`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let add = false | ||||
|  | ||||
|         if (args.curls.length) { | ||||
|             add = true | ||||
|         } | ||||
|         else { | ||||
|             args.curls = Curls.get_curls() | ||||
|         } | ||||
|  | ||||
|         if (!args.curls.length) { | ||||
|             Container.show_empty() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let url = `/curls` | ||||
|         let params = new URLSearchParams() | ||||
|  | ||||
|         for (let curl of args.curls) { | ||||
|             params.append(`curl`, curl); | ||||
|         } | ||||
|  | ||||
|         this.show_updating() | ||||
|         let response = `` | ||||
|         this.updating = true | ||||
|         Utils.info(`Update: Request ${Utils.network} (${args.curls.length})`) | ||||
|  | ||||
|         if (!Items.list.length) { | ||||
|             Container.show_loading() | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             response = await fetch(url, { | ||||
|                 method: `POST`, | ||||
|                 headers: { | ||||
|                     "Content-Type": `application/x-www-form-urlencoded` | ||||
|                 }, | ||||
|                 body: params, | ||||
|             }) | ||||
|         } | ||||
|         catch (e) { | ||||
|             Utils.error(`Failed to update`) | ||||
|             this.hide_updating() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             let items = await response.json() | ||||
|             this.last_update = Utils.now() | ||||
|  | ||||
|             if (add) { | ||||
|                 Container.add(items, args.curls) | ||||
|             } | ||||
|             else { | ||||
|                 Container.insert(items) | ||||
|             } | ||||
|         } | ||||
|         catch (e) { | ||||
|             Utils.error(`Failed to parse response`) | ||||
|             Utils.error(e) | ||||
|         } | ||||
|  | ||||
|         this.hide_updating() | ||||
|     } | ||||
|  | ||||
|     static show_updating() { | ||||
|         let button = DOM.el(`#updater`) | ||||
|         clearTimeout(this.clear_timeout) | ||||
|         button.classList.add(`active`) | ||||
|     } | ||||
|  | ||||
|     static hide_updating() { | ||||
|         this.updating = false | ||||
|  | ||||
|         this.clear_timeout = setTimeout(() => { | ||||
|             let button = DOM.el(`#updater`) | ||||
|             button.classList.remove(`active`) | ||||
|         }, this.clear_delay) | ||||
|     } | ||||
|  | ||||
|     static change(mode) { | ||||
|         if (mode === `now`) { | ||||
|             this.update() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.mode = mode | ||||
|         Utils.save(this.ls_name, mode) | ||||
|         this.check() | ||||
|     } | ||||
|  | ||||
|     static fill_modes() { | ||||
|         for (let mode of this.modes) { | ||||
|             if (mode.value.startsWith(`minutes_`)) { | ||||
|                 let minutes = parseInt(mode.value.split(`_`)[1]) | ||||
|                 let word = Utils.plural(minutes, `minute`, `minutes`) | ||||
|                 mode.name = `${minutes} ${word}` | ||||
|                 mode.info = `Update automatically every ${minutes} ${word}` | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										287
									
								
								server/static/dashboard/js/main/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								server/static/dashboard/js/main/utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| /* | ||||
|  | ||||
| These are some utility functions | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Utils { | ||||
|     static console_logs = true | ||||
|     static SECOND = 1000 | ||||
|     static MINUTE = this.SECOND * 60 | ||||
|     static HOUR = this.MINUTE * 60 | ||||
|     static DAY = this.HOUR * 24 | ||||
|     static WEEK = this.DAY * 7 | ||||
|     static MONTH = this.DAY * 30 | ||||
|     static YEAR = this.DAY * 365 | ||||
|     static scroll_wheel_step = 25 | ||||
|  | ||||
|     static network = `🛜` | ||||
|     static separator = `__separator__` | ||||
|     static curl_too_long = `Curl is too long` | ||||
|     static key_too_long = `Key is too long` | ||||
|     static status_too_long = `Status is too long` | ||||
|  | ||||
|     static deselect() { | ||||
|         window.getSelection().removeAllRanges() | ||||
|     } | ||||
|  | ||||
|     static plural(n, singular, plural) { | ||||
|         if (n === 1) { | ||||
|             return singular | ||||
|         } | ||||
|         else { | ||||
|             return plural | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static info(msg) { | ||||
|         if (this.console_logs) { | ||||
|             // eslint-disable-next-line no-console | ||||
|             console.info(`💡 ${msg}`) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static error(msg) { | ||||
|         if (this.console_logs) { | ||||
|             // eslint-disable-next-line no-console | ||||
|             console.info(`❌ ${msg}`) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static sanitize(s) { | ||||
|         return s.replace(/</g, `<`).replace(/>/g, `>`) | ||||
|     } | ||||
|  | ||||
|     static urlize(el) { | ||||
|         let html = el.innerHTML | ||||
|         let urlRegex = /(https?:\/\/[^\s]+)/g | ||||
|         let replacedText = html.replace(urlRegex, `<a href="$1" target="_blank">$1</a>`) | ||||
|         el.innerHTML = replacedText | ||||
|     } | ||||
|  | ||||
|     static create_debouncer(func, delay) { | ||||
|         if (typeof func !== `function`) { | ||||
|             this.error(`Invalid debouncer function`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         if (!delay) { | ||||
|             this.error(`Invalid debouncer delay`) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let timer | ||||
|         let obj = {} | ||||
|  | ||||
|         let clear = () => { | ||||
|             clearTimeout(timer) | ||||
|         } | ||||
|  | ||||
|         let run = (...args) => { | ||||
|             func(...args) | ||||
|         } | ||||
|  | ||||
|         obj.call = (...args) => { | ||||
|             clear() | ||||
|  | ||||
|             timer = setTimeout(() => { | ||||
|                 run(...args) | ||||
|             }, delay) | ||||
|         } | ||||
|  | ||||
|         obj.now = (...args) => { | ||||
|             clear() | ||||
|             run(...args) | ||||
|         } | ||||
|  | ||||
|         obj.cancel = () => { | ||||
|             clear() | ||||
|         } | ||||
|  | ||||
|         return obj | ||||
|     } | ||||
|  | ||||
|     static wheel_direction(e) { | ||||
|         if (e.deltaY > 0) { | ||||
|             return `down` | ||||
|         } | ||||
|         else { | ||||
|             return `up` | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static capitalize(s) { | ||||
|         return s.charAt(0).toUpperCase() + s.slice(1) | ||||
|     } | ||||
|  | ||||
|     static now() { | ||||
|         return Date.now() | ||||
|     } | ||||
|  | ||||
|     static copy_to_clipboard(text) { | ||||
|         navigator.clipboard.writeText(text) | ||||
|     } | ||||
|  | ||||
|     static smart_list(string) { | ||||
|         return string.split(/[\s,;]+/).filter(Boolean) | ||||
|     } | ||||
|  | ||||
|     static clean_modes(modes) { | ||||
|         return modes.filter(x => x.value !== Utils.separator) | ||||
|     } | ||||
|  | ||||
|     static def_args(def, args) { | ||||
|         for (let key in def) { | ||||
|             if ((args[key] === undefined) && (def[key] !== undefined)) { | ||||
|                 args[key] = def[key] | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static scroll_element(args = {}) { | ||||
|         let def_args = { | ||||
|             behavior: `instant`, | ||||
|             block: `nearest`, | ||||
|         } | ||||
|  | ||||
|         if (!args.item) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         this.def_args(def_args, args) | ||||
|         args.item.scrollIntoView({ behavior: args.behavior, block: args.block }) | ||||
|         window.scrollTo(0, window.scrollY) | ||||
|     } | ||||
|  | ||||
|     static last(list) { | ||||
|         return list.slice(-1)[0] | ||||
|     } | ||||
|  | ||||
|     static load_modes(name, modes, def_value) { | ||||
|         let saved = localStorage.getItem(name) || def_value | ||||
|         let values = this.clean_modes(modes).map(x => x.value) | ||||
|  | ||||
|         if (!values.includes(saved)) { | ||||
|             saved = def_value | ||||
|         } | ||||
|  | ||||
|         return saved | ||||
|     } | ||||
|  | ||||
|     static load_boolean(name, positive = true) { | ||||
|         let value = positive ? `true` : `false` | ||||
|         let saved = localStorage.getItem(name) || value | ||||
|         return saved === `true` | ||||
|     } | ||||
|  | ||||
|     static load_array(name) { | ||||
|         try { | ||||
|             return JSON.parse(localStorage.getItem(name) || `[]`) | ||||
|         } | ||||
|         catch (err) { | ||||
|             this.error(err) | ||||
|             return [] | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static load_string(name, def_value = ``) { | ||||
|         return localStorage.getItem(name) || def_value | ||||
|     } | ||||
|  | ||||
|     static save(name, value) { | ||||
|         localStorage.setItem(name, value) | ||||
|     } | ||||
|  | ||||
|     static context(args) { | ||||
|         let def_args = { | ||||
|             focus: true, | ||||
|         } | ||||
|  | ||||
|         this.def_args(def_args, args) | ||||
|  | ||||
|         if (args.focus) { | ||||
|             args.after_hide = () => { | ||||
|                 if (args.input) { | ||||
|                     args.input.focus() | ||||
|                 } | ||||
|                 else { | ||||
|                     Container.focus() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         NeedContext.show({ | ||||
|             e: args.e, | ||||
|             items: args.items, | ||||
|             element: args.element, | ||||
|             after_hide: args.after_hide, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static timeago = (date) => { | ||||
|         let diff = this.now() - date | ||||
|         let decimals = true | ||||
|  | ||||
|         let n = 0 | ||||
|         let m = `` | ||||
|  | ||||
|         if (diff < this.SECOND) { | ||||
|             return `Just Now` | ||||
|         } | ||||
|         else if (diff < this.MINUTE) { | ||||
|             n = diff / this.SECOND | ||||
|             m = [`second`, `seconds`] | ||||
|             decimals = false | ||||
|         } | ||||
|         else if (diff < this.HOUR) { | ||||
|             n = diff / this.MINUTE | ||||
|             m = [`minute`, `minutes`] | ||||
|             decimals = false | ||||
|         } | ||||
|         else if (diff >= this.HOUR && diff < this.DAY) { | ||||
|             n = diff / this.HOUR | ||||
|             m = [`hour`, `hours`] | ||||
|         } | ||||
|         else if (diff >= this.DAY && diff < this.MONTH) { | ||||
|             n = diff / this.DAY | ||||
|             m = [`day`, `days`] | ||||
|         } | ||||
|         else if (diff >= this.MONTH && diff < this.YEAR) { | ||||
|             n = diff / this.MONTH | ||||
|             m = [`month`, `months`] | ||||
|         } | ||||
|         else if (diff >= this.YEAR) { | ||||
|             n = diff / this.YEAR | ||||
|             m = [`year`, `years`] | ||||
|         } | ||||
|  | ||||
|         if (decimals) { | ||||
|             n = this.round(n, 1) | ||||
|         } | ||||
|         else { | ||||
|             n = Math.round(n) | ||||
|         } | ||||
|  | ||||
|         let w = this.plural(n, m[0], m[1]) | ||||
|         return `${n} ${w} ago` | ||||
|     } | ||||
|  | ||||
|     static round = (n, decimals) => { | ||||
|         return Math.round(n * Math.pow(10, decimals)) / Math.pow(10, decimals) | ||||
|     } | ||||
|  | ||||
|     static find_item(items, key, value) { | ||||
|         return items.find(x => x[key] === value) | ||||
|     } | ||||
|  | ||||
|     static scroll_wheel(e) { | ||||
|         let direction = this.wheel_direction(e) | ||||
|  | ||||
|         if (direction === `up`) { | ||||
|             e.target.scrollLeft -= this.scroll_wheel_step | ||||
|         } | ||||
|         else { | ||||
|             e.target.scrollLeft += this.scroll_wheel_step | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										213
									
								
								server/static/dashboard/js/main/windows.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								server/static/dashboard/js/main/windows.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | ||||
| /* | ||||
|  | ||||
| This creates and shows modal windows | ||||
|  | ||||
| */ | ||||
|  | ||||
| class Windows { | ||||
|     static max_items = 1000 | ||||
|     static popup_delay = 2750 | ||||
|  | ||||
|     static setup() { | ||||
|         this.make_alert() | ||||
|         this.make_prompt() | ||||
|         this.make_confirm() | ||||
|     } | ||||
|  | ||||
|     static create() { | ||||
|         let common = { | ||||
|             enable_titlebar: true, | ||||
|             center_titlebar: true, | ||||
|             window_x: `none`, | ||||
|             after_close: () => { | ||||
|                 Container.focus() | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         return Msg.factory(Object.assign({}, common, { | ||||
|             class: `modal`, | ||||
|         })) | ||||
|     } | ||||
|  | ||||
|     static make_alert() { | ||||
|         this.alert_window = this.create() | ||||
|         let template = DOM.el(`#alert_template`) | ||||
|         let html = template.innerHTML | ||||
|         this.alert_window.set(html) | ||||
|         let copy = DOM.el(`#alert_copy`) | ||||
|  | ||||
|         DOM.ev(copy, `click`, () => { | ||||
|             this.alert_copy() | ||||
|         }) | ||||
|  | ||||
|         let ok = DOM.el(`#alert_ok`) | ||||
|  | ||||
|         DOM.ev(ok, `click`, (e) => { | ||||
|             this.alert_window.close() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static make_prompt() { | ||||
|         this.prompt_window = this.create() | ||||
|         let template = DOM.el(`#prompt_template`) | ||||
|         let html = template.innerHTML | ||||
|         this.prompt_window.set(html) | ||||
|         this.prompt_window.set_title(`Prompt`) | ||||
|         let submit = DOM.el(`#prompt_submit`) | ||||
|         let input = DOM.el(`#prompt_input`) | ||||
|  | ||||
|         DOM.ev(submit, `click`, () => { | ||||
|             this.prompt_submit() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(input, `keydown`, (e) => { | ||||
|             if (e.key === `Enter`) { | ||||
|                 this.prompt_submit() | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(input, `wheel`, (e) => { | ||||
|             Utils.scroll_wheel(e) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static make_confirm() { | ||||
|         this.confirm_window = this.create() | ||||
|         let template = DOM.el(`#confirm_template`) | ||||
|         let html = template.innerHTML | ||||
|         this.confirm_window.set(html) | ||||
|         let ok = DOM.el(`#confirm_ok`) | ||||
|  | ||||
|         DOM.ev(ok, `click`, () => { | ||||
|             this.confirm_ok() | ||||
|             this.confirm_window.close() | ||||
|         }) | ||||
|  | ||||
|         DOM.ev(ok, `keydown`, (e) => { | ||||
|             if (e.key === `Enter`) { | ||||
|                 this.confirm_ok() | ||||
|                 this.confirm_window.close() | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     static alert(args = {}) { | ||||
|         let def_args = { | ||||
|             title: `Information`, | ||||
|             message: ``, | ||||
|             copy: false, | ||||
|             ok: true, | ||||
|         } | ||||
|  | ||||
|         Utils.def_args(def_args, args) | ||||
|         this.alert_window.set_title(args.title) | ||||
|         let msg = DOM.el(`#alert_message`) | ||||
|  | ||||
|         if (args.message) { | ||||
|             DOM.show(msg) | ||||
|             msg.textContent = args.message | ||||
|         } | ||||
|         else { | ||||
|             DOM.hide(msg) | ||||
|         } | ||||
|  | ||||
|         let copy = DOM.el(`#alert_copy`) | ||||
|  | ||||
|         if (args.copy) { | ||||
|             DOM.show(copy) | ||||
|         } | ||||
|         else { | ||||
|             DOM.hide(copy) | ||||
|         } | ||||
|  | ||||
|         let ok = DOM.el(`#alert_ok`) | ||||
|  | ||||
|         if (args.ok) { | ||||
|             DOM.show(ok) | ||||
|         } | ||||
|         else { | ||||
|             DOM.hide(ok) | ||||
|         } | ||||
|  | ||||
|         this.alert_window.show() | ||||
|     } | ||||
|  | ||||
|     static alert_copy() { | ||||
|         let text = DOM.el(`#alert_message`) | ||||
|         Utils.copy_to_clipboard(text.textContent) | ||||
|         this.alert_window.close() | ||||
|     } | ||||
|  | ||||
|     static confirm(args = {}) { | ||||
|         let def_args = { | ||||
|             message: ``, | ||||
|         } | ||||
|  | ||||
|         Utils.def_args(def_args, args) | ||||
|         this.confirm_ok = args.ok | ||||
|         this.confirm_window.set_title(args.title) | ||||
|         this.confirm_window.show() | ||||
|         DOM.el(`#confirm_ok`).focus() | ||||
|         let msg = DOM.el(`#confirm_message`) | ||||
|  | ||||
|         if (args.message) { | ||||
|             msg.textContent = args.message | ||||
|             DOM.show(msg) | ||||
|         } | ||||
|         else { | ||||
|             DOM.hide(msg) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static prompt_submit() { | ||||
|         let value = DOM.el(`#prompt_input`).value.trim() | ||||
|         this.prompt_callback(value) | ||||
|         this.prompt_window.close() | ||||
|     } | ||||
|  | ||||
|     static prompt(args = {}) { | ||||
|         let def_args = { | ||||
|             value: ``, | ||||
|             message: ``, | ||||
|         } | ||||
|  | ||||
|         Utils.def_args(def_args, args) | ||||
|         this.prompt_callback = args.callback | ||||
|         let input = DOM.el(`#prompt_input`) | ||||
|         input.value = args.value | ||||
|         this.prompt_window.set_title(args.title) | ||||
|         let msg = DOM.el(`#prompt_message`) | ||||
|  | ||||
|         if (args.message) { | ||||
|             msg.textContent = args.message | ||||
|             DOM.show(msg) | ||||
|         } | ||||
|         else { | ||||
|             DOM.hide(msg) | ||||
|         } | ||||
|  | ||||
|         this.prompt_window.show() | ||||
|         input.focus() | ||||
|     } | ||||
|  | ||||
|     static alert_export(data) { | ||||
|         let data_str = Utils.sanitize(JSON.stringify(data)) | ||||
|         this.alert({title: `Copy the data below`, message: data_str, copy: true, ok: false}) | ||||
|     } | ||||
|  | ||||
|     static popup(message) { | ||||
|         let popup = Msg.factory({ | ||||
|             preset: `popup_autoclose`, | ||||
|             position: `bottomright`, | ||||
|             window_x: `none`, | ||||
|             enable_titlebar: true, | ||||
|             center_titlebar: true, | ||||
|             autoclose_delay: this.popup_delay, | ||||
|             class: `popup`, | ||||
|         }) | ||||
|  | ||||
|         popup.set(message) | ||||
|         popup.set_title(`Information`) | ||||
|         popup.show() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								server/static/icon.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/icon.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 236 KiB | 
							
								
								
									
										60
									
								
								server/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								server/templates/base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <!doctype html> | ||||
|  | ||||
| <head> | ||||
|     <title>{% block title %}{% endblock %}</title> | ||||
|     <link rel="icon" href="{{ url_for('static', filename='icon.jpg') }}"> | ||||
|  | ||||
|     <style> | ||||
|         body, | ||||
|         html { | ||||
|             color: white; | ||||
|             background-color: rgb(12, 12, 12); | ||||
|             font-family: sans-serif; | ||||
|             font-size: 18px; | ||||
|         } | ||||
|  | ||||
|         #main { | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             gap: 0.8rem; | ||||
|             max-width: 777px; | ||||
|             padding: 1rem; | ||||
|             padding-top: 0.66rem; | ||||
|         } | ||||
|  | ||||
|         #captcha { | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             gap: 1rem; | ||||
|         } | ||||
|  | ||||
|         input[type="text"], | ||||
|         input[type="password"] { | ||||
|             color: black; | ||||
|             background-color: white; | ||||
|             padding: 0.25rem; | ||||
|             font-size: 1rem; | ||||
|         } | ||||
|  | ||||
|         input[type="submit"] { | ||||
|             padding: 0.35rem; | ||||
|             font-size: 1rem; | ||||
|             background-color: #6f3eff; | ||||
|             color: white; | ||||
|         } | ||||
|  | ||||
|         a:visited, | ||||
|         a:link, | ||||
|         a:hover { | ||||
|             color: white; | ||||
|         } | ||||
|  | ||||
|         .header { | ||||
|             font-size: 1.6rem; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     {% block body %}{% endblock %} | ||||
| </body> | ||||
							
								
								
									
										34
									
								
								server/templates/change.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								server/templates/change.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %} | ||||
| Change Curl | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <form method="post" action="/change"> | ||||
|     <div id="main"> | ||||
|         <div class="header">Change</div> | ||||
|         <input type="text" id="curl" placeholder="Curl" name="curl" autocomplete="on"> | ||||
|         <input type="password" placeholder="Key" name="key" autocomplete="on"> | ||||
|         <input type="text" placeholder="Status" name="status" autocomplete="on"> | ||||
|         <input type="submit" value="Change"> | ||||
|  | ||||
|         <br><br> | ||||
|  | ||||
|         You can also do this using the terminal: | ||||
|  | ||||
|         <br><br> | ||||
|  | ||||
|         <div> | ||||
|             curl -X POST -d "curl=mycurl&key=mykey&status=mystatus" https://this.website/change | ||||
|         </div> | ||||
|     </div> | ||||
| </form> | ||||
|  | ||||
| <script> | ||||
|     window.onload = () => { | ||||
|         let curl = document.querySelector("#curl") | ||||
|         curl.focus() | ||||
|     } | ||||
| </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										43
									
								
								server/templates/claim.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								server/templates/claim.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %} | ||||
| Claim Curl | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <form method="post" action="/claim"> | ||||
|     <div id="main"> | ||||
|         <div class="header">Claim</div> | ||||
|  | ||||
|         <div> | ||||
|             Curls are single lower-case words. Letters and/or numbers. | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             You will receive a key to be able to control the following curl: | ||||
|         </div> | ||||
|  | ||||
|         <input type="text" id="curl" placeholder="Write the curl you want here" name="curl"> | ||||
|  | ||||
|         <div> | ||||
|             Solve this slightly annoying captcha: | ||||
|         </div> | ||||
|  | ||||
|         <div id="captcha"> | ||||
|             {{ captcha_html(captcha)|safe }} | ||||
|         </div> | ||||
|  | ||||
|         <input type="submit" value="Claim"> | ||||
|     </div> | ||||
| </form> | ||||
|  | ||||
| <script> | ||||
|     window.onload = () => { | ||||
|         let captcha = document.querySelector("#captcha-text") | ||||
|         captcha.placeholder = "Enter the captcha here" | ||||
|  | ||||
|         let curl = document.querySelector("#curl") | ||||
|         curl.focus() | ||||
|     } | ||||
| </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										102
									
								
								server/templates/dashboard.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								server/templates/dashboard.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| <!doctype html> | ||||
|  | ||||
| <head> | ||||
|     <title>Curls Dashboard</title> | ||||
|     <link rel="icon" href="{{ url_for('static', filename='icon.jpg') }}"> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='dashboard/css/style.css') }}"> | ||||
|     <script src="{{ url_for('static', filename='dashboard/js/bundle.libs.js') }}"></script> | ||||
|     <script src="{{ url_for('static', filename='dashboard/js/bundle.main.js') }}"></script> | ||||
|  | ||||
|     <script> | ||||
|         App.version = "{{ version }}"; | ||||
|     </script> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     <div id="main"> | ||||
|         <div id="controls"> | ||||
|             <div class="control_bar"> | ||||
|                 <div class="control_section"> | ||||
|                     <div class="button" id="menu" title="Show the main menu">Menu</div> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="control_section"> | ||||
|                     <div class="button" id="color"></div> | ||||
|                     <div class="button" id="sort"></div> | ||||
|                     <div class="button" id="updater"></div> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="control_section"> | ||||
|                     <div class="button" id="filter_button">F</div> | ||||
|                     <input type="text" id="filter" placeholder="Filter"> | ||||
|                     <div class="button" id="filter_modes">Filters</div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="control_bar"> | ||||
|                 <div class="control_section"> | ||||
|                     <div class="button" id="picker" title="Switch to other curls you own">C</div> | ||||
|                     <input type="text" id="change_curl" placeholder="Curl" title="A curl you own"> | ||||
|                     <input type="password" id="change_key" placeholder="Key" title="The key used to control the curl"> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="control_section"> | ||||
|                     <div class="button" id="status_button">S</div> | ||||
|                     <input type="text" id="change_status" placeholder="Status" spellcheck="true" title="The new status of the curl"> | ||||
|                     <div class="button" id="change_submit" title="Change the status of the curl">></div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div id="container_outer"> | ||||
|             <div id="container" tabindex="0"></div> | ||||
|         </div> | ||||
|  | ||||
|         <div id="footer"> | ||||
|             <div class="footer_item"> | ||||
|                 <div class="button" id="scroller" title="Scroll to the bottom and to the top">Scroll</div> | ||||
|                 <div class="button" id="font"></div> | ||||
|                 <div class="button" id="border"></div> | ||||
|                 <div class="button" id="footer_more">More</div> | ||||
|             </div> | ||||
|  | ||||
|             <div id="infobar"> | ||||
|                 <div id="infobar_curls" class="glow"></div> | ||||
|                 <div class="infobar_separator">|</div> | ||||
|                 <div id="infobar_date" class="glow"></div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="glow noselect" id="version" title="The current version of Curls and the Dashboard">v{{ version }}</div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <template id="alert_template"> | ||||
|         <div class="modal_items"> | ||||
|             <div id="alert_message_container"> | ||||
|                 <div class="modal_message" id="alert_message"></div> | ||||
|             </div> | ||||
|  | ||||
|             <div id="alert_buttons"> | ||||
|                 <div class="modal_button" id="alert_copy">Copy</div> | ||||
|                 <div class="modal_button" id="alert_ok">Okay</div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </template> | ||||
|  | ||||
|     <template id="prompt_template"> | ||||
|         <div class="modal_items"> | ||||
|             <div class="modal_message" id="prompt_message"></div> | ||||
|             <input type="text" id="prompt_input" placeholder="Type something" list="curls_datalist"> | ||||
|             <div class="modal_button" id="prompt_submit">Submit</div> | ||||
|         </div> | ||||
|     </template> | ||||
|  | ||||
|     <template id="confirm_template"> | ||||
|         <div class="modal_items"> | ||||
|             <div class="modal_message" id="confirm_message"></div> | ||||
|             <div class="modal_button" id="confirm_ok" tabindex="0">Confirm</div> | ||||
|         </div> | ||||
|     </template> | ||||
|  | ||||
|     <datalist id="curls_datalist"></datalist> | ||||
| </body> | ||||
							
								
								
									
										41
									
								
								server/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								server/templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %} | ||||
| Curls | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <form method="post" action="/change"> | ||||
|     <div id="main"> | ||||
|         <div class="header">Curls</div> | ||||
|  | ||||
|         <div>A curl is a text status that only you can update.</div> | ||||
|  | ||||
|         <br><br> | ||||
|  | ||||
|         <div>There is no history, comments, or anything, it's just text that you update.</div> | ||||
|  | ||||
|         <br> | ||||
|  | ||||
|         <a href="/claim">Claim your own curl</a> | ||||
|         <a href="/change">Change the status of a curl you already own</a> | ||||
|         <a href="/dashboard">Monitor curls in the Dashboard</a> | ||||
|  | ||||
|         <br> | ||||
|  | ||||
|         <div>To view a curl from a browser: | ||||
|  | ||||
|         <div>Go to: https://this.website/[curl]</div> | ||||
|  | ||||
|         <br> | ||||
|  | ||||
|         <div>To view a curl from the terminal:</div> | ||||
|  | ||||
|         <div>curl https://this.website/[curl]</div> | ||||
|  | ||||
|         <br> | ||||
|  | ||||
|         The response is pure text. | ||||
|     </div> | ||||
| </form> | ||||
| {% endblock %} | ||||
							
								
								
									
										9
									
								
								server/templates/message.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/templates/message.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %} | ||||
| Curls Feedback | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| {{message|safe}} | ||||
| {% endblock %} | ||||
		Reference in New Issue
	
	Block a user
	 Auric Vente
					Auric Vente