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